Eigenverft.Manifested.Drydock.Execution.ps1
| function Invoke-Exec { <# .SYNOPSIS Run an external executable with merged per-call and shared arguments, optional timing/output capture, strict exit-code validation, and configurable return shaping. .DESCRIPTION This function invokes an external process and enforces a clear separation between: - Arguments - Command-specific parameters (subcommands, positional paths, call-specific flags). - CommonArguments - Stable, shared parameters reused across many calls. Combination order: Arguments first, then CommonArguments (keeps subcommands/positionals early; shared flags last). Behavior - If -CaptureOutput is true, returns the command's output; else streams to host and returns $null. - If -MeasureTime is true, prints elapsed time. - Exit code must be in AllowedExitCodes. If not: - If the code is 0 but 0 is disallowed, map to custom code 99; otherwise exit with the actual code. - Diagnostics include a "Full Command" line and, on error, optional echo of captured output. Return shaping (applies only when -CaptureOutput:$true) - Objects : Preserve native objects (PowerShell may auto-collapse a single item). - Strings : Force string[] (each item cast to [string]). - Text : Single string with all (stringified) lines joined by the platform newline. (Default) Tip for single-value “true”/“false” outputs from a CLI: - Use `-ReturnType Objects` to keep native typing and then coerce explicitly when needed, e.g.: `[bool]::Parse([string](Invoke-Exec2 ... -ReturnType Objects))` .PARAMETER Executable The program to run (path or name resolvable via PATH). .PARAMETER Arguments Per-invocation arguments (subcommands, positional paths, and flags unique to this call). Preserves order and precedes CommonArguments. .PARAMETER CommonArguments Reusable, environment- or pipeline-wide arguments appended after Arguments. Intended for consistency and DRY usage across calls. .PARAMETER HideValues String values to mask in diagnostic command displays. These do not affect actual execution. Any substring match in arguments is replaced with "[HIDDEN]" for display. .PARAMETER MeasureTime When true (default), measures and prints elapsed execution time. .PARAMETER CaptureOutput When true (default), captures and returns process output; when false, streams to the host and returns $null. .PARAMETER CaptureOutputDump When true and -CaptureOutput:$false, suppresses streaming and discards process output. .PARAMETER AllowedExitCodes Exit codes considered successful; defaults to @(0). If 0 is excluded and occurs, it is treated as an error and mapped to 99. .PARAMETER ReturnType Shapes the return value when output is captured (ignored if -CaptureOutput:$false). Allowed: Objects | Strings | Text (default: Text) .OUTPUTS System.String (when -CaptureOutput and -ReturnType Text) System.String[] (when -CaptureOutput and -ReturnType Strings) System.Object[] or scalar (when -CaptureOutput and -ReturnType Objects; PowerShell may collapse single item) System.Object ($null when -CaptureOutput:$false) .EXAMPLE # Reuse shared flags; keep default shaping as a single text blob $common = @("--verbosity","minimal","-c","Release") $txt = Invoke-Exec -Executable "dotnet" -Arguments @("build","MyApp.csproj") -CommonArguments $common -ReturnType Text .EXAMPLE # Capture a single boolean-like value robustly $raw = Invoke-Exec -Executable "cmd" -Arguments @("/c","echo","True") -ReturnType Objects $ok = [bool]::Parse([string]$raw) # $ok = $true .EXAMPLE # Mask a password sourced from a CI pipeline environment variable (e.g., Azure DevOps, GitHub Actions) # The real value is used for execution, but the displayed command is scrubbed. $pwd = $env:TOOL_PASSWORD # Provided by the pipeline as a secret env var Invoke-Exec -Executable "tool" -Arguments @("--password=$pwd") -HideValues @($pwd) # Displays: ==> Full Command: tool --password=[HIDDEN] .EXAMPLE # Variant: CLI expects the value as a separate argument (no inline '=' form) $pwd = $env:TOOL_PASSWORD Invoke-Exec -Executable "tool" -Arguments @("--password", $pwd) -HideValues @($pwd) # Displays: ==> Full Command: tool --password [HIDDEN] .EXAMPLE # Variant: multiple sensitive tokens (e.g., token + url with embedded token) $token = $env:API_TOKEN $url = "https://api.example.com?access_token=$token" Invoke-Exec -Executable "curl" -Arguments @("-H", "Authorization: Bearer $token", $url) -HideValues @($token) # Displays: ==> Full Command: curl -H Authorization: Bearer [HIDDEN] https://api.example.com?access_token=[HIDDEN] #> [CmdletBinding()] [Alias('iexec')] param( [Alias('exe')] [Parameter(Mandatory = $true)] [string]$Executable, [Alias('args')] [Parameter(Mandatory = $true)] [string[]]$Arguments, [Alias('argsc')] [Parameter(Mandatory = $false)] [string[]]$CommonArguments, [Alias('hide','mask','hidevalue','maskval')] [Parameter(Mandatory = $false)] [string[]]$HideValues = @(), [Alias('mt')] [bool]$MeasureTime = $true, [Alias('co')] [bool]$CaptureOutput = $true, [Alias('cod')] [bool]$CaptureOutputDump = $false, [Alias('ok')] [int[]]$AllowedExitCodes = @(0), [Alias('rt')] [ValidateSet('Objects','Strings','Text')] [string]$ReturnType = 'Text' ) # Internal fixed values for custom error handling $ExtraErrorMessage = "Disallowed exit code 0 exitcode encountered." $CustomErrorCode = 99 # Combine CommonArguments and Arguments (handle null or empty) $finalArgs = @() if ($Arguments -and $Arguments.Count -gt 0) { $finalArgs += $Arguments } if ($CommonArguments -and $CommonArguments.Count -gt 0) { $finalArgs += $CommonArguments } # Build display-only args with masking (execution still uses $finalArgs) $displayArgs = @($finalArgs) if ($HideValues -and $HideValues.Count -gt 0) { foreach ($h in $HideValues) { if ([string]::IsNullOrWhiteSpace($h)) { continue } $pattern = [regex]::Escape($h) $displayArgs = $displayArgs | ForEach-Object { $_ -replace $pattern, '[HIDDEN]' } } } Write-Host "===> Before Command (Executable: $Executable, Args Count: $($finalArgs.Count)) ==============================================" -ForegroundColor Yellow Write-Host "===> Full Command: $Executable $($displayArgs -join ' ')" -ForegroundColor Cyan if ($MeasureTime) { $stopwatch = [System.Diagnostics.Stopwatch]::StartNew() } if ($CaptureOutput) { $result = & $Executable @finalArgs } else { if ($CaptureOutputDump) { & $Executable @finalArgs | Out-Null } else { & $Executable @finalArgs } $result = $null } if ($MeasureTime) { $stopwatch.Stop() } # Check if the actual exit code is allowed. if (-not ($AllowedExitCodes -contains $LASTEXITCODE)) { if ($CaptureOutput -and $result) { Write-Host "===> Captured Output:" -ForegroundColor Yellow $result | ForEach-Object { Write-Host $_ -ForegroundColor Gray } } if ($LASTEXITCODE -eq 0) { Write-Error "Command '$Executable $($displayArgs -join ' ')' returned exit code 0, which is disallowed. $ExtraErrorMessage Translated to custom error code $CustomErrorCode." if ($MeasureTime) { Write-Host "===> After Command (Execution time: $($stopwatch.Elapsed)) ==============================================" -ForegroundColor DarkGreen } else { Write-Host "===> After Command ==============================================" -ForegroundColor DarkGreen } exit $CustomErrorCode } else { Write-Error "Command '$Executable $($displayArgs -join ' ')' returned disallowed exit code $LASTEXITCODE. Exiting script with exit code $LASTEXITCODE." if ($MeasureTime) { Write-Host "===> After Command (Execution time: $($stopwatch.Elapsed)) ==============================================" -ForegroundColor DarkGreen } else { Write-Host "===> After Command ==============================================" -ForegroundColor DarkGreen } exit $LASTEXITCODE } } if ($MeasureTime) { Write-Host "===> After Command (Execution time: $($stopwatch.Elapsed)) ==============================================" -ForegroundColor DarkGreen } else { Write-Host "===> After Command ==============================================" -ForegroundColor DarkGreen } # Return shaping if (-not $CaptureOutput) { return $null } switch ($ReturnType.ToLowerInvariant()) { 'objects' { if ($null -eq $result) { return @() } return @($result) } 'strings' { if ($null -eq $result) { return @() } return @($result | ForEach-Object { [string]$_ }) } 'text' { if ($null -eq $result) { return '' } $lines = $result | ForEach-Object { [string]$_ } return ($lines -join [Environment]::NewLine) } } } |