{"id":7549,"date":"2020-06-16T13:32:31","date_gmt":"2020-06-16T17:32:31","guid":{"rendered":"https:\/\/jdhitsolutions.com\/blog\/?p=7549"},"modified":"2020-06-19T10:05:57","modified_gmt":"2020-06-19T14:05:57","slug":"building-a-powershell-inventory","status":"publish","type":"post","link":"https:\/\/jdhitsolutions.com\/blog\/powershell\/7549\/building-a-powershell-inventory\/","title":{"rendered":"Building a PowerShell Inventory"},"content":{"rendered":"<p>A few weeks ago, a new Iron Scripter <a href=\"https:\/\/ironscripter.us\/building-a-powershell-command-inventory\/\" target=\"_blank\" rel=\"noopener noreferrer\">PowerShell scripting challenge was issued<\/a>. For this challenge we were asked to write some PowerShell code that we could use to inventory our PowerShell script library.\u00a0 Here's how I approached the problem, which by no means is the only way.<\/p>\n<h2>Lines of Code<\/h2>\n<p>The first part of the challenge was to count the number of lines of code in PowerShell script files.\u00a0 Any file with a PowerShell-related extension. This is going to require several steps.<\/p>\n<ol>\n<li>Get the PowerShell files<\/li>\n<li>Get the content of each file<\/li>\n<li>Count the number of lines.<\/li>\n<\/ol>\n<p>Get-ChildItem is the likely command to list all the files. Get-Content is the obvious choice to read each file. For counting, Measure-Object has parameters to measure lines of text in a file. But you won't know that unless you are in the habit of reading help files. Here's a possible solution.<\/p>\n<pre class=\"lang:ps mark:0 decode:true\">$Path = \"C:\\scripts\"\nGet-ChildItem -Path $Path -file -Recurse -Filter \"*.ps*\" |\nWhere-Object {$_.Extension -match \"\\.ps(m)?1$\"} -outvariable o |\nGet-Content | Measure-Object -line |\nSelect-Object -Property @{Name=\"Path\";Expression={$Path}},\n@{Name=\"TotalFiles\";Expression = {$o.count}},Lines,\n@{Name=\"Date\";Expression = {Get-Date}}\n<\/pre>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"alignnone size-full wp-image-7550\" src=\"https:\/\/jdhitsolutions.com\/blog\/wp-content\/uploads\/2020\/06\/pscount.png\" alt=\"pscount\" width=\"886\" height=\"179\" srcset=\"https:\/\/jdhitsolutions.com\/blog\/wp-content\/uploads\/2020\/06\/pscount.png 886w, https:\/\/jdhitsolutions.com\/blog\/wp-content\/uploads\/2020\/06\/pscount-300x61.png 300w, https:\/\/jdhitsolutions.com\/blog\/wp-content\/uploads\/2020\/06\/pscount-768x155.png 768w, https:\/\/jdhitsolutions.com\/blog\/wp-content\/uploads\/2020\/06\/pscount-850x172.png 850w\" sizes=\"auto, (max-width: 886px) 100vw, 886px\" \/><\/p>\n<p>This took about 35 seconds to run. I could probably improve performance by using .NET classes directly, but I wanted to stick to cmdlets where I could. By the way, you'll notice I'm essentially filtering twice. You always want to filter as early in your expression as you can. I'm using the -Filter parameter to get what should be PowerShell files. This does the bulk of the filtering. Where-Object is filtering out the odd files like foo.psx using a regular expression pattern.<\/p>\n<p>Here's a multi-step equivalent.<\/p>\n<pre class=\"lang:ps mark:0 decode:true\">$Path = \"C:\\scripts\"\n\n$files = Get-ChildItem -Path $Path -file -Recurse -Filter \"*.ps*\" |\nWhere-Object {$_.Extension -match \"\\.ps(m)?1$\"}\n\n$measure = $files | Get-Content | Measure-Object -line\n[pscustomobject]@{\n    Computername = $env:COMPUTERNAME\n    Date = (Get-Date)\n    Path = $Path\n    TotalFiles = $files.count\n    TotalLines = $measure.lines\n}\n<\/pre>\n<p>Same information structured a different way.<\/p>\n<h2>Command Inventory<\/h2>\n<p>So I have a little over 4700 files. These files go back to 2006. What commands have I been using? That was the second part of the challenge.<\/p>\n<p>My intial thought was to follow a process like this:<\/p>\n<ol>\n<li>Get a list of all possible commands<\/li>\n<li>Get all PowerShell files<\/li>\n<li>Go through the content of each file<\/li>\n<li>Match content to command names with Select-String<\/li>\n<\/ol>\n<p>This brute force approach went something like this:<\/p>\n<pre class=\"lang:ps mark:0 decode:true\">#brute force approach\n#filter out location changing functions\n$cmds = (Get-Command -CommandType Cmdlet,Function).Name.where({$_ -notmatch \":|\\\\|\\.\\.\"}) | Get-Unique\n$cmds| foreach-object -Begin {$cmdHash = @{}} -process {\n  if (-Not $cmdhash.ContainsKey($_)) {$cmdHash.Add($_,0) }\n}\n\n$Path = \"C:\\scripts\"\n$files = (Get-ChildItem -Path $Path -file -Recurse -Filter \"*.ps*\").Where({$_.Extension -match \"\\.ps(m)?1$\"})\n\n[string[]]$keys =  $cmdHash.Keys\n\nforeach ($file in $files) {\n  foreach ($key in $keys) {\n    if (Select-String -path $file.fullname -pattern $key -Quiet) {\n     $cmdhash.item($key)++\n    }\n  } #foreach key\n\n} #foreach file\n\n($cmdhash.GetEnumerator()).WHERE({$_.value -gt 0}) | sort-object -Property Value -Descending | select-object -first 100\n<\/pre>\n<p>But this didn't scale at all and took way too long to run to be practical. Then I thought, why not use one gigantic regex pattern?<\/p>\n<pre class=\"lang:ps mark:0 decode:true\">$r = @{}\n$pester = (get-command -module pester).name\n\n$cmds2 = $cmds.where({$pester -notcontains $_})\n$pattern = $cmds2 -join \"|\"\n[System.Text.RegularExpressions.Regex]::Matches((Get-Content $files[1].FullName),\"$pattern\",\"IgnoreCase\") |\nForeach-object {\n if ($r.ContainsKey($_.value)) {\n    $r.item($($_.value))++\n }\n else {\n    $r.add($($_.value),1)\n }\n}\n$r.GetEnumerator() | Sort-Object Value -descending | Select-Object -first 25\n<\/pre>\n<p>This is using $cmds from the previous example. I also realized I need to filter out the Pester module because commands like It and Should were being falsely detected. This approach was fine for 10 files. It finished in about 9 seconds. It took about 2 minutes to do 1000 files. But all 4800 files took over 2 hours.\u00a0 Sure, I could run as a job and get the results later but I wanted better.<\/p>\n<h2>Using the AST<\/h2>\n<p>So I turned to the AST. This is how PowerShell processes code under the hood. I started out with a proof-of-concept using a single file.<\/p>\n<pre class=\"lang:ps mark:0 decode:true\">New-Variable astTokens -force\nNew-Variable astErr -force\n$path = 'C:\\scripts\\JDH-Functions.ps1'\n$AST = [System.Management.Automation.Language.Parser]::ParseFile($Path,[ref]$astTokens,[ref]$astErr)\n\n$found = $AST.FindAll(\n    {$args[0] -is [System.Management.Automation.Language.CommandAst]},\n    $true\n)\n<\/pre>\n<p>The $asttokens variable contains an object for the different command and language elements detected. All I needed to do was do a little filtering, grouping and sorting.<\/p>\n<pre class=\"lang:ps mark:0 decode:true\">($asttokens).where({$_.tokenFlags -eq \"commandname\" -AND (-Not $_.nestedtokens)}) | \nSelect-Object -property text | \nGroup-Object -property text -NoElement |\nSort-Object -Property count -Descending\n<\/pre>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"alignnone size-full wp-image-7552\" src=\"https:\/\/jdhitsolutions.com\/blog\/wp-content\/uploads\/2020\/06\/ast-1.png\" alt=\"ast-1\" width=\"661\" height=\"621\" srcset=\"https:\/\/jdhitsolutions.com\/blog\/wp-content\/uploads\/2020\/06\/ast-1.png 661w, https:\/\/jdhitsolutions.com\/blog\/wp-content\/uploads\/2020\/06\/ast-1-300x282.png 300w\" sizes=\"auto, (max-width: 661px) 100vw, 661px\" \/>These are results for a single file. With a little tweaking,\u00a0 I came up with this function.<\/p>\n<pre class=\"lang:ps mark:0 decode:true\">Function Measure-ScriptFile {\n    [cmdletbinding()]\n    [alias(\"msf\")]\n\n    Param(\n        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]\n        [alias(\"fullname\")]\n        [string]$Path\n    )\n\n    Begin {\n        Write-Verbose \"[BEGIN  ] Starting: $($MyInvocation.Mycommand)\"\n        $countHash = @{}\n\n        #get all commands\n        #Write-Verbose \"[BEGIN  ] Building command inventory\"\n        #$cmds = (Get-Command -commandtype Filter, Function, Cmdlet).Name\n        #Write-Verbose \"[BEGIN  ] Using $($cmds.count) command names\"\n\n        New-Variable astTokens -force\n        New-Variable astErr -force\n    } #begin\n    Process {\n        $AST = [System.Management.Automation.Language.Parser]::ParseFile($Path, [ref]$astTokens, [ref]$astErr)\n        [void]$AST.FindAll({$args[0] -is [System.Management.Automation.Language.CommandAst]}, $true)\n\n        #filter for commands but not file system commands like notepad.exe\n        ($asttokens).where( {$_.tokenFlags -eq \"commandname\" -AND (-Not $_.nestedtokens) -AND ($_.text -notmatch \"\\.\")}) |\n        ForEach-Object {\n            #resolve alias\n            $token = $_\n            if ($_.kind -eq 'identifier') {\n                Try {\n                    $value = (Get-Command -Name $_.text -ErrorAction stop).ResolvedCommandName\n                }\n                Catch {\n                    #ignore the error\n                    $msg = \"Unresolved: text:{1} in $filename\" -f $filename, $token.text\n                    Write-Verbose $msg\n                    $value = $null\n                }\n            }   #if identifier\n            elseif ($token.text -eq '?') {\n                $Value = 'Where-Object'\n            }\n            elseif ($token.text -eq '%') {\n                $value = 'ForEach-Object'\n            }\n            #test if the text looks like a command name\n            elseif ($token.text -match \"\\w+-\\w+\") {\n                $value = $token.text\n            }\n            &lt;#\n            Use if testing for actual commands\n            elseif ($cmds -contains $token.text) {\n                $value = $token.text\n            }\n            #&gt;\n            #add the value to the counting hashtable\n            if ($value) {\n                if ($countHash.ContainsKey($value)) {\n                    $countHash.item($value)++\n                }\n                else {\n                    $countHash.add($value, 1)\n                }\n            } #if $value\n        } #foreach\n    } #process\n    End {\n        $countHash\n        Write-Verbose \"[END    ] Ending: $($MyInvocation.Mycommand)\"\n    } #end\n}\n<\/pre>\n<p>The function will resolve aliases and builds a command table if the text looks like a command. That is, with Verb-Noun naming convention. I have code that tests each possbile command against the list of command names built from Get-Command. But this adds at least 30+ seconds to the processing time. I apparently have over 16K commands in and that takes time to count.<\/p>\n<p>I can run it like this:<\/p>\n<pre class=\"lang:ps mark:0 decode:true\">$Path = \"C:\\scripts\"\n(Get-ChildItem -Path $Path -file -Recurse -Filter \"*.ps*\").Where({$_.Extension -match \"\\.ps(m)?1$\"}) | Measure-ScriptFile\n<\/pre>\n<p>This still takes time. Over 45 minutes but definitely faster. That averages out to just under 2 seconds per file which isn't too bad.<\/p>\n<h2>Parallel<\/h2>\n<p>Now the big question becomes, is this a case where the new -Parallel feature in PowerShell 7 can come to the party? The answer is a cautious \"maybe\". Remember that there is overhead in setting up and tearing down the parallel processing.\u00a0 You need to make sure that your parallel operation is \"cost-effective\".<\/p>\n<p>It would make no sense to process individual files in parallel. It would take at least a second or two to account for the overhead which is as long at it takes my function to process an average file. My solution was to divide my files into sets of 500 files and then <em>process each set<\/em> in parallel.<\/p>\n<pre class=\"lang:ps mark:0 decode:true\">#requires -version 7.0\n\n[cmdletbinding()]\nParam([string]$Path = \"C:\\scripts\",[int]$ThrottleLimit = 5)\n\nWrite-Host \"Getting PowerShell files from $Path\" -ForegroundColor Green\n$files = (Get-ChildItem -Path $Path -file -Recurse -Filter \"*.ps*\").Where({$_.Extension -match \"\\.ps(m)?1$\"})\nWrite-Host \"Processing $($files.count) files\" -ForegroundColor Green\n\n#divide the files into sets\n$sets = @{}\n$c=0\nfor ($i = 0 ; $i -lt $files.count; $i+=500) {\n$c++\n $start = $i\n $end = $i+499\n $sets.Add(\"Set$C\",@($files[$start..$end]))\n }\n\n #process the file sets in parallel\n $results = $sets.GetEnumerator() | \nForEach-Object -throttlelimit $ThrottleLimit -parallel {\n  . C:\\scripts\\Measure-ScriptFile.ps1\n  Write-Host \"Processing $($_.name)\" -fore cyan\n  $_.value | Measure-ScriptFile\n }\n\n #assemble the results into a single data set.\n $data = @{}\n foreach ($result in $results) {\n   $result.getenumerator().foreach({\n     if ($data.containskey($_.name)) {\n        $data.item($_.name)+=$_.value\n     }\n     else {\n        $data.add($_.name,$_.value)\n     }\n   })\n }\n\n #the unified results\n $data\n<\/pre>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"alignnone size-full wp-image-7556\" src=\"https:\/\/jdhitsolutions.com\/blog\/wp-content\/uploads\/2020\/06\/ast-parallel-rev.png\" alt=\"ast-parallel-rev\" width=\"1085\" height=\"346\" srcset=\"https:\/\/jdhitsolutions.com\/blog\/wp-content\/uploads\/2020\/06\/ast-parallel-rev.png 1085w, https:\/\/jdhitsolutions.com\/blog\/wp-content\/uploads\/2020\/06\/ast-parallel-rev-300x96.png 300w, https:\/\/jdhitsolutions.com\/blog\/wp-content\/uploads\/2020\/06\/ast-parallel-rev-1024x327.png 1024w, https:\/\/jdhitsolutions.com\/blog\/wp-content\/uploads\/2020\/06\/ast-parallel-rev-768x245.png 768w, https:\/\/jdhitsolutions.com\/blog\/wp-content\/uploads\/2020\/06\/ast-parallel-rev-850x271.png 850w\" sizes=\"auto, (max-width: 1085px) 100vw, 1085px\" \/>I added the Write-Host lines so I could see the parallel processing in action.<\/p>\n<p>My function includes the ThrottleLimit parameter which gets passed to ForEach-Object. I tried my script with different throttle values and it didn't really make much of a difference for this task. But it was significantly faster! Processing time for almost 5000 files was about 10 minutes!<\/p>\n<p>And this is apparently what I use the most in my PowerShell work. Here are the top 25 commands.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"alignnone size-full wp-image-7557\" src=\"https:\/\/jdhitsolutions.com\/blog\/wp-content\/uploads\/2020\/06\/ast-results.png\" alt=\"ast-results\" width=\"686\" height=\"825\" srcset=\"https:\/\/jdhitsolutions.com\/blog\/wp-content\/uploads\/2020\/06\/ast-results.png 686w, https:\/\/jdhitsolutions.com\/blog\/wp-content\/uploads\/2020\/06\/ast-results-249x300.png 249w, https:\/\/jdhitsolutions.com\/blog\/wp-content\/uploads\/2020\/06\/ast-results-300x361.png 300w\" sizes=\"auto, (max-width: 686px) 100vw, 686px\" \/><\/p>\n<p>All from an overall result of 2422 commands.<\/p>\n<p>This is not an absolute list of course. It doesn't take into account commands from modules that no longer exist on my computer or from non-standard command names I might have used, such as for private functions. But it should be close enough.<\/p>\n<p>I don't think I solved everything in the challenge, but this is a good place to start. I'll let you play with the code and see what kind of results you get.<\/p>\n<p>Comments and questions are always welcome.<\/p>\n<h2>Update<\/h2>\n<p>Check out the expanded version of this code, now packaged as a PowerShell module at <a href=\"https:\/\/jdhitsolutions.com\/blog\/powershell\/7559\/an-expanded-powershell-scripting-inventory-tool\/\" target=\"_blank\" rel=\"noopener noreferrer\">https:\/\/jdhitsolutions.com\/blog\/powershell\/7559\/an-expanded-powershell-scripting-inventory-tool\/<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>A few weeks ago, a new Iron Scripter PowerShell scripting challenge was issued. For this challenge we were asked to write some PowerShell code that we could use to inventory our PowerShell script library.\u00a0 Here&#8217;s how I approached the problem, which by no means is the only way. Lines of Code The first part of&#8230;<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_monsterinsights_skip_tracking":false,"_monsterinsights_sitenote_active":false,"_monsterinsights_sitenote_note":"","_monsterinsights_sitenote_category":0,"_jetpack_memberships_contains_paid_content":false,"footnotes":"","jetpack_publicize_message":"Just posted: Building a #PowerShell Command Inventory","jetpack_publicize_feature_enabled":true,"jetpack_social_post_already_shared":true,"jetpack_social_options":{"image_generator_settings":{"template":"highway","default_image_id":0,"font":"","enabled":false},"version":2}},"categories":[4,8],"tags":[447,618,612,534],"class_list":["post-7549","post","type-post","status-publish","format-standard","hentry","category-powershell","category-scripting","tag-ast","tag-iron-scripter","tag-parallel","tag-powershell"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v27.6 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>Building a PowerShell Inventory &#8226; The Lonely Administrator<\/title>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/jdhitsolutions.com\/blog\/powershell\/7549\/building-a-powershell-inventory\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Building a PowerShell Inventory &#8226; The Lonely Administrator\" \/>\n<meta property=\"og:description\" content=\"A few weeks ago, a new Iron Scripter PowerShell scripting challenge was issued. For this challenge we were asked to write some PowerShell code that we could use to inventory our PowerShell script library.\u00a0 Here&#039;s how I approached the problem, which by no means is the only way. Lines of Code The first part of...\" \/>\n<meta property=\"og:url\" content=\"https:\/\/jdhitsolutions.com\/blog\/powershell\/7549\/building-a-powershell-inventory\/\" \/>\n<meta property=\"og:site_name\" content=\"The Lonely Administrator\" \/>\n<meta property=\"article:published_time\" content=\"2020-06-16T17:32:31+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2020-06-19T14:05:57+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/jdhitsolutions.com\/blog\/wp-content\/uploads\/2020\/06\/pscount.png\" \/>\n<meta name=\"author\" content=\"Jeffery Hicks\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:creator\" content=\"@JeffHicks\" \/>\n<meta name=\"twitter:site\" content=\"@JeffHicks\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"Jeffery Hicks\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"7 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\\\/\\\/jdhitsolutions.com\\\/blog\\\/powershell\\\/7549\\\/building-a-powershell-inventory\\\/#article\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/jdhitsolutions.com\\\/blog\\\/powershell\\\/7549\\\/building-a-powershell-inventory\\\/\"},\"author\":{\"name\":\"Jeffery Hicks\",\"@id\":\"https:\\\/\\\/jdhitsolutions.com\\\/blog\\\/#\\\/schema\\\/person\\\/d0258030b41f07fd745f4078bdf5b6c9\"},\"headline\":\"Building a PowerShell Inventory\",\"datePublished\":\"2020-06-16T17:32:31+00:00\",\"dateModified\":\"2020-06-19T14:05:57+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\\\/\\\/jdhitsolutions.com\\\/blog\\\/powershell\\\/7549\\\/building-a-powershell-inventory\\\/\"},\"wordCount\":883,\"commentCount\":2,\"publisher\":{\"@id\":\"https:\\\/\\\/jdhitsolutions.com\\\/blog\\\/#\\\/schema\\\/person\\\/d0258030b41f07fd745f4078bdf5b6c9\"},\"image\":{\"@id\":\"https:\\\/\\\/jdhitsolutions.com\\\/blog\\\/powershell\\\/7549\\\/building-a-powershell-inventory\\\/#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/jdhitsolutions.com\\\/blog\\\/wp-content\\\/uploads\\\/2020\\\/06\\\/pscount.png\",\"keywords\":[\"AST\",\"Iron Scripter\",\"parallel\",\"PowerShell\"],\"articleSection\":[\"PowerShell\",\"Scripting\"],\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"CommentAction\",\"name\":\"Comment\",\"target\":[\"https:\\\/\\\/jdhitsolutions.com\\\/blog\\\/powershell\\\/7549\\\/building-a-powershell-inventory\\\/#respond\"]}]},{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/jdhitsolutions.com\\\/blog\\\/powershell\\\/7549\\\/building-a-powershell-inventory\\\/\",\"url\":\"https:\\\/\\\/jdhitsolutions.com\\\/blog\\\/powershell\\\/7549\\\/building-a-powershell-inventory\\\/\",\"name\":\"Building a PowerShell Inventory &#8226; The Lonely Administrator\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/jdhitsolutions.com\\\/blog\\\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\\\/\\\/jdhitsolutions.com\\\/blog\\\/powershell\\\/7549\\\/building-a-powershell-inventory\\\/#primaryimage\"},\"image\":{\"@id\":\"https:\\\/\\\/jdhitsolutions.com\\\/blog\\\/powershell\\\/7549\\\/building-a-powershell-inventory\\\/#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/jdhitsolutions.com\\\/blog\\\/wp-content\\\/uploads\\\/2020\\\/06\\\/pscount.png\",\"datePublished\":\"2020-06-16T17:32:31+00:00\",\"dateModified\":\"2020-06-19T14:05:57+00:00\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/jdhitsolutions.com\\\/blog\\\/powershell\\\/7549\\\/building-a-powershell-inventory\\\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/jdhitsolutions.com\\\/blog\\\/powershell\\\/7549\\\/building-a-powershell-inventory\\\/\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/jdhitsolutions.com\\\/blog\\\/powershell\\\/7549\\\/building-a-powershell-inventory\\\/#primaryimage\",\"url\":\"https:\\\/\\\/jdhitsolutions.com\\\/blog\\\/wp-content\\\/uploads\\\/2020\\\/06\\\/pscount.png\",\"contentUrl\":\"https:\\\/\\\/jdhitsolutions.com\\\/blog\\\/wp-content\\\/uploads\\\/2020\\\/06\\\/pscount.png\",\"width\":886,\"height\":179},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/jdhitsolutions.com\\\/blog\\\/powershell\\\/7549\\\/building-a-powershell-inventory\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"PowerShell\",\"item\":\"https:\\\/\\\/jdhitsolutions.com\\\/blog\\\/category\\\/powershell\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Building a PowerShell Inventory\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/jdhitsolutions.com\\\/blog\\\/#website\",\"url\":\"https:\\\/\\\/jdhitsolutions.com\\\/blog\\\/\",\"name\":\"The Lonely Administrator\",\"description\":\"Practical Advice for the Automating IT Pro\",\"publisher\":{\"@id\":\"https:\\\/\\\/jdhitsolutions.com\\\/blog\\\/#\\\/schema\\\/person\\\/d0258030b41f07fd745f4078bdf5b6c9\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/jdhitsolutions.com\\\/blog\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":[\"Person\",\"Organization\"],\"@id\":\"https:\\\/\\\/jdhitsolutions.com\\\/blog\\\/#\\\/schema\\\/person\\\/d0258030b41f07fd745f4078bdf5b6c9\",\"name\":\"Jeffery Hicks\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/832ae5d438fdcfc1420d720cd1991307927de8a0b12f2342e81c30f773e21098?s=96&d=wavatar&r=pg\",\"url\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/832ae5d438fdcfc1420d720cd1991307927de8a0b12f2342e81c30f773e21098?s=96&d=wavatar&r=pg\",\"contentUrl\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/832ae5d438fdcfc1420d720cd1991307927de8a0b12f2342e81c30f773e21098?s=96&d=wavatar&r=pg\",\"caption\":\"Jeffery Hicks\"},\"logo\":{\"@id\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/832ae5d438fdcfc1420d720cd1991307927de8a0b12f2342e81c30f773e21098?s=96&d=wavatar&r=pg\"}}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"Building a PowerShell Inventory &#8226; The Lonely Administrator","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/jdhitsolutions.com\/blog\/powershell\/7549\/building-a-powershell-inventory\/","og_locale":"en_US","og_type":"article","og_title":"Building a PowerShell Inventory &#8226; The Lonely Administrator","og_description":"A few weeks ago, a new Iron Scripter PowerShell scripting challenge was issued. For this challenge we were asked to write some PowerShell code that we could use to inventory our PowerShell script library.\u00a0 Here's how I approached the problem, which by no means is the only way. Lines of Code The first part of...","og_url":"https:\/\/jdhitsolutions.com\/blog\/powershell\/7549\/building-a-powershell-inventory\/","og_site_name":"The Lonely Administrator","article_published_time":"2020-06-16T17:32:31+00:00","article_modified_time":"2020-06-19T14:05:57+00:00","og_image":[{"url":"https:\/\/jdhitsolutions.com\/blog\/wp-content\/uploads\/2020\/06\/pscount.png","type":"","width":"","height":""}],"author":"Jeffery Hicks","twitter_card":"summary_large_image","twitter_creator":"@JeffHicks","twitter_site":"@JeffHicks","twitter_misc":{"Written by":"Jeffery Hicks","Est. reading time":"7 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/jdhitsolutions.com\/blog\/powershell\/7549\/building-a-powershell-inventory\/#article","isPartOf":{"@id":"https:\/\/jdhitsolutions.com\/blog\/powershell\/7549\/building-a-powershell-inventory\/"},"author":{"name":"Jeffery Hicks","@id":"https:\/\/jdhitsolutions.com\/blog\/#\/schema\/person\/d0258030b41f07fd745f4078bdf5b6c9"},"headline":"Building a PowerShell Inventory","datePublished":"2020-06-16T17:32:31+00:00","dateModified":"2020-06-19T14:05:57+00:00","mainEntityOfPage":{"@id":"https:\/\/jdhitsolutions.com\/blog\/powershell\/7549\/building-a-powershell-inventory\/"},"wordCount":883,"commentCount":2,"publisher":{"@id":"https:\/\/jdhitsolutions.com\/blog\/#\/schema\/person\/d0258030b41f07fd745f4078bdf5b6c9"},"image":{"@id":"https:\/\/jdhitsolutions.com\/blog\/powershell\/7549\/building-a-powershell-inventory\/#primaryimage"},"thumbnailUrl":"https:\/\/jdhitsolutions.com\/blog\/wp-content\/uploads\/2020\/06\/pscount.png","keywords":["AST","Iron Scripter","parallel","PowerShell"],"articleSection":["PowerShell","Scripting"],"inLanguage":"en-US","potentialAction":[{"@type":"CommentAction","name":"Comment","target":["https:\/\/jdhitsolutions.com\/blog\/powershell\/7549\/building-a-powershell-inventory\/#respond"]}]},{"@type":"WebPage","@id":"https:\/\/jdhitsolutions.com\/blog\/powershell\/7549\/building-a-powershell-inventory\/","url":"https:\/\/jdhitsolutions.com\/blog\/powershell\/7549\/building-a-powershell-inventory\/","name":"Building a PowerShell Inventory &#8226; The Lonely Administrator","isPartOf":{"@id":"https:\/\/jdhitsolutions.com\/blog\/#website"},"primaryImageOfPage":{"@id":"https:\/\/jdhitsolutions.com\/blog\/powershell\/7549\/building-a-powershell-inventory\/#primaryimage"},"image":{"@id":"https:\/\/jdhitsolutions.com\/blog\/powershell\/7549\/building-a-powershell-inventory\/#primaryimage"},"thumbnailUrl":"https:\/\/jdhitsolutions.com\/blog\/wp-content\/uploads\/2020\/06\/pscount.png","datePublished":"2020-06-16T17:32:31+00:00","dateModified":"2020-06-19T14:05:57+00:00","breadcrumb":{"@id":"https:\/\/jdhitsolutions.com\/blog\/powershell\/7549\/building-a-powershell-inventory\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/jdhitsolutions.com\/blog\/powershell\/7549\/building-a-powershell-inventory\/"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/jdhitsolutions.com\/blog\/powershell\/7549\/building-a-powershell-inventory\/#primaryimage","url":"https:\/\/jdhitsolutions.com\/blog\/wp-content\/uploads\/2020\/06\/pscount.png","contentUrl":"https:\/\/jdhitsolutions.com\/blog\/wp-content\/uploads\/2020\/06\/pscount.png","width":886,"height":179},{"@type":"BreadcrumbList","@id":"https:\/\/jdhitsolutions.com\/blog\/powershell\/7549\/building-a-powershell-inventory\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"PowerShell","item":"https:\/\/jdhitsolutions.com\/blog\/category\/powershell\/"},{"@type":"ListItem","position":2,"name":"Building a PowerShell Inventory"}]},{"@type":"WebSite","@id":"https:\/\/jdhitsolutions.com\/blog\/#website","url":"https:\/\/jdhitsolutions.com\/blog\/","name":"The Lonely Administrator","description":"Practical Advice for the Automating IT Pro","publisher":{"@id":"https:\/\/jdhitsolutions.com\/blog\/#\/schema\/person\/d0258030b41f07fd745f4078bdf5b6c9"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/jdhitsolutions.com\/blog\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":["Person","Organization"],"@id":"https:\/\/jdhitsolutions.com\/blog\/#\/schema\/person\/d0258030b41f07fd745f4078bdf5b6c9","name":"Jeffery Hicks","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/secure.gravatar.com\/avatar\/832ae5d438fdcfc1420d720cd1991307927de8a0b12f2342e81c30f773e21098?s=96&d=wavatar&r=pg","url":"https:\/\/secure.gravatar.com\/avatar\/832ae5d438fdcfc1420d720cd1991307927de8a0b12f2342e81c30f773e21098?s=96&d=wavatar&r=pg","contentUrl":"https:\/\/secure.gravatar.com\/avatar\/832ae5d438fdcfc1420d720cd1991307927de8a0b12f2342e81c30f773e21098?s=96&d=wavatar&r=pg","caption":"Jeffery Hicks"},"logo":{"@id":"https:\/\/secure.gravatar.com\/avatar\/832ae5d438fdcfc1420d720cd1991307927de8a0b12f2342e81c30f773e21098?s=96&d=wavatar&r=pg"}}]}},"jetpack_publicize_connections":[],"jetpack_featured_media_url":"","jetpack_sharing_enabled":true,"jetpack_likes_enabled":true,"jetpack-related-posts":[{"id":8107,"url":"https:\/\/jdhitsolutions.com\/blog\/powershell\/8107\/scripting-challenge-meetup\/","url_meta":{"origin":7549,"position":0},"title":"Scripting Challenge Meetup","author":"Jeffery Hicks","date":"February 1, 2021","format":false,"excerpt":"As you probably know, I am the PowerShell problem master behind the challenges from the Iron Scripter site. Solving a PowerShell scripting challenge is a great way to test your skills and expand your knowledge. The final result is merely a means to an end. How you get there and\u2026","rel":"","context":"In &quot;PowerShell&quot;","block_context":{"text":"PowerShell","link":"https:\/\/jdhitsolutions.com\/blog\/category\/powershell\/"},"img":{"alt_text":"","src":"https:\/\/i0.wp.com\/jdhitsolutions.com\/blog\/wp-content\/uploads\/2021\/02\/rubik.jpg?resize=350%2C200&ssl=1","width":350,"height":200},"classes":[]},{"id":7559,"url":"https:\/\/jdhitsolutions.com\/blog\/powershell\/7559\/an-expanded-powershell-scripting-inventory-tool\/","url_meta":{"origin":7549,"position":1},"title":"An Expanded PowerShell Scripting Inventory Tool","author":"Jeffery Hicks","date":"June 19, 2020","format":false,"excerpt":"The other day I shared my code that I worked up to solve an Iron Scripter PowerShell challenge. One of the shortcomings was that I didn't address a challenge to include a property that would indicate what file was using a given command. I also felt I could do better\u2026","rel":"","context":"In &quot;PowerShell&quot;","block_context":{"text":"PowerShell","link":"https:\/\/jdhitsolutions.com\/blog\/category\/powershell\/"},"img":{"alt_text":"","src":"https:\/\/i0.wp.com\/jdhitsolutions.com\/blog\/wp-content\/uploads\/2020\/06\/get-psscriptinventory.png?resize=350%2C200&ssl=1","width":350,"height":200,"srcset":"https:\/\/i0.wp.com\/jdhitsolutions.com\/blog\/wp-content\/uploads\/2020\/06\/get-psscriptinventory.png?resize=350%2C200&ssl=1 1x, https:\/\/i0.wp.com\/jdhitsolutions.com\/blog\/wp-content\/uploads\/2020\/06\/get-psscriptinventory.png?resize=525%2C300&ssl=1 1.5x, https:\/\/i0.wp.com\/jdhitsolutions.com\/blog\/wp-content\/uploads\/2020\/06\/get-psscriptinventory.png?resize=700%2C400&ssl=1 2x, https:\/\/i0.wp.com\/jdhitsolutions.com\/blog\/wp-content\/uploads\/2020\/06\/get-psscriptinventory.png?resize=1050%2C600&ssl=1 3x, https:\/\/i0.wp.com\/jdhitsolutions.com\/blog\/wp-content\/uploads\/2020\/06\/get-psscriptinventory.png?resize=1400%2C800&ssl=1 4x"},"classes":[]},{"id":7489,"url":"https:\/\/jdhitsolutions.com\/blog\/powershell\/7489\/powershell-word-play\/","url_meta":{"origin":7549,"position":2},"title":"PowerShell Word Play","author":"Jeffery Hicks","date":"May 19, 2020","format":false,"excerpt":"A few weeks ago an Iron Scripter PowerShell challenge was issued that involved playing with words and characters. Remember, the Iron Scripter challenges aren't intended to create meaningful, production worthy code. They are designed to help you learn PowerShell fundamentals and scripting techniques. This particular challenge was aimed at beginner\u2026","rel":"","context":"In &quot;PowerShell&quot;","block_context":{"text":"PowerShell","link":"https:\/\/jdhitsolutions.com\/blog\/category\/powershell\/"},"img":{"alt_text":"","src":"https:\/\/i0.wp.com\/jdhitsolutions.com\/blog\/wp-content\/uploads\/2020\/05\/doublechar.png?resize=350%2C200&ssl=1","width":350,"height":200,"srcset":"https:\/\/i0.wp.com\/jdhitsolutions.com\/blog\/wp-content\/uploads\/2020\/05\/doublechar.png?resize=350%2C200&ssl=1 1x, https:\/\/i0.wp.com\/jdhitsolutions.com\/blog\/wp-content\/uploads\/2020\/05\/doublechar.png?resize=525%2C300&ssl=1 1.5x, https:\/\/i0.wp.com\/jdhitsolutions.com\/blog\/wp-content\/uploads\/2020\/05\/doublechar.png?resize=700%2C400&ssl=1 2x, https:\/\/i0.wp.com\/jdhitsolutions.com\/blog\/wp-content\/uploads\/2020\/05\/doublechar.png?resize=1050%2C600&ssl=1 3x, https:\/\/i0.wp.com\/jdhitsolutions.com\/blog\/wp-content\/uploads\/2020\/05\/doublechar.png?resize=1400%2C800&ssl=1 4x"},"classes":[]},{"id":7024,"url":"https:\/\/jdhitsolutions.com\/blog\/powershell\/7024\/managing-the-recycle-bin-with-powershell\/","url_meta":{"origin":7549,"position":3},"title":"Managing the Recycle Bin with PowerShell","author":"Jeffery Hicks","date":"December 3, 2019","format":false,"excerpt":"A while ago, I posted an Iron Scripter challenge asking you to write some PowerShell code for working with items in the recycle bin. You were asked to calculate how much space the recycle bin is using and then restore a file. If you'd prefer, stop reading this post, check\u2026","rel":"","context":"In &quot;PowerShell&quot;","block_context":{"text":"PowerShell","link":"https:\/\/jdhitsolutions.com\/blog\/category\/powershell\/"},"img":{"alt_text":"","src":"https:\/\/i0.wp.com\/jdhitsolutions.com\/blog\/wp-content\/uploads\/2019\/12\/image_thumb-7.png?resize=350%2C200&ssl=1","width":350,"height":200,"srcset":"https:\/\/i0.wp.com\/jdhitsolutions.com\/blog\/wp-content\/uploads\/2019\/12\/image_thumb-7.png?resize=350%2C200&ssl=1 1x, https:\/\/i0.wp.com\/jdhitsolutions.com\/blog\/wp-content\/uploads\/2019\/12\/image_thumb-7.png?resize=525%2C300&ssl=1 1.5x, https:\/\/i0.wp.com\/jdhitsolutions.com\/blog\/wp-content\/uploads\/2019\/12\/image_thumb-7.png?resize=700%2C400&ssl=1 2x"},"classes":[]},{"id":8128,"url":"https:\/\/jdhitsolutions.com\/blog\/powershell\/8128\/powershell-puzzles-and-challenges\/","url_meta":{"origin":7549,"position":4},"title":"PowerShell Puzzles and Challenges","author":"Jeffery Hicks","date":"February 3, 2021","format":false,"excerpt":"I just wrapped up presenting to the Research Triangle PowerShell User Group. I gave them a set of PowerShell problems to solve. The idea is that in solving the problem, you improve your PowerShell skills and expertise. Tonight's problems were not scripting problems. The answers to these puzzles are not\u2026","rel":"","context":"In &quot;PowerShell&quot;","block_context":{"text":"PowerShell","link":"https:\/\/jdhitsolutions.com\/blog\/category\/powershell\/"},"img":{"alt_text":"","src":"https:\/\/i0.wp.com\/jdhitsolutions.com\/blog\/wp-content\/uploads\/2018\/11\/brainidea_thumb_thumb.jpg?resize=350%2C200&ssl=1","width":350,"height":200},"classes":[]},{"id":2330,"url":"https:\/\/jdhitsolutions.com\/blog\/scripting\/2330\/friday-fun-get-latest-powershell-scripts\/","url_meta":{"origin":7549,"position":5},"title":"Friday Fun: Get Latest PowerShell Scripts","author":"Jeffery Hicks","date":"May 18, 2012","format":false,"excerpt":"Probably like many of you I keep almost all of my scripts in a single location. I'm also usually working on multiple items at the same time. Some times I have difficult remembering the name of a script I might have been working on a few days ago that I\u2026","rel":"","context":"In &quot;Friday Fun&quot;","block_context":{"text":"Friday Fun","link":"https:\/\/jdhitsolutions.com\/blog\/category\/friday-fun\/"},"img":{"alt_text":"","src":"https:\/\/i0.wp.com\/jdhitsolutions.com\/blog\/wp-content\/uploads\/2012\/05\/get-latestscript-300x133.png?resize=350%2C200","width":350,"height":200},"classes":[]}],"_links":{"self":[{"href":"https:\/\/jdhitsolutions.com\/blog\/wp-json\/wp\/v2\/posts\/7549","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/jdhitsolutions.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/jdhitsolutions.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/jdhitsolutions.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/jdhitsolutions.com\/blog\/wp-json\/wp\/v2\/comments?post=7549"}],"version-history":[{"count":0,"href":"https:\/\/jdhitsolutions.com\/blog\/wp-json\/wp\/v2\/posts\/7549\/revisions"}],"wp:attachment":[{"href":"https:\/\/jdhitsolutions.com\/blog\/wp-json\/wp\/v2\/media?parent=7549"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/jdhitsolutions.com\/blog\/wp-json\/wp\/v2\/categories?post=7549"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/jdhitsolutions.com\/blog\/wp-json\/wp\/v2\/tags?post=7549"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}