Native Executables, Exit Codes, Arguments, and Cross-Shell Boundaries
Learn the PowerShell-to-native process boundary: executable discovery, argument passing, text streams, exit codes, modern PowerShell 7.x native preferences, and safe cross-platform wrappers.
Learning objectives
By the end of this lesson
- Differentiate a PowerShell cmdlet/function invocation from launching a native executable process.
- Use $LASTEXITCODE and $? correctly after native commands and explain what each signal represents.
- Describe how native stdout/stderr differ from PowerShell rich object output.
- Inspect and reason about modern $PSNativeCommandArgumentPassing and $PSNativeCommandUseErrorActionPreference behavior.
- Pass native arguments as structured values and build a small cross-platform lab using pwsh as the guaranteed native executable.
1. A native command crosses into another process
When you run a cmdlet such as Get-Process, PowerShell invokes a PowerShell command that participates directly in the engine’s parameter binding, streams, and object model. When you run a native executable such as pwsh, git, or curl, PowerShell starts another operating-system process.
Get-Command Get-Process | Select-Object Name, CommandType, Source
Get-Command pwsh -CommandType Application | Select-Object Name, CommandType, PathThat process boundary matters. PowerShell must convert argument values into the form the child process receives. The child communicates success primarily through a numeric process exit code and typically communicates data through stdout/stderr text streams. Those are not the same contracts as PowerShell rich objects and PowerShell error records.
2. Compare a cmdlet object with native text output
First inspect a PowerShell object:
$processObject = Get-Process -Id $PID
$processObject.GetType().FullName
$processObject | Get-Member | Select-Object -First 8 Name, MemberTypeNow ask a child pwsh process to print its version. The executable writes text to its standard output; the parent PowerShell receives that output as string data:
$pwsh = (Get-Command pwsh -CommandType Application).Path
$nativeOutput = & $pwsh -NoProfile -Command '$PSVersionTable.PSVersion.ToString()'
$nativeOutput
$nativeOutput.GetType().FullNameThe exact type display can reflect collection/scalar behavior when multiple lines are returned, but the conceptual difference is stable: the child process did not send a live System.Diagnostics.Process object across the operating-system process boundary. It emitted text.
3. $LASTEXITCODE is the native process result signal
Native programs return an integer exit code to the parent process. By long-standing convention, zero commonly means success and nonzero commonly means some kind of failure—but every tool defines its own contract. PowerShell stores the most recent native exit code in $LASTEXITCODE.
& $pwsh -NoProfile -Command 'exit 0'
$LASTEXITCODE
& $pwsh -NoProfile -Command 'exit 7'
$LASTEXITCODE0
7Do not discard a nonzero code merely because the tool printed useful-looking output. In automation, the exit-code contract is often what CI runners and wrapper scripts use to decide success.
4. $? is a PowerShell success flag for the last operation
$? is a Boolean automatic variable that reports success/failure of the last operation. For native executables, current PowerShell sets $? to $true when $LASTEXITCODE is zero and $false when it is nonzero.
& $pwsh -NoProfile -Command 'exit 0'
[pscustomobject]@{ LastExitCode = $LASTEXITCODE; Success = $? }
& $pwsh -NoProfile -Command 'exit 9'
[pscustomobject]@{ LastExitCode = $LASTEXITCODE; Success = $? }Be careful when inspecting these variables: every additional command you run can update session state. Capture values immediately after the native invocation when they matter.
Zero/nonzero is common, but some tools use multiple nonzero codes for informational outcomes. A production wrapper should implement the specific tool’s documented policy rather than assuming every nonzero value means the same thing.
5. Keep native arguments as separate values
The safest mental model is executable + ordered argument values. Avoid concatenating all arguments into one shell-like string. PowerShell 7.3 introduced a newer native argument-passing implementation that better preserves quotes and empty-string arguments.
$PSNativeCommandArgumentPassing
$arguments = @(
'-NoProfile'
'-Command'
'exit 0'
)
& $pwsh @arguments
$LASTEXITCODEArray splatting passes the array elements as command arguments. That preserves logical boundaries in your PowerShell code and makes logging/testing easier. The exact conversion at the native boundary is controlled by the current native argument-passing mode.
6. Modern PowerShell 7.x native argument-passing modes
In current PowerShell 7.x, $PSNativeCommandArgumentPassing can be Legacy, Standard, or Windows. Microsoft documents the default as Windows on Windows and Standard on non-Windows platforms.
| Mode | Meaning at a beginner level |
|---|---|
Standard | Use the modern native argument-passing behavior. This is the default on non-Windows platforms. |
Windows | Use the modern behavior generally, but automatically use legacy-style passing for specific Windows executables/file types such as cmd/batch-family scenarios documented by Microsoft. This is the default on Windows. |
Legacy | Use the historical argument behavior for compatibility with scripts/tools that depended on it. |
[pscustomobject]@{
PowerShellVersion = $PSVersionTable.PSVersion.ToString()
PlatformMode = $PSNativeCommandArgumentPassing
}Do not change this preference globally just because one old command behaves strangely. First identify the target tool, its quoting requirements, and whether the script depends on legacy behavior. Scope compatibility work narrowly.
7. stdout/stderr are native streams, not rich PowerShell objects
Native programs conventionally write normal output to stdout and diagnostics/errors to stderr. PowerShell can capture and redirect these streams, but the originating contract remains a native text-stream contract.
# Child process writes one line to stdout and exits successfully.
$out = & $pwsh -NoProfile -Command "[Console]::Out.WriteLine('native stdout')"
$out
$out.GetType().FullName
# Capture both native stdout and stderr into the success pipeline for inspection.
$combined = & $pwsh -NoProfile -Command "[Console]::Out.WriteLine('out'); [Console]::Error.WriteLine('err')" 2>&1
$combinedRedirection can deliberately merge streams, but do not confuse “I captured text” with “the child succeeded.” Inspect the exit code separately. Chapter 10 develops PowerShell streams and redirection in much more detail.
8. $PSNativeCommandUseErrorActionPreference can integrate nonzero exits with PowerShell errors
Current PowerShell exposes $PSNativeCommandUseErrorActionPreference. When it is $true, native commands that return nonzero exit codes issue PowerShell errors according to $ErrorActionPreference. This can make native failures participate more naturally in PowerShell error workflows.
$PSNativeCommandUseErrorActionPreferenceDo not assume enabling it globally is always correct. Some native tools assign nonzero codes to outcomes that are not failures. A wrapper can temporarily scope behavior or handle $LASTEXITCODE explicitly using the tool’s documented contract.
This lesson targets current PowerShell 7.6.x behavior. Windows PowerShell 5.1 and older PowerShell 7 releases have different native argument/error capabilities. Treat compatibility as an explicit requirement, not an invisible default.
9. Lab: use pwsh itself as a guaranteed native executable
Because you are already running PowerShell, the pwsh executable is the most reliable cross-platform native tool for this lab. We will create a disposable child script that prints the arguments it received and exits with a caller-selected code.
$lab = Join-Path $HOME 'devops-academy/powershell/chapter02/lesson05'
New-Item -ItemType Directory -Path $lab -Force | Out-Null
$child = Join-Path $lab 'show-args.ps1'
@'
$index = 0
foreach ($arg in $args) {
"ARG[$index]=<$arg>"
$index++
}
exit 0
'@ | Set-Content -LiteralPath $child -Encoding utf8
$nativeArgs = @(
'-NoProfile'
'-File'
$child
'alpha'
'two words'
'$HOME stays data'
'value=with=equals'
)
& $pwsh @nativeArgs
$exitCode = $LASTEXITCODE
$success = $?
[pscustomobject]@{ ExitCode = $exitCode; Success = $success }Expected observation: each logical value should appear as one child-script argument, including two words and the literal dollar-sign text. Your paths differ, but the argument boundaries should remain visible. The child exits zero, so $LASTEXITCODE should be 0 and $? should be true when captured immediately.
Now exercise a known failure code without changing system state:
& $pwsh -NoProfile -Command 'exit 23'
$failedCode = $LASTEXITCODE
$failed = $?
[pscustomobject]@{ ExitCode = $failedCode; Success = $failed }ExitCode Success
-------- -------
23 FalseVerification checklist
10. Cleanup the disposable native-process lab
The only state created by the lab is the isolated lesson directory under your home-folder course workspace. Preview the exact target, then remove it if it matches the lab path you created:
Set-Location $HOME
Remove-Item -LiteralPath $lab -Recurse -Force -WhatIf
# Execute only after verifying the WhatIf target.
Remove-Item -LiteralPath $lab -Recurse -Force
Test-Path -LiteralPath $labThe final result should be False. The course root can remain for later chapters.
11. Common native-boundary mistakes
Assuming printed output means success. Check the documented exit-code contract.
Treating native text like rich cmdlet objects. Parse or convert it deliberately, or prefer a PowerShell-native API/cmdlet when one provides structured data.
Building a single command string. Preserve the executable and each argument as separate values.
Copying Windows PowerShell 5.1 quoting workarounds into modern PowerShell without testing. Current PowerShell uses newer native argument behavior.
Assuming every nonzero exit code is the same error. Implement the target tool’s documented policy.
Changing global native preferences to fix one tool. Prefer narrow compatibility handling and document why it is needed.
12. Knowledge check
Question 1. What does $LASTEXITCODE preserve after a native executable?
Question 2. For a native command in current PowerShell, when is $? normally false?
$LASTEXITCODE.Question 3. Why is an argument array preferable to one concatenated command string?
Question 4. What is the default $PSNativeCommandArgumentPassing mode on Windows versus non-Windows in modern PowerShell?
Windows on Windows and Standard on non-Windows platforms.Question 5. Does native stdout automatically contain PowerShell rich objects?
13. Summary
Native executables are a separate process boundary. Discover the executable, keep arguments structured, understand the current argument-passing mode, treat stdout/stderr as native text streams, and capture the numeric exit code immediately. $? is a Boolean success signal; $LASTEXITCODE retains the tool-specific numeric result. This distinction is foundational for reliable CI, cloud CLIs, package managers, Git, container tools, and every other native utility PowerShell orchestrates.
14. Further reading
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this address.