Private/Install/Read-PwshProfileInstalledSetting.ps1
|
function Read-PwshProfileInstalledSetting { <# .SYNOPSIS Parses an existing managed bootstrap block back into the prior run's settings plus its recorded tools snapshot, for re-run prefill and new-tool detection. .DESCRIPTION When Install-PwshProfile re-runs against a profile that already carries a managed block, the wizard should default to the choices from last time and highlight tools added to the module since. This helper reads that back: - It locates the managed region between the Get-PwshProfileMarker sentinels. - It AST-parses the embedded Initialize-PwshProfile call ([System.Management.Automation.Language.Parser]::ParseInput) and maps the bound parameters to a settings hashtable (only the keys that were present): Theme, CustomTheme, BannerText, BannerColor, BannerAlignment, BannerFont, BannerFontPath, StepIcon, ZoxideCommand, BatTheme, BatStyle (strings); Enable (string[]); EnableAll, NoBanner, ReplaceCat, ReplaceMore (switches -> $true when present). - It reads the `# Tools available: a,b,c` snapshot comment (the full catalog at write time) into ToolSnapshot. It is failure-tolerant by design (it feeds an interactive setup, but must never throw): a missing file, a missing block, a parse error, or a missing Initialize call all return $null, and the caller falls back to first-run defaults. Returns a hashtable @{ Settings = <parsed subset>; ToolSnapshot = <string[]> } on success, or $null when there is nothing usable to read. .PARAMETER Path The profile file to read. .EXAMPLE $prior = Read-PwshProfileInstalledSetting -Path $PROFILE Returns the prior settings + tools snapshot, or $null if the profile has no managed block. #> [CmdletBinding()] param( [Parameter(Mandatory, Position = 0)] [string]$Path ) try { if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { return $null } $content = Get-Content -LiteralPath $Path -Raw -Encoding utf8 if ([string]::IsNullOrEmpty($content)) { return $null } $marker = Get-PwshProfileMarker $pattern = '(?s)' + [regex]::Escape($marker.Open) + '(.*?)' + [regex]::Escape($marker.Close) $match = [regex]::Match($content, $pattern) if (-not $match.Success) { return $null } $block = $match.Groups[1].Value # The recorded tools snapshot (catalog at write time) — the baseline for new-tool detection. $snapshot = @() $snapMatch = [regex]::Match($block, '(?im)^\s*#\s*Tools available:\s*(.+?)\s*$') if ($snapMatch.Success) { $snapshot = @($snapMatch.Groups[1].Value -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) } # Parse the block and find the Initialize-PwshProfile command. $tokens = $null; $errors = $null $ast = [System.Management.Automation.Language.Parser]::ParseInput($block, [ref]$tokens, [ref]$errors) $cmd = $ast.FindAll({ $args[0] -is [System.Management.Automation.Language.CommandAst] -and $args[0].GetCommandName() -eq 'Initialize-PwshProfile' }, $true) | Select-Object -First 1 if (-not $cmd) { return $null } # Extract a scalar string from a value AST (constant or expandable string keep their literal # text, e.g. '$env:COMPUTERNAME'); fall back to the source text for anything unusual. $scalar = { param($node) if ($node -is [System.Management.Automation.Language.StringConstantExpressionAst] -or $node -is [System.Management.Automation.Language.ExpandableStringExpressionAst]) { return $node.Value } try { return [string]$node.SafeGetValue() } catch { return $node.Extent.Text } } # Extract a value AST as either a scalar or an array (for -Enable a,b,c / -Enable @()). $value = { param($node) if ($node -is [System.Management.Automation.Language.ArrayLiteralAst]) { return @($node.Elements | ForEach-Object { & $scalar $_ }) } if ($node -is [System.Management.Automation.Language.ArrayExpressionAst]) { # Handles the @(...) form, e.g. `-Enable @()` (no sub-statements -> empty) or # `-Enable @('Zoxide','Bat')`. This is tuned to the array shape Build-PwshProfile- # InitializeCall emits, where every element is a plain string literal; the recursive # FindAll harvests those constants. It is NOT a general expression evaluator — for an # arbitrary @(...) it would also collect strings nested in sub-expressions. $items = @($node.FindAll({ $args[0] -is [System.Management.Automation.Language.StringConstantExpressionAst] }, $true) | ForEach-Object { $_.Value }) return $items } & $scalar $node } $stringParams = @('Theme', 'CustomTheme', 'BannerText', 'BannerColor', 'BannerAlignment', 'BannerFont', 'BannerFontPath', 'StepIcon', 'ZoxideCommand', 'BatTheme', 'BatStyle') $switchParams = @('EnableAll', 'NoBanner', 'ReplaceCat', 'ReplaceMore') # Canonical-case lookup so '-bannertext' etc. still map to the proper key. $canon = @{} foreach ($p in $stringParams + $switchParams + 'Enable') { $canon[$p.ToLowerInvariant()] = $p } $settings = @{} $elements = @($cmd.CommandElements) for ($idx = 1; $idx -lt $elements.Count; $idx++) { $el = $elements[$idx] if ($el -isnot [System.Management.Automation.Language.CommandParameterAst]) { continue } $name = $canon[$el.ParameterName.ToLowerInvariant()] if (-not $name) { continue } # A value can be attached (-Foo:bar) or be the next element (-Foo bar). Switches usually # have neither. $argNode = $el.Argument if (-not $argNode -and ($idx + 1) -lt $elements.Count -and $elements[$idx + 1] -isnot [System.Management.Automation.Language.CommandParameterAst]) { $argNode = $elements[$idx + 1] $idx++ } if ($switchParams -contains $name) { # Present switch -> $true unless explicitly -Foo:$false. if ($argNode) { $settings[$name] = [bool](& $value $argNode) } else { $settings[$name] = $true } } elseif ($name -eq 'Enable') { $settings.Enable = @(if ($argNode) { & $value $argNode }) } elseif ($argNode) { $settings[$name] = [string](& $value $argNode) } } return @{ Settings = $settings; ToolSnapshot = $snapshot } } catch { Write-Warning "Read-PwshProfileInstalledSetting: could not parse the existing bootstrap ($($_.Exception.Message)); treating as a fresh setup." return $null } } |