{"id":55023,"date":"2008-10-09T11:09:00","date_gmt":"2008-10-09T11:09:00","guid":{"rendered":"https:\/\/blogs.technet.microsoft.com\/heyscriptingguy\/2008\/10\/09\/hey-scripting-guy-how-can-i-read-microsoft-excel-metadata\/"},"modified":"2008-10-09T11:09:00","modified_gmt":"2008-10-09T11:09:00","slug":"hey-scripting-guy-how-can-i-read-microsoft-excel-metadata","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/scripting\/hey-scripting-guy-how-can-i-read-microsoft-excel-metadata\/","title":{"rendered":"Hey, Scripting Guy! How Can I Read Microsoft Excel Metadata?"},"content":{"rendered":"<p><img decoding=\"async\" class=\"nearGraphic\" title=\"Hey, Scripting Guy! Question\" height=\"34\" alt=\"Hey, Scripting Guy! Question\" src=\"https:\/\/devblogs.microsoft.com\/wp-content\/uploads\/sites\/29\/2019\/02\/q-for-powertip.jpg\" width=\"34\" align=\"left\" border=\"0\" \/> <\/p>\n<p>Hey, Scripting Guy! I have a VBScript script that reads Microsoft Office Excel metadata. I would like to update the script to Windows PowerShell, but can I do this? I tried using <b>New-Object<\/b> to create the <b>Excel.Application<\/b> object but somehow it does not seem to work. What gives? I thought Windows PowerShell could do everything VBScript can do and more. Am I wrong?<\/p>\n<p>&#8211; SV<\/p>\n<p><img decoding=\"async\" height=\"5\" alt=\"Spacer\" src=\"https:\/\/devblogs.microsoft.com\/scripting\/wp-content\/uploads\/sites\/29\/2019\/05\/spacer.gif\" width=\"5\" border=\"0\" \/><img decoding=\"async\" class=\"nearGraphic\" title=\"Hey, Scripting Guy! Answer\" height=\"34\" alt=\"Hey, Scripting Guy! Answer\" src=\"https:\/\/devblogs.microsoft.com\/wp-content\/uploads\/sites\/29\/2019\/02\/a-for-powertip.jpg\" width=\"34\" align=\"left\" border=\"0\" \/> <\/p>\n<p>Hi SV,<\/p>\n<p>Wow! How many questions can you ask in one question? I am impressed! Let\u2019s see if I can answer them all: yes, a lot, sort of, not really. I think that\u2019s right. Let\u2019s begin at the beginning. You want to update a previously working VBScript script to Windows PowerShell. I appreciate your enthusiasm, but if you have a perfectly fine VBScript script, there is really no need to &#8220;translate&#8221; it to Windows PowerShell. In fact, of the more than 3,000 VBScript scripts I have written, I have actually translated fewer than 100 of them to Windows PowerShell. Generally, I am so excited about the new capabilities of Windows PowerShell that I have not had time to do the translation work. This having been said, I do know of a few companies that have passed down the edict &#8220;no more VBScript,&#8221; and they actually assigned several of the network administrators to translate the existing VBScript scripts to Windows PowerShell. If you are interested in this process, let me know via <a href=\"mailto:scripter@microsoft.com?subject=Questions%20about%20translating%20VBScript%20scripts%20to%20Windows%20PowerShell\">scripter@microsoft.com (in English, if possible)<\/a>, and I will be glad to go into it in more detail. <\/p>\n<p>Anyway, SV, I looked through the archives and found this script, which you may have been referring&nbsp;to:<\/p>\n<pre class=\"codeSample\">On Error Resume Next\nSet objExcel = CreateObject(\"Excel.Application\")\nobjExcel.Visible = True\nSet objWorkbook = objExcel.Workbooks.Open(\"C:\\Scripts\\Test.xls\")\nFor Each strProperty in objWorkbook.BuiltInDocumentProperties\n    Wscript.Echo strProperty.Name &amp; \" - \" &amp; strProperty.Value\nNext<\/pre>\n<p>This script took Greg probably about an hour (or less) to write. The <b>GetExcelMetaData.ps1<\/b> script that follows this paragraph took me <i>two days<\/i> to write, and in the end, I had to get help from my good friend and fellow Microsoftie Luis in Lisbon to solve a very perplexing problem with COM interop assemblies. So I am very glad you wrote and asked this question because I believe it will be instructive for us all. If you want to skip the details, here is the <b>GetExcelMetaData.ps1<\/b> script. If you are a real geek (or geek at heart or geek wannabe), read&nbsp;on:<\/p>\n<pre class=\"codeSample\">$excel = New-Object -ComObject Excel.Application\n$excel.Visible = $false\n$workbook = $excel.Workbooks.Open(\"C:\\test\\excel.xls\")\n$binding = \"System.Reflection.BindingFlags\" -as [type]\nForeach($property in $workbook.BuiltInDocumentProperties)\n{\n  $pn = [System.__ComObject].invokemember(\"name\",$binding::GetProperty,$null,$property,$null)\n  trap [system.exception]\n   {\n     write-host -foreground blue \"Value not found for $pn\"\n    continue\n   }\n  \"$pn`: \" +\n   [System.__ComObject].invokemember(\"value\",$binding::GetProperty,$null,$property,$null)\n }\n$excel.quit()<\/pre>\n<p>So what is all this stuff about Excel metadata anyway? It sounds like something from some space epic: <\/p>\n<p>&#8220;Oh, my! The rebels are attacking the metadata!&#8221; <\/p>\n<p>&#8220;Launch the PowerShell!&#8221; <\/p>\n<p>(Or a scenario to that effect.) <\/p>\n<p>Anyway, in the image that follows this paragraph, you see some of the metadata associated with an Excel spreadsheet. There are actually two ways in Windows Vista to view metadata. When a file is selected in Explorer, some of the more important metadata properties are displayed in summary fashion along the bottom of the screen. A more complete listing is found when you right-click the file, choose properties, and then click the <b>Details<\/b> tab. As you can see, some of this information can be extremely useful. I love adding the comments field and the tags property as they can be used with the search feature in Windows Vista to allow me to easily find particular documents among the billions of files that hide on my 200 GB laptop drive: <\/p>\n<p><img decoding=\"async\" height=\"338\" alt=\"Excel metadata graphic\" src=\"http:\/\/img.microsoft.com\/library\/media\/1033\/technet\/images\/scriptcenter\/qanda\/hsg\/hey1009\/hsg_xlmeta01.jpg\" width=\"450\" border=\"0\" \/> <\/p>\n<p>&nbsp;<\/p>\n<p>Okay, SV, back to your question: Can I do this? Sure you can. The first thing we do in the <b>GetExcelMetaData.ps1<\/b> script is the exact same thing you said you did: We create an instance of the <b>Excel.Application<\/b> object. In VBScript you use the <b>CreateObject<\/b> command, but in Windows PowerShell, we use the <b>New-Object<\/b> cmdlet as seen here: <\/p>\n<pre class=\"codeSample\">$excel = New-Object -ComObject Excel.Application<\/pre>\n<p>So far so good. In the VBScript, Greg made the instance of Excel visible. That is cool, but is not needed for this particular script. After all we only want the metadata from the spreadsheet, not the actual data in the spreadsheet. To keep the spreadsheet invisible, we set the visible property of the <b>Excel.Application<\/b> object to false. This is seen here: <\/p>\n<pre class=\"codeSample\">$excel.Visible = $false<\/pre>\n<p>(Okay, one more easy line of code, and then things are going to get hairy\u2014bail out now if you wish.) The next thing we need to do is to open the Excel spreadsheet. To do this, we use the <b>open<\/b> method from the <b>workbook<\/b> object. The <b>open<\/b> method needs the path to the Microsoft Excel workbook on which you are planning on working. Here is the line of code that opens the workbook: <\/p>\n<pre class=\"codeSample\">$workbook = $excel.Workbooks.Open(\"C:\\test\\excel.xls\")<\/pre>\n<p>Now for the hard part (actually it is not <i>too<\/i> bad). The next thing we do is turn a string into a data type. The reason for doing this is twofold. First, I want to show you how to use this technique. Second, doing this will actually make the code a bit easier to read. What we are really doing here is creating an alias for a data type. The data type in question is the <b>System.Reflection.BindingFlags<\/b> class. If we just assigned the string &#8220;System.Reflection.BindingFlags&#8221; to the variable <b>$binding<\/b>, we would simply have a string and not an instance of the <b>System.Reflection.BindingFlags<\/b> class. The <b>BindingFlags<\/b> are used to control the binding of many classes in the <b>System.Reflection<\/b> and other Microsoft .NET Framework namespaces. We need to use this, so we can gain access to the <b>InvokeMember<\/b> method. You will see this later. To turn a string into a data type, use the <b>-as<\/b> operator and specify that it is <b>-as [type]<\/b>. This is seen here:<\/p>\n<pre class=\"codeSample\">$binding = \"System.Reflection.BindingFlags\" -as [type]<\/pre>\n<p>Now we need to use the <b>ForEach<\/b> statement to walk through the collection of <b>BuiltInDocumentProperties<\/b> from the <b>Excel Workbook<\/b> object. We use the <b>$property<\/b> variable as our enumerator while walking through the collection. This is seen here: <\/p>\n<pre class=\"codeSample\">Foreach($property in $workbook.BuiltInDocumentProperties)<\/pre>\n<p>Now for the two-day problem! I should be able to simply use the following code to obtain the name of each <b>BuiltInDocumentProperties<\/b> from the collection:<\/p>\n<pre class=\"codeSample\">$property.name<\/pre>\n<p>The problem is the way the interop assembly for this particular COM object works. The object comes back and says it is a <b>[System.__ComObject]<\/b>. When you try to use <b>$property.name<\/b>, you get an error that says, &#8220;<b>[System.__ComObject]<\/b> does not have a property named <b>name<\/b>.\u201d Here is where we need to use reflections: <\/p>\n<pre class=\"codeSample\">$pn = [System.__ComObject].invokemember(\"name\",$binding::GetProperty,$null,$property,$null)<\/pre>\n<p>The <b>InvokeMember<\/b> can be used in three different ways (called overloaded). Here is the breakdown of the method call:<\/p>\n<table class=\"dataTable\" id=\"EFH\" cellSpacing=\"0\" cellPadding=\"0\">\n<thead>\n<tr class=\"stdHeader\" vAlign=\"top\">\n<td class=\"\" id=\"colEHH\">Value<\/td>\n<td class=\"\" id=\"colELH\">Meaning<\/td>\n<\/tr>\n<\/thead>\n<tbody>\n<tr class=\"record\" vAlign=\"top\">\n<td class=\"\">\n<p class=\"lastInCell\">name<\/p>\n<\/td>\n<td class=\"\">\n<p class=\"lastInCell\">The name of the property to query.<\/p>\n<\/td>\n<\/tr>\n<tr class=\"evenRecord\" vAlign=\"top\">\n<td class=\"\">\n<p class=\"lastInCell\">$binding::GetProperty<\/p>\n<\/td>\n<td class=\"\">\n<p class=\"lastInCell\">The <b>GetProperty<\/b> static method from the <b>System.Reflection.BindingFlags<\/b> class we created earlier and stored in the <b>$binding<\/b> variable.<\/p>\n<\/td>\n<\/tr>\n<tr class=\"record\" vAlign=\"top\">\n<td class=\"\">\n<p class=\"lastInCell\">$null<\/p>\n<\/td>\n<td class=\"\">\n<p class=\"lastInCell\">The binder object that specifies a set of properties and enables binding. We use <b>$null<\/b> here to use the <b>defaultBinder<\/b>.<\/p>\n<\/td>\n<\/tr>\n<tr class=\"evenRecord\" vAlign=\"top\">\n<td class=\"\">\n<p class=\"lastInCell\">$property<\/p>\n<\/td>\n<td class=\"\">\n<p class=\"lastInCell\">The target object. Here is an instance of a document property from the <b>BuiltInDocumentProperties<\/b> collection.<\/p>\n<\/td>\n<\/tr>\n<tr class=\"record\" vAlign=\"top\">\n<td class=\"\">\n<p class=\"lastInCell\">$null<\/p>\n<\/td>\n<td class=\"\">\n<p class=\"lastInCell\">An array of objects to pass as arguments to the method. This is not needed here because we are querying a property.<\/p>\n<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<div class=\"dataTableBottomMargin\"><\/div>\n<p>If you query for an instance of a document property that does not exist, the object throws an error. This is true even if you are using VBScript. Because we went to all this trouble to query the document properties, we decided to go one step farther and show you how to trap errors with Windows PowerShell. In VBScript the original script used On Error Resume Next and move to the next property in the collection when an error occurred. We could have done exactly the same thing by using <b>$ErrorActionPreference = &#8220;SilentlyContinue&#8221;<\/b>. We decided it would be cool to use the <b>Trap<\/b> statement instead because this is the way errors are handled in C#. When using the <b>Trap<\/b> statement, you need to specify what kind of error to catch. Here we are catching a <b>system.exception<\/b>, which is pretty generic. When the exception is detected, we print out the name of the document property that does not exist in blue (much more friendly than red), and then we continue to the next property. This is seen&nbsp;here:<\/p>\n<pre class=\"codeSample\">trap [system.exception]\n   {\n     write-host -foreground blue \"Value not found for $pn\"\n    continue\n   }<\/pre>\n<p>If the property does exist, we want to print out the name of the property and the value. This is seen here. Note we repeat the same command line that uses <b>invokemember<\/b> as seen earlier. This time, however, we also print out the value of the property and the name of the property, which is contained in the <b>$pn<\/b> variablevariable: <\/p>\n<pre class=\"codeSample\">\"$pn`: \" +\n   [System.__ComObject].invokemember(\"value\",$binding::GetProperty,$null,$property,$null)<\/pre>\n<p>When we have the list of document properties and their values, we simply need to exit Excel (if you do not do this, it will continue to hang around and eat up memory). This line is about the same as in the VBScript script:<\/p>\n<pre class=\"codeSample\">$excel.quit()<\/pre>\n<p>When we run the script, we are greeted with the following output: <\/p>\n<p><img decoding=\"async\" height=\"339\" alt=\"Excel metadata graphic\" src=\"http:\/\/img.microsoft.com\/library\/media\/1033\/technet\/images\/scriptcenter\/qanda\/hsg\/hey1009\/hsg_xlmeta02.jpg\" width=\"450\" border=\"0\" \/> <\/p>\n<p>&nbsp;<\/p>\n<p>So, SV, as you can see, not everything is necessarily easier to do in Windows PowerShell than in VBScript. As I said at the outset, if you have a script that works, leave it alone unless there is some compelling reason to revise it (such as official edicts). This also illustrates why I prefer to use .NET Framework classes when working with Windows PowerShell\u2014in general, they will work easier and perform better.<\/p>\n<p><font class=\"Apple-style-span\" face=\"Verdana\" size=\"3\"><span class=\"Apple-style-span\"><b><b>Ed Wilson and Craig Liebendorfer, Scripting Guys<\/b><\/b><\/span><\/font><\/p>\n<p><font class=\"Apple-style-span\" face=\"Verdana\" size=\"3\"><b><\/b><\/font><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Hey, Scripting Guy! I have a VBScript script that reads Microsoft Office Excel metadata. I would like to update the script to Windows PowerShell, but can I do this? I tried using New-Object to create the Excel.Application object but somehow it does not seem to work. What gives? I thought Windows PowerShell could do everything [&hellip;]<\/p>\n","protected":false},"author":595,"featured_media":87096,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[1],"tags":[48,49,3,45],"class_list":["post-55023","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-scripting","tag-microsoft-excel","tag-office","tag-scripting-guy","tag-windows-powershell"],"acf":[],"blog_post_summary":"<p>Hey, Scripting Guy! I have a VBScript script that reads Microsoft Office Excel metadata. I would like to update the script to Windows PowerShell, but can I do this? I tried using New-Object to create the Excel.Application object but somehow it does not seem to work. What gives? I thought Windows PowerShell could do everything [&hellip;]<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/scripting\/wp-json\/wp\/v2\/posts\/55023","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/scripting\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/scripting\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/scripting\/wp-json\/wp\/v2\/users\/595"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/scripting\/wp-json\/wp\/v2\/comments?post=55023"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/scripting\/wp-json\/wp\/v2\/posts\/55023\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/scripting\/wp-json\/wp\/v2\/media\/87096"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/scripting\/wp-json\/wp\/v2\/media?parent=55023"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/scripting\/wp-json\/wp\/v2\/categories?post=55023"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/scripting\/wp-json\/wp\/v2\/tags?post=55023"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}