{"id":50753,"date":"2010-04-05T00:01:00","date_gmt":"2010-04-05T00:01:00","guid":{"rendered":"https:\/\/blogs.technet.microsoft.com\/heyscriptingguy\/2010\/04\/05\/hey-scripting-guy-how-can-i-search-a-microsoft-excel-workbook-for-links-to-other-workbooks\/"},"modified":"2010-04-05T00:01:00","modified_gmt":"2010-04-05T00:01:00","slug":"hey-scripting-guy-how-can-i-search-a-microsoft-excel-workbook-for-links-to-other-workbooks","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/scripting\/hey-scripting-guy-how-can-i-search-a-microsoft-excel-workbook-for-links-to-other-workbooks\/","title":{"rendered":"Hey, Scripting Guy! How Can I Search a Microsoft Excel Workbook for Links to Other Workbooks?"},"content":{"rendered":"<p><a class=\"addthis_button\" href=\"http:\/\/www.addthis.com\/bookmark.php?v=250&amp;pub=scriptingguys\"><img decoding=\"async\" alt=\"Bookmark and Share\" src=\"http:\/\/s7.addthis.com\/static\/btn\/v2\/lg-share-en.gif\" width=\"125\" height=\"16\"><\/a>&nbsp;<\/p>\n<p>&nbsp;\n<img decoding=\"async\" title=\"Hey, Scripting Guy! Question\" border=\"0\" alt=\"Hey, Scripting Guy! Question\" align=\"left\" src=\"https:\/\/devblogs.microsoft.com\/wp-content\/uploads\/sites\/29\/2019\/02\/q-for-powertip.jpg\" width=\"34\" height=\"34\"><\/p>\n<p class=\"MsoNormal\">Hey, Scripting Guy! I have a user in our finance department that asked me today about using a Windows PowerShell script to identify Microsoft Excel workbooks that have links inside them to other Microsoft Excel workbooks. Frankly, I am not exactly sure what she is talking about, but I do think it is great that she asked me about Windows PowerShell. Can you help me out? I do not want this user to get all depressed the first time she attempts to do something with Windows PowerShell.<\/p>\n<p class=\"MsoNormal\">&#8212; SV<\/p>\n<p class=\"MsoNormal\">&nbsp;<\/p>\n<p class=\"MsoNormal\"><img decoding=\"async\" title=\"Hey, Scripting Guy! Answer\" border=\"0\" alt=\"Hey, Scripting Guy! Answer\" align=\"left\" src=\"https:\/\/devblogs.microsoft.com\/wp-content\/uploads\/sites\/29\/2019\/02\/a-for-powertip.jpg\" width=\"34\" height=\"34\">Hello SV, <\/p>\n<p class=\"MsoNormal\">Microsoft Scripting Guy Ed Wilson here. It has been many years since I created Microsoft Excel workbooks that contained links to other workbooks, or more accurately, links to spreadsheets in other workbooks. I believe I know the reason your user is asking for such a script, and that is because according to the Microsoft Excel online Help, there is no automated method of detecting such links. But if one of the workbooks becomes moved, renamed, or deleted, the workbook containing the link will not work properly. This is shown in the following image.<\/p>\n<p class=\"Fig-Graphic\"><img decoding=\"async\" title=\"Image of broken workbook\" alt=\"Image of broken workbook\" src=\"http:\/\/img.microsoft.com\/library\/media\/1033\/technet\/images\/scriptcenter\/qanda\/hsg\/2010\/april\/hey0405\/hsg-04-05-10-01.jpg\" width=\"524\" height=\"280\"><\/p>\n<p class=\"MsoNormal\">&nbsp;<\/p>\n<p class=\"MsoNormal\">The link to a spreadsheet in another Microsoft Excel workbook looks something like this: <b>=&#8217;C:fso[cdRoms.xlsx]Sheet1&#8242;!$A$1<\/b>. Therefore, it will be possible to write a Windows PowerShell script that will search a workbook for formulas that match that pattern. The Get-LinkedExcelSheets.ps1 script is an example of a script that does this. The complete Get-LinkedExcelSheets.ps1 script is shown here. <\/p>\n<p class=\"CodeBlockScreenedHead\"><strong>Get-LinkedExcelSheets.ps1<\/p>\n<p><\/strong><\/p>\n<p class=\"CodeBlockScreened\"><font><font face=\"Lucida Sans Typewriter\">$path = &#8220;C:fso&#8221;<br \/>$excelSheets = Get-Childitem -Path $path -Include *.xls,*.xlsx -Recurse<br \/>$excel = New-Object -comobject Excel.Application<br \/>$excel.visible = $false<\/p>\n<p>foreach($excelSheet in $excelSheets)<br \/>{<br \/><span>&nbsp;<\/span>$workbook = $excel.Workbooks.Open($excelSheet)<br \/><span>&nbsp;<\/span>&#8220;There are $($workbook.Sheets.count) sheets in $excelSheet&#8221;<\/p>\n<p><span>&nbsp;<\/span>For($i = 1 ; $i -le $workbook.Sheets.count ; $i++)<br \/><span>&nbsp;<\/span>{<br \/><span>&nbsp; <\/span>$worksheet = $workbook.sheets.item($i)<br \/><span>&nbsp; <\/span>&#8220;`tLooking for links on $($worksheet.name) worksheet&#8221;<br \/><span>&nbsp; <\/span>$rowMax = ($worksheet.usedRange.rows).count<br \/><span>&nbsp; <\/span>$columnMax = ($worksheet.usedRange.columns).count<br \/><span>&nbsp; <\/span>For($row = 1 ; $row -le $rowMax ; $row ++)<br \/><span>&nbsp; <\/span>{<br \/><span>&nbsp;&nbsp; <\/span>For($column = 1 ; $column -le $columnMax ; $column ++)<br \/><span>&nbsp;&nbsp;&nbsp; <\/span>{ <br \/><span>&nbsp;&nbsp;&nbsp;&nbsp; <\/span>[string]$formula = $workSheet.cells.item($row,$column).formula<br \/><span>&nbsp;&nbsp;&nbsp;&nbsp; <\/span>if($formula -match &#8220;w?:\\w*\\[w*.xlsw?]&#8221;) {&#8220;`t`t$($formula)&#8221;}<br \/><span>&nbsp;&nbsp;&nbsp; <\/span>} #end for $column<br \/><span>&nbsp;&nbsp; <\/span>} #end for $row<br \/><span>&nbsp; <\/span>$worksheet = $rowmax = $columnMax = $row = $column = $formula = $null<br \/><span>&nbsp;<\/span>} #end for<br \/><span>&nbsp;<\/span>$workbook.saved = $true<br \/><span>&nbsp;<\/span>$workbook.close()<br \/>} #end foreach<br \/><span>&nbsp;<\/span><br \/><span>&nbsp;<\/span>$excel.quit()<br \/><span>&nbsp;<\/span>$excel = $null<br \/><span>&nbsp;<\/span>[gc]::collect()<br \/><span>&nbsp;<\/span>[gc]::WaitForPendingFinalizers()<\/p>\n<p><\/font><\/font><\/p>\n<p class=\"Readeraidonly\">For more information about using Windows PowerShell with the Microsoft Excel automation model, refer to the <a href=\"http:\/\/blogs.technet.com\/heyscriptingguy\/archive\/tags\/Microsoft+Excel\/Windows+PowerShell\/default.aspx\"><font face=\"Segoe\">Microsoft Excel articles<\/font><\/a> on the Hey, Scripting Guy! Blog.<\/p>\n<p class=\"MsoNormal\">To illustrate working with linked Microsoft Excel spreadsheets, I created a couple of Microsoft Excel workbooks containing spreadsheets with links to other spreadsheets in other workbooks. This is shown in the following image.<\/p>\n<p class=\"Fig-Graphic\"><img decoding=\"async\" title=\"Image of Microsoft workbook with links\" alt=\"Image of Microsoft workbook with links\" src=\"http:\/\/img.microsoft.com\/library\/media\/1033\/technet\/images\/scriptcenter\/qanda\/hsg\/2010\/april\/hey0405\/hsg-04-05-10-02.jpg\" width=\"600\" height=\"382\"><\/p>\n<p class=\"Fig-Graphic\">&nbsp;<\/p>\n<p class=\"MsoNormal\">The first thing to do in the Get-LinkedExcelSheets.ps1 script is to assign the path to the folder that contains Microsoft Excel workbooks. The path is assigned to the <b>$path<\/b> variable. Next, use the <b>Get-ChildItem<\/b> cmdlet to retrieve all of the Microsoft Excel spreadsheets. The <b>include<\/b> parameter includes all of the legacy XLS workbooks as well as the new XLSX workbooks. The <b>recurse<\/b> parameter is required to make the <b>Get-ChildItem<\/b> cmdlet return all files that match the search criteria in the folder. This section of the script is shown here:<\/p>\n<p class=\"CodeBlockScreened\"><font><font face=\"Lucida Sans Typewriter\">$path = &#8220;C:fso&#8221;<br \/>$excelSheets = Get-Childitem -Path $path -Include *.xls,*.xlsx -Recurse<\/p>\n<p><\/font><\/font><\/p>\n<p class=\"MsoNormal\">Now it is time to create an instance of the <b>Excel.Application<\/b> COM object. To do this, use the <b>New-Object<\/b> cmdlet and specify the name of the COM object. The application object is the main object that is used when working with Microsoft Excel automation. Because there are possibly many Microsoft Excel workbooks in the folder, it is not desirable to have the Microsoft Excel application appearing and disappearing during the running of the script. Therefore, the <b>visible<\/b> property from the Microsoft Excel application object is set to <b>$false<\/b>. This is shown here:<\/p>\n<p class=\"CodeBlockScreened\"><font><font face=\"Lucida Sans Typewriter\">$excel = New-Object -comobject Excel.Application<br \/>$excel.visible = $false<\/p>\n<p><\/font><\/font><\/p>\n<p class=\"MsoNormal\">When the <b>Excel.Application<\/b> object has been created, and the collection of Microsoft Excel workbooks has been generated, it is time to walk through the collection. To do this, use the <b>ForEach<\/b> statement as shown here:<\/p>\n<p class=\"CodeBlockScreened\"><font><font face=\"Lucida Sans Typewriter\">foreach($excelSheet in $excelSheets)<br \/>{<\/p>\n<p><\/font><\/font><\/p>\n<p class=\"MsoNormal\">Inside the <b>ForEach<\/b> statement, use the <b>Open<\/b> method from the <b>Workbooks<\/b> collection of the <b>Excel.Application<\/b> object. Pass the path that is stored in the <b>$excelSheet<\/b> variable to the <b>Open<\/b> method, and store the returned workbook object in the <b>$workbook<\/b> variable as shown here:<\/p>\n<p class=\"CodeBlockScreened\"><font><font face=\"Lucida Sans Typewriter\"><span>&nbsp;<\/span>$workbook = $excel.Workbooks.Open($excelSheet)<\/p>\n<p><\/font><\/font><\/p>\n<p class=\"MsoNormal\">The <b>sheets<\/b> object, which is obtained from the <b>workbook<\/b> object, contains the <b>count<\/b> property that can be used to determine how many spreadsheets are contained in a Microsoft Excel workbook. This information is displayed in the Windows PowerShell console. A subexpression <b>$() <\/b>is required to prevent the COM object from unraveling inside the double quotation marks. The subexpression forces the value of the expression to be processed before the string is displayed. This is shown here:<\/p>\n<p class=\"CodeBlockScreened\"><font><font face=\"Lucida Sans Typewriter\"><span>&nbsp;<\/span>&#8220;There are $($workbook.Sheets.count) sheets in $excelSheet&#8221;<\/p>\n<p><\/font><\/font><\/p>\n<p class=\"MsoNormal\">Now it is time to walk through the collection of spreadsheets. To do this, the <b>For<\/b> statement is used as shown here:<\/p>\n<p class=\"CodeBlockScreened\"><font><font face=\"Lucida Sans Typewriter\"><span>&nbsp;<\/span>For($i = 1 ; $i -le $workbook.Sheets.count ; $i++)<\/p>\n<p><\/font><\/font><\/p>\n<p class=\"MsoNormal\">The <b>item<\/b> method from the <b>sheets<\/b> collection is used to retrieve a specific spreadsheet. Because the <b>For<\/b> statement will continue to operate once for each of the total number of spreadsheets in the collection, the <b>$i<\/b> variable is used to keep track of where the operation is within the collection. The <b>worksheet<\/b> object is stored in the <b>$worksheet<\/b> variable as shown here:<\/p>\n<p class=\"CodeBlockScreened\"><font><font face=\"Lucida Sans Typewriter\"><span>&nbsp;<\/span>{<br \/><span>&nbsp; <\/span>$worksheet = $workbook.sheets.item($i)<\/p>\n<p><\/font><\/font><\/p>\n<p class=\"MsoNormal\">Next, a status message is displayed on the Windows PowerShell console. The <b>`t<\/b> character is a special character that causes the text to tab over one stop. A subexpression is used to retrieve the name of the worksheet via the <b>name<\/b> property. This is shown here:<\/p>\n<p class=\"CodeBlockScreened\"><font><font face=\"Lucida Sans Typewriter\"><span>&nbsp; <\/span>&#8220;`tLooking for links on $($worksheet.name) worksheet&#8221;<\/p>\n<p><\/font><\/font><\/p>\n<p class=\"MsoNormal\">Now the number of rows and columns that are used within the spreadsheet are calculated via the <b>count<\/b> property of the <b>range<\/b> object. The number of rows and the number of columns are assigned to variables as shown here:<\/p>\n<p class=\"CodeBlockScreened\"><font><font face=\"Lucida Sans Typewriter\"><span>&nbsp; <\/span>$rowMax = ($worksheet.usedRange.rows).count<br \/><span>&nbsp; <\/span>$columnMax = ($worksheet.usedRange.columns).count<\/p>\n<p><\/font><\/font><\/p>\n<p class=\"MsoNormal\">For each used row and each used column, the value of the <b>formula<\/b> property of the cell is retrieved and stored in the <b>$formula<\/b> variable. The <b>[string]<\/b> type is used to ensure the formula is returned as a string so that a regular expression pattern match can be used to look for linked spreadsheets. This is shown here:<\/p>\n<p class=\"CodeBlockScreened\"><font><font face=\"Lucida Sans Typewriter\"><span>&nbsp; <\/span>For($row = 1 ; $row -le $rowMax ; $row ++)<br \/><span>&nbsp; <\/span>{<br \/><span>&nbsp;&nbsp; <\/span>For($column = 1 ; $column -le $columnMax ; $column ++)<br \/><span>&nbsp;&nbsp;&nbsp; <\/span>{ <br \/><span>&nbsp;&nbsp;&nbsp;&nbsp; <\/span>[string]$formula = $workSheet.cells.item($row,$column).formula<\/p>\n<p><\/font><\/font><\/p>\n<p class=\"MsoNormal\">The <b>If<\/b> statement is used to see if the formula stored in the <b>$formula<\/b> variable matches a particular regular expression pattern. The regular expression pattern is not particularly complicated, but it is a bit cumbersome. The regular expression pattern is shown here:<\/p>\n<p class=\"CodeBlockScreened\"><font><font face=\"Lucida Sans Typewriter\">w?:\\w*\\[w*.xlsw?]<\/p>\n<p><\/font><\/font><\/p>\n<p class=\"MsoNormal\">A useful breakdown of the pattern is shown in the following table.\n  <\/p>\n<table class=\"MsoNormalTable\" border=\"1\" cellspacing=\"0\" cellpadding=\"0\">\n<tbody>\n<tr>\n<td valign=\"top\" width=\"168\">\n<div>\n<p class=\"TableHead\"><b>Regular expression<\/p>\n<p><\/b><\/p>\n<\/div>\n<\/td>\n<td valign=\"top\" width=\"258\">\n<div>\n<p class=\"TableHead\"><b>Meaning<\/p>\n<p><\/b><\/p>\n<\/div>\n<\/td>\n<\/tr>\n<tr>\n<td valign=\"top\" width=\"168\">\n<div>\n<p class=\"TableText\">w<\/p>\n<\/div>\n<\/td>\n<td valign=\"top\" width=\"258\">\n<div>\n<p class=\"TableText\">a word character [a-zA-Z_0-9]<\/p>\n<\/div>\n<\/td>\n<\/tr>\n<tr>\n<td valign=\"top\" width=\"168\">\n<div>\n<p class=\"TableText\">?<\/p>\n<\/div>\n<\/td>\n<td valign=\"top\" width=\"258\">\n<div>\n<p class=\"TableText\">one instance of preceding character<\/p>\n<\/div>\n<\/td>\n<\/tr>\n<tr>\n<td valign=\"top\" width=\"168\">\n<div>\n<p class=\"TableText\">: <\/p>\n<\/div>\n<\/td>\n<td valign=\"top\" width=\"258\">\n<div>\n<p class=\"TableText\">literal character<\/p>\n<\/div>\n<\/td>\n<\/tr>\n<tr>\n<td valign=\"top\" width=\"168\">\n<div>\n<p class=\"TableText\">\n<\/div>\n<\/td>\n<td valign=\"top\" width=\"258\">\n<div>\n<p class=\"TableText\">escapes a backslash<\/p>\n<\/div>\n<\/td>\n<\/tr>\n<tr>\n<td valign=\"top\" width=\"168\">\n<div>\n<p class=\"TableText\">\n<\/div>\n<\/td>\n<td valign=\"top\" width=\"258\">\n<div>\n<p class=\"TableText\">the backslash character<\/p>\n<\/div>\n<\/td>\n<\/tr>\n<tr>\n<td valign=\"top\" width=\"168\">\n<div>\n<p class=\"TableText\">w<\/p>\n<\/div>\n<\/td>\n<td valign=\"top\" width=\"258\">\n<div>\n<p class=\"TableText\">a word character [a-zA-Z_0-9]<\/p>\n<\/div>\n<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n","protected":false},"excerpt":{"rendered":"<p>&nbsp; &nbsp; Hey, Scripting Guy! I have a user in our finance department that asked me today about using a Windows PowerShell script to identify Microsoft Excel workbooks that have links inside them to other Microsoft Excel workbooks. Frankly, I am not exactly sure what she is talking about, but I do think it is [&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-50753","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>&nbsp; &nbsp; Hey, Scripting Guy! I have a user in our finance department that asked me today about using a Windows PowerShell script to identify Microsoft Excel workbooks that have links inside them to other Microsoft Excel workbooks. Frankly, I am not exactly sure what she is talking about, but I do think it is [&hellip;]<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/scripting\/wp-json\/wp\/v2\/posts\/50753","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=50753"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/scripting\/wp-json\/wp\/v2\/posts\/50753\/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=50753"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/scripting\/wp-json\/wp\/v2\/categories?post=50753"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/scripting\/wp-json\/wp\/v2\/tags?post=50753"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}