| | 1 | | <# |
| | 2 | | .SYNOPSIS |
| | 3 | | Find variables that are *defined/assigned* in a scriptblock and (optionally) fetch their values. |
| | 4 | | .DESCRIPTION |
| | 5 | | Scans the AST for AssignmentStatementAst where the LHS is a VariableExpressionAst, |
| | 6 | | and optionally Set-Variable/New-Variable calls. Can resolve current values from |
| | 7 | | the appropriate scope (local/script/global) in the *current* runspace. |
| | 8 | | .PARAMETER ScriptBlock |
| | 9 | | ScriptBlock to scan. If omitted, use -Current to inspect the currently running block. |
| | 10 | | .PARAMETER FromParent |
| | 11 | | Inspect the *currently executing* scriptblock (via $MyInvocation.MyCommand.ScriptBlock). |
| | 12 | | .PARAMETER IncludeSetVariable |
| | 13 | | Also detect variables created via Set-Variable/New-Variable (-Name … [-Scope …]). |
| | 14 | | .PARAMETER ResolveValues |
| | 15 | | Resolve current values from variable:<scope>:<name> and include Type/Value. |
| | 16 | | .PARAMETER DefaultScope |
| | 17 | | Scope to assume when the LHS had no explicit scope (default: Local). |
| | 18 | | #> |
| | 19 | | function Get-KrAssignedVariable { |
| | 20 | | [CmdletBinding(DefaultParameterSetName = 'Given')] |
| | 21 | | param( |
| | 22 | | [Parameter(ParameterSetName = 'Given', Position = 0)] |
| | 23 | | [scriptblock]$ScriptBlock, |
| | 24 | |
|
| | 25 | | # NEW: use the caller's scriptblock (parent frame) |
| | 26 | | [Parameter(ParameterSetName = 'FromParent', Mandatory)] |
| | 27 | | [switch]$FromParent, |
| | 28 | |
|
| | 29 | | # How many frames up to climb (1 = immediate caller) |
| | 30 | | [Parameter(ParameterSetName = 'FromParent')] |
| | 31 | | [int]$Up = 1, |
| | 32 | |
|
| | 33 | | # Optional: skip frames from these modules when searching |
| | 34 | | [Parameter(ParameterSetName = 'FromParent')] |
| 1 | 35 | | [string[]]$ExcludeModules = @('Kestrun'), |
| | 36 | |
|
| | 37 | | [switch]$IncludeSetVariable, |
| | 38 | | [switch]$ResolveValues, |
| | 39 | | [ValidateSet('Local', 'Script', 'Global')] |
| | 40 | | [string]$DefaultScope = 'Script' |
| | 41 | | ) |
| | 42 | |
|
| | 43 | | # ---------- resolve $ScriptBlock source ---------- |
| 1 | 44 | | if ($FromParent.IsPresent) { |
| 1 | 45 | | $allFrames = Get-PSCallStack |
| | 46 | | # 0 = this function, 1 = immediate caller, 2+ = higher parents |
| 2 | 47 | | $frames = $allFrames | Select-Object -Skip 1 |
| | 48 | |
|
| 1 | 49 | | if ($ExcludeModules.Count) { |
| 2 | 50 | | $frames = $frames | Where-Object { |
| 1 | 51 | | $mn = $_.InvocationInfo.MyCommand.ModuleName |
| 3 | 52 | | -not ($mn -and ($mn -in $ExcludeModules)) |
| | 53 | | } |
| | 54 | | } |
| | 55 | |
|
| | 56 | | # pick the desired parent frame |
| 3 | 57 | | $frame = $frames | Select-Object -Skip ($Up - 1) -First 1 |
| 1 | 58 | | if (-not $frame) { throw "No parent frame found (Up=$Up)." } |
| | 59 | |
|
| | 60 | | # Figure out how far “up” that is compared to the original call stack |
| 2 | 61 | | $scopeUp = ($allFrames.IndexOf($frame)) + 1 |
| 1 | 62 | | if ($scopeUp -lt 1) { throw "Parent frame not found." } |
| | 63 | |
|
| | 64 | | # prefer its live ScriptBlock; if null, rebuild from file |
| 1 | 65 | | $ScriptBlock = $frame.InvocationInfo.MyCommand.ScriptBlock |
| 1 | 66 | | if (-not $ScriptBlock -and $frame.ScriptName) { |
| 0 | 67 | | $ScriptBlock = [scriptblock]::Create((Get-Content -Raw -LiteralPath $frame.ScriptName)) |
| | 68 | | } |
| 1 | 69 | | if (-not $ScriptBlock) { throw "Parent frame has no scriptblock or script file to parse." } |
| | 70 | | } |
| | 71 | |
|
| 1 | 72 | | if (-not $ScriptBlock) { |
| 0 | 73 | | throw "No scriptblock provided. Use -FromParent or pass a ScriptBlock." |
| | 74 | | } |
| 2 | 75 | | $ast = ($ScriptBlock.Ast).ToString() |
| | 76 | |
|
| 1 | 77 | | $endstring = $ast.IndexOf("Enable-KrConfiguration", [StringComparison]::OrdinalIgnoreCase) |
| 1 | 78 | | if ($endstring -lt 0) { |
| 0 | 79 | | throw "The provided scriptblock does not appear to contain 'Enable-KrConfiguration' call." |
| | 80 | | } |
| 1 | 81 | | $ast = $ast.Substring(0, $endstring).Trim() |
| 1 | 82 | | if ($ast.StartsWith('{')) { |
| 1 | 83 | | $ast += "`n}" |
| | 84 | | } |
| 1 | 85 | | $ScriptBlock = [scriptblock]::Create($ast) |
| | 86 | |
|
| | 87 | |
|
| | 88 | | <# |
| | 89 | | .SYNOPSIS |
| | 90 | | Checks if a given AST node is inside a function. |
| | 91 | | .DESCRIPTION |
| | 92 | | This function traverses the parent nodes of the given AST node to determine if it is |
| | 93 | | located within a function definition. |
| | 94 | | .PARAMETER node |
| | 95 | | The AST node to check. |
| | 96 | | .OUTPUTS |
| | 97 | | [bool] Returns true if the node is inside a function, false otherwise. |
| | 98 | | #> |
| | 99 | | function _IsInFunction([System.Management.Automation.Language.Ast] $node) { |
| 1 | 100 | | $p = $node.Parent |
| 1 | 101 | | while ($p) { |
| 1 | 102 | | if ($p -is [System.Management.Automation.Language.FunctionDefinitionAst]) { return $true } |
| 1 | 103 | | $p = $p.Parent |
| | 104 | | } |
| 1 | 105 | | return $false |
| | 106 | | } |
| | 107 | |
|
| 1 | 108 | | $assignAsts = $ScriptBlock.Ast.FindAll( |
| 1 | 109 | | { param($n) $n -is [System.Management.Automation.Language.AssignmentStatementAst] }, $true) |
| | 110 | |
|
| 1 | 111 | | foreach ($a in $assignAsts) { |
| 1 | 112 | | $varAst = $a.Left.Find( |
| 1 | 113 | | { param($n) $n -is [System.Management.Automation.Language.VariableExpressionAst] }, $true |
| 1 | 114 | | ) | Select-Object -First 1 |
| 1 | 115 | | if (-not $varAst) { continue } |
| | 116 | |
|
| 1 | 117 | | $vp = $varAst.VariablePath |
| 1 | 118 | | $name = $vp.UnqualifiedPath |
| | 119 | |
|
| 2 | 120 | | if (-not $name) { $name = $vp.UserPath } # ← fallback |
| 1 | 121 | | if (-not $name) { continue } |
| 1 | 122 | | $name = $name -replace '^[^:]*:', '' |
| 1 | 123 | | if ($name.Contains('.') -or $name.Contains('[')) { continue } |
| 1 | 124 | | if ($name.StartsWith('{') -and $name.EndsWith('}')) { |
| 0 | 125 | | $name = $name.Substring(1, $name.Length - 2) # ← ${foo} → foo |
| | 126 | | } |
| 1 | 127 | | $val = Get-Variable -Name $name -Scope $scopeUp -ErrorAction SilentlyContinue |
| | 128 | |
|
| 1 | 129 | | [pscustomobject]@{ |
| 1 | 130 | | Name = $name |
| 1 | 131 | | ScopeHint = $scope |
| 1 | 132 | | ProviderPath = $provider |
| 1 | 133 | | Source = 'Assignment' |
| 1 | 134 | | Operator = $a.Operator.ToString() |
| 1 | 135 | | Type = $type |
| 1 | 136 | | Value = $val |
| | 137 | | } |
| | 138 | | } |
| | 139 | |
|
| 1 | 140 | | if ($IncludeSetVariable) { |
| 1 | 141 | | $cmdAsts = $ScriptBlock.Ast.FindAll( |
| 2 | 142 | | { param($n) $n -is [System.Management.Automation.Language.CommandAst] -and -not (_IsInFunction $n) }, $true) |
| | 143 | |
|
| 1 | 144 | | foreach ($c in $cmdAsts) { |
| 1 | 145 | | $cmd = $c.GetCommandName() |
| 1 | 146 | | if ($cmd -notin 'Set-Variable', 'New-Variable') { continue } |
| 0 | 147 | | $named = @{} |
| 0 | 148 | | foreach ($e in $c.CommandElements) { |
| 0 | 149 | | if ($e -is [System.Management.Automation.Language.CommandParameterAst] -and $e.ParameterName -in 'Name', |
| 0 | 150 | | $arg = $e.Argument |
| 0 | 151 | | if ($arg -is [System.Management.Automation.Language.StringConstantExpressionAst]) { |
| 0 | 152 | | $named[$e.ParameterName] = $arg.Value |
| | 153 | | } |
| | 154 | | } |
| | 155 | | } |
| 0 | 156 | | if ($named.ContainsKey('Name')) { |
| 0 | 157 | | $name = $named['Name'] |
| 0 | 158 | | $scope = $named['Scope'] ?? $DefaultScope |
| 0 | 159 | | $provider = "variable:$($scope):$name" |
| 0 | 160 | | $val = $null; $type = $null |
| 0 | 161 | | if ($ResolveValues) { |
| | 162 | | try { |
| 0 | 163 | | $val = (Get-Item -EA SilentlyContinue $provider).Value |
| 0 | 164 | | if ($null -ne $val) { $type = $val.GetType().FullName } |
| | 165 | | } catch { |
| 0 | 166 | | Write-Warning "Failed to resolve variable '$name' in scope '$scope': $_" |
| | 167 | | } |
| | 168 | | } |
| 0 | 169 | | [pscustomobject]@{ |
| 0 | 170 | | Name = $name |
| 0 | 171 | | ScopeHint = $scope |
| 0 | 172 | | ProviderPath = $provider |
| 0 | 173 | | Source = $cmd |
| 0 | 174 | | Operator = $null |
| 0 | 175 | | Type = $type |
| 0 | 176 | | Value = $val |
| 0 | 177 | | } | ForEach-Object { [void]$rows.Add($_) } |
| | 178 | | } |
| | 179 | | } |
| | 180 | | } |
| | 181 | |
|
| | 182 | | # keep last occurrence per (ScopeHint, Name) |
| 3 | 183 | | $rows | Group-Object ScopeHint, Name | ForEach-Object { $_.Group[-1] } |
| | 184 | | } |
| | 185 | |
|