Get-ScriptReference.ps1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 |
#requires -version 3.0 function Get-ScriptReference { <# .Synopsis Gets a script's references .Description Gets the external references of a given PowerShell command. These are the commands the script calls, and the types the script uses. .Example Get-Command Get-ScriptReference | Get-ScriptReference #> [CmdletBinding(DefaultParameterSetName='FilePath')] param( # The path to a file [Parameter(Mandatory=$true,Position=0,ParameterSetName='FilePath',ValueFromPipelineByPropertyName=$true)] [Alias('Fullname')] [string[]] $FilePath, # One or more PowerShell ScriptBlocks [Parameter(Mandatory=$true,Position=0,ParameterSetName='ScriptBlock',ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)] [Alias('Definition')] [ScriptBlock[]] $ScriptBlock, # If set, will recursively find references. [switch] $Recurse ) begin { # Let's declare some collections we'll need: $allFiles = [Collections.ArrayList]::new() # * A list of all files (if any are piped in) $LookedUpCommands = @{} # * The commands we've already looked up (to save time) } process { #region Process Piped in Files if ($PSCmdlet.ParameterSetName -eq 'FilePath') { # If we're piping in files, $allFiles.AddRange($FilePath) # add them to the list and process them in the end, return # and stop processing for good measure. } #endregion Process Piped in Files #region Get the Script References # To start off with, take all of the scripts passed in and put them in a queue. $scriptBlockQueue = [Collections.Generic.Queue[ScriptBlock]]::new($ScriptBlock) $resolvedCmds = @{} # Then create a hashtable to store the resolved references $alreadyChecked = [Collections.Generic.List[ScriptBlock]]::new() # and a list of all of the ScriptBlock's we've already taken a look at. # Now it's time for some syntax trickery that should probably be explained. # We're going to want to be able to recursively find references too. # By putting this in a queue, we've already done part of the work, # because we can just enqueue the nested commands. # However, we also want to know _which nested command had which references_ # This means we have to collect all of the references as we go, # and output them in a different way if we're running recursively. # Got all that? # First, we need a tracking variable $CurrentCommand = '.' # for the current command. # Now the syntax trickery: We put the do loop inside of a lambda running in our scope (. {}). # This gives us all of our variables, but lets the results stream to the pipeline. # This is actually pretty important, since this way our tracking variable is accurate when we're outputting the results. # Now that we understand how it works, let's get to: #region Process the Queue of Script Blocks . { $alreadyChecked = [Collections.ArrayList]::new() do { $scriptBlock = $scriptBlockQueue.Dequeue() if ($alreadyChecked -contains $scriptBlock) { continue } $null= $alreadyChecked.Add($ScriptBlock) $foundRefs = $Scriptblock.Ast.FindAll({ param($ast) $ast -is [Management.Automation.Language.CommandAst] -or $ast -is [Management.Automation.Language.TypeConstraintAst] -or $ast -is [Management.Automation.Language.TypeExpressionAst] }, $true) $cmdRefs = [Collections.ArrayList]::new() $cmdStatements = [Collections.ArrayList]::new() $typeRefs = [Collections.ArrayList]::new() foreach ($ref in $foundRefs) { if ($ref -is [Management.Automation.Language.CommandAst]) { $null = $cmdStatements.Add($ref) if (-not $ref.CommandElements) { continue } $theCmd = $ref.CommandElements[0] if ($theCmd.Value) { if (-not $LookedUpCommands[$theCmd.Value]) { $LookedUpCommands[$thecmd.Value] = $ExecutionContext.InvokeCommand.GetCommand($theCmd.Value, 'Cmdlet, Function, Alias') } if ($cmdRefs -notcontains $LookedUpCommands[$theCmd.Value]) { $null = $cmdRefs.Add($LookedUpCommands[$thecmd.Value]) } } else { # referencing a lambda, leave it alone for now } } elseif ($ref.TypeName) { $refType = $ref.TypeName.Fullname -as [type] if ($typeRefs -notcontains $refType) { $null = $typeRefs.Add($refType) } } } [PSCustomObject][Ordered]@{ Commands = $cmdRefs.ToArray() Statements = $cmdStatements.ToArray() Types = $typeRefs.ToArray() } if ($Recurse) { $uniqueCmdRefs | & { process { if ($resolvedCmds.ContainsKey($_.Name)) { return } $nextScriptBlock = $_.ScriptBlock if (-not $nextScriptBlock -and $_.ResolvedCommand.ScriptBlock) { $nextScriptBlock = $_.ResolvedCommand.ScriptBlock } if ($nextScriptBlock) { $scriptBlockQueue.Enqueue($nextScriptBlock) $resolvedCmds[$_.Name] = $true } } } } } while ($ScriptBlockQueue.Count) } | #endregion Process the Queue of Script Blocks #region Handle Each Output & { begin { $refTable = @{} } process { if (-not $Recurse) { return $_ } } } #endregion Handle Each Output #endregion Get the Script References } end { $myParams = @{} + $PSBoundParameters if (-not $allFiles.Count) { return } $c, $t, $id = 0, $allFiles.Count, $(Get-Random) foreach ($file in $allFiles) { $c++ $resolvedFile= try { $ExecutionContext.SessionState.Path.GetResolvedPSPathFromPSPath($file)} catch { $null } if (-not $resolvedFile) { continue } $resolvedFile = [IO.FileInfo]"$resolvedFile" if (-not $resolvedFile.Name) { continue } if (-not $resolvedFile.Length) { continue } if ('.ps1', '.psm1' -notcontains $resolvedFile.Extension) { continue } $p = $c * 100 / $t $text = [IO.File]::ReadAllText($resolvedFile.FullName) $scriptBlock= [ScriptBlock]::Create($text) Write-Progress "Getting References" " $($resolvedFile.Name) " -PercentComplete $p -Id $id if (-not $scriptBlock) { continue } Get-ScriptReference -ScriptBlock $scriptBlock | & { process { $_.psobject.properties.add([Management.Automation.PSNoteProperty]::new('FileName',$resolvedFile.Name)) $_.psobject.properties.add([Management.Automation.PSNoteProperty]::new('FilePath',$resolvedFile.Fullname)) $_.pstypenames.add('HelpOut.Script.Reference') $_ } } Write-Progress "Getting References" " " -Completed -Id $id } } } |