There comes a time when you are neck deep in code, and come to the realization that one of your modules could really use a tweak. There is just one problem – what other scripts are using this module? What if you wanted to inventory your scripts and the modules that each script is using? This was the main use-case for this post. I have a scripts directory, and want to get a quick inventory of what modules each one is using.
First, the function:
#requires -version 3.0 Function Test-ScriptFile { <# .Synopsis Test a PowerShell script for cmdlets .Description This command will analyze a PowerShell script file and display a list of detected commands such as PowerShell cmdlets and functions. Commands will be compared to what is installed locally. It is recommended you run this on a Windows 8.1 client with the latest version of RSAT installed. Unknown commands could also be internally defined functions. If in doubt view the contents of the script file in the PowerShell ISE or a script editor. You can test any .ps1, .psm1 or .txt file. .Parameter Path The path to the PowerShell script file. You can test any .ps1, .psm1 or .txt file. .Example PS C:\> test-scriptfile C:\scripts\Remove-MyVM2.ps1 CommandType Name ModuleName ----------- ---- ---------- Cmdlet Disable-VMEventing Hyper-V Cmdlet ForEach-Object Microsoft.PowerShell.Core Cmdlet Get-VHD Hyper-V Cmdlet Get-VMSnapshot Hyper-V Cmdlet Invoke-Command Microsoft.PowerShell.Core Cmdlet New-PSSession Microsoft.PowerShell.Core Cmdlet Out-Null Microsoft.PowerShell.Core Cmdlet Out-String Microsoft.PowerShell.Utility Cmdlet Remove-Item Microsoft.PowerShell.Management Cmdlet Remove-PSSession Microsoft.PowerShell.Core Cmdlet Remove-VM Hyper-V Cmdlet Remove-VMSnapshot Hyper-V Cmdlet Write-Debug Microsoft.PowerShell.Utility Cmdlet Write-Verbose Microsoft.PowerShell.Utility Cmdlet Write-Warning Microsoft.PowerShell.Utility .Notes Original script provided by Jeff Hicks at (https://www.petri.com/powershell-problem-solver-find-script-commands) #> [cmdletbinding()] Param( [Parameter(Position = 0, Mandatory = $True, HelpMessage = "Enter the path to a PowerShell script file,", ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] [ValidatePattern( "\.(ps1|psm1|txt)$")] [ValidateScript( { Test-Path $_ })] [string]$Path ) Begin { Write-Verbose "Starting $($MyInvocation.Mycommand)" Write-Verbose "Defining AST variables" New-Variable astTokens -force New-Variable astErr -force } Process { Write-Verbose "Parsing $path" $AST = [System.Management.Automation.Language.Parser]::ParseFile($Path, [ref]$astTokens, [ref]$astErr) #group tokens and turn into a hashtable $h = $astTokens | Group-Object tokenflags -AsHashTable -AsString $commandData = $h.CommandName | where-object { $_.text -notmatch "-TargetResource$" } | ForEach-Object { Write-Verbose "Processing $($_.text)" Try { $cmd = $_.Text $resolved = $cmd | get-command -ErrorAction Stop if ($resolved.CommandType -eq 'Alias') { Write-Verbose "Resolving an alias" #manually handle "?" because Get-Command and Get-Alias won't. Write-Verbose "Detected the Where-Object alias '?'" if ($cmd -eq '?') { Get-Command Where-Object } else { $resolved.ResolvedCommandName | Get-Command } } else { $resolved } } Catch { Write-Verbose "Command is not recognized" #create a custom object for unknown commands [PSCustomobject]@{ CommandType = "Unknown" Name = $cmd ModuleName = "Unknown" } } } write-output $CommandData } End { Write-Verbose -Message "Ending $($MyInvocation.Mycommand)" } }
This function was originally from Jeff Hicks . I made a few tweaks, but the majority of this is his code. As always, Jeff puts out amazing work.
Next, a simple script to call the function on all of the scripts in a certain directory:
if ($IsWindows -or $IsWindows -eq $null){$ModulesDir = (Get-ItemProperty HKLM:\SOFTWARE\PWSH).PowerShell_Modules} else{get-content '/var/opt/tifa/settings.json'} $log = $PSScriptRoot + '\'+ ($MyInvocation.MyCommand.Name).split('.')[0] + '.log' $ModulesToImport = 'logging', 'SQL', 'PoshKeePass', 'PoshRSJob', 'SCOM', "OMI", 'DynamicMonitoring', 'Nix','Utils' foreach ($module in $ModulesToImport) {Get-ChildItem $ModulesDir\$module\*.psd1 -Recurse | resolve-path | ForEach-Object { import-module $_.providerpath -force }} $files = (Get-ChildItem 'D:\pwsh\' -Include *.ps1 -Recurse | where-object { $_.FullName -notmatch "\\modules*" }).fullname [System.Collections.ArrayList]$AllData = @() foreach ($file in $files) { [array]$data = Test-ScriptFile -Path $file foreach ($dataline in $data) { $path = $Dataline.module.path $version = $dataline.Module.version $dataobj = [PSCustomObject]@{ File = $File; Type = $dataline.CommandType; Name = $dataline.Name; ModuleName = $dataline.ModuleName; ModulePath = $path; ModuleVersion = $version; } $AllData.add($dataobj)|out-null } } $AllData|select-object -Property file, type, name, modulename,modulepath,moduleversion -Unique|out-gridview
There are a few gotchas you want to keep an eye out for – in order to correctly identify the cmdlets, you will need to load the module that contains said cmdlet. You can see where I am loading several modules – SCOM, SQL, logging, etc. The modules in my script are loaded from modules stored in a reg key – HKLM:\Software\PWSH. Load your modules in any way that works for you. Also note that the last line sends the output to a gridview – change that output to something more useful unless you just previewing it.
And there you have it! Thanks to Jeff for the core function, and I hope this helps keep your modules organized!