Public/Invoke-WithEcho.ps1
|
function Invoke-WithEcho { <# .SYNOPSIS Runs a script block and logs the command text beforehand — like ECHO ON in DOS batch files. .DESCRIPTION The command text is written unchanged via Write-Host (information stream: visible in the console, captured by Start-Transcript, redirectable with 6>). Below it, every variable the block reads appears as a row in an indented table (Variable, Type, Count, Value), the value collapsed to a single line and truncated to -MaxValueLength characters. Resolution only reads variable values in the caller's scope and never executes anything. The block's pipeline output is passed through unchanged. Assignments therefore belong outside the call, not inside the block: $x = Invoke-WithEcho { Get-ChildItem $path } # correct Invoke-WithEcho { $x = Get-ChildItem $path } # $x evaporates in the child scope .PARAMETER ScriptBlock The block to execute. .PARAMETER MaxValueLength Maximum length per logged variable value (default 100); longer values end with …. .PARAMETER NoExpand Suppresses the variable lines; only the command text is logged. .PARAMETER CommandColor Console color of the command lines (default: Cyan). Affects console rendering only; the transcript contains plain text. .PARAMETER ValueColor Console color of the variable lines (default: DarkGray). .PARAMETER NoColor Writes all echo lines in the console's default color. .EXAMPLE $files = Invoke-WithEcho { Get-ChildItem $sourcePath -Filter *.csv } #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '', Justification = 'Write-Host is a deliberate design decision here: the information stream is captured by Start-Transcript, redirectable with 6>, and never pollutes the return value.')] [CmdletBinding()] param( [Parameter(Mandatory, Position = 0)] [scriptblock] $ScriptBlock, [ValidateRange(1, [int]::MaxValue)] [int] $MaxValueLength = 100, [switch] $NoExpand, [ConsoleColor] $CommandColor = [ConsoleColor]::Cyan, [ConsoleColor] $ValueColor = [ConsoleColor]::DarkGray, [switch] $NoColor ) $commandStyle = @{} $valueStyle = @{} if (-not $NoColor) { $commandStyle['ForegroundColor'] = $CommandColor $valueStyle['ForegroundColor'] = $ValueColor } $lines = $ScriptBlock.ToString().Trim() -split '\r?\n' $indent = ($lines | Select-Object -Skip 1 | Where-Object { $_.Trim() } | ForEach-Object { $_.Length - $_.TrimStart().Length } | Measure-Object -Minimum).Minimum foreach ($line in $lines) { if ($indent -and $line.Length -ge $indent -and -not $line.Substring(0, $indent).Trim()) { $line = $line.Substring($indent) } Write-Host ">> $line" @commandStyle } if (-not $NoExpand) { $occurrences = $ScriptBlock.Ast.FindAll( { param($node) $node -is [System.Management.Automation.Language.VariableExpressionAst] }, $true) | Sort-Object { $_.Extent.StartOffset } # Only values read from the caller's scope are logged: a variable whose # first read happens after a completed assignment inside the block is # block-local and does not appear. $firstRead = @{} $firstWrite = @{} foreach ($occurrence in $occurrences) { $name = $occurrence.VariablePath.UserPath if ($name -in $script:SkipVariables) { continue } $use = Get-EchoVariableUse -Variable $occurrence if ($use.Reads -and -not $firstRead.ContainsKey($name)) { $firstRead[$name] = $occurrence.Extent.StartOffset } if ($null -ne $use.WriteOffset -and (-not $firstWrite.ContainsKey($name) -or $use.WriteOffset -lt $firstWrite[$name])) { $firstWrite[$name] = $use.WriteOffset } } $names = $firstRead.Keys | Where-Object { -not $firstWrite.ContainsKey($_) -or $firstRead[$_] -lt $firstWrite[$_] } | Sort-Object $rows = foreach ($name in $names) { try { $value = $PSCmdlet.GetVariableValue($name) } catch { continue } $itemCount = if ($null -eq $value) { '-' } elseif ($value -is [System.Collections.IDictionary]) { "$($value.Count)" } elseif ($value -is [System.Collections.IEnumerable] -and $value -isnot [string]) { "$(@($value).Count)" } else { '1' } [pscustomobject]@{ Name = "`$$name" Type = if ($null -eq $value) { '-' } else { $value.GetType().Name } ItemCount = $itemCount Value = Format-EchoValue -Value $value -MaxLength $MaxValueLength } } if ($rows) { $nameWidth = (@('Variable') + @($rows.Name) | Measure-Object -Property Length -Maximum).Maximum $typeWidth = (@('Type') + @($rows.Type) | Measure-Object -Property Length -Maximum).Maximum $countWidth = (@('Count') + @($rows.ItemCount) | Measure-Object -Property Length -Maximum).Maximum $rowFormat = " {0,-$nameWidth} {1,-$typeWidth} {2,$countWidth} {3}" Write-Host ($rowFormat -f 'Variable', 'Type', 'Count', 'Value') @valueStyle Write-Host ($rowFormat -f ('-' * 8), ('-' * 4), ('-' * 5), ('-' * 5)) @valueStyle foreach ($row in $rows) { Write-Host ($rowFormat -f $row.Name, $row.Type, $row.ItemCount, $row.Value) @valueStyle } } } & $ScriptBlock } |