Helping PowerShell users run C# console applications
Posted: (EET/GMT+2)
C# console applications are great for small internal tools, but they do not automatically feel native in PowerShell. One such small annoyance is the lack of parameter completion.
As you are surely aware, PowerShell can complete cmdlet parameter names for PowerShell scripts and functions. Type a dash, enter the first few letters of a parameter name, press Tab, and PowerShell can complete the rest. For instance, if you type:
.\Invoke-MyTool.ps1 -Inp
...after pressing Tab, it might become:
.\Invoke-MyTool.ps1 -InputFile
With a plain C# executable, PowerShell usually does not know which parameters the application supports, as there's no metadata (hint, Microsoft). So, if you had a command like the following and attempted completion:
.\MyTool.exe --inp
...your executable may support a parameter named --input-file, but PowerShell does not automatically know
that.
A simple solution is to add a small PowerShell wrapper script around the C# application. The C# tool can still use normal command-line arguments, such as:
MyTool.exe --input-file input.json --output-folder C:\Temp --overwrite
The PowerShell wrapper exposes friendly PowerShell parameters:
.\Invoke-MyTool.ps1 -InputFile .\input.json -OutputFolder C:\Temp -Overwrite
Here is a minimal wrapper example script:
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string] $InputFile,
[Parameter(Mandatory)]
[string] $OutputFolder,
[switch] $Overwrite
)
$exePath = Join-Path $PSScriptRoot "MyTool.exe"
$arguments =
@(
"--input-file", $InputFile,
"--output-folder", $OutputFolder
)
if ($Overwrite) {
$arguments += "--overwrite"
}
& $exePath @arguments
exit $LASTEXITCODE
The param block is the important part for the user experience. It tells PowerShell which parameters the script supports. Because of that, users get parameter name completion:
.\Invoke-MyTool.ps1 -Out
After pressing Tab:
.\Invoke-MyTool.ps1 -OutputFolder
The wrapper can also make the tool feel more natural for PowerShell users. Use PowerShell style parameter names in the script, and translate them to the argument names expected by the C# executable. For example:
-InputFilebecomes--input-file-OutputFolderbecomes--output-folder-Overwritebecomes--overwrite.
This keeps the C# tool clean while giving PowerShell users a better command-line experience.
Another nice benefit is validation.
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[ValidateScript({ Test-Path $_ -PathType Leaf })]
[string] $InputFile,
[Parameter(Mandatory)]
[string] $OutputFolder,
[switch] $Overwrite
)
Now PowerShell can catch a missing input file before the C# application even starts. The wrapper can also provide defaults:
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string] $InputFile,
[string] $OutputFolder = ".\out",
[switch] $Overwrite
)
Tip: keep the wrapper thin. It should help users call the application, not reimplement the application logic.
Good things to put in the wrapper:
- PowerShell-friendly parameter names
- basic validation
- reasonable defaults
- argument translation
- correct exit code forwarding.
Good things to keep in the C# application:
- business logic
- file processing
- API calls
- real error handling
- the command-line contract used by automation.
For more advanced scenarios, PowerShell also supports custom argument completers. That can provide completion directly for native executables. For many internal tools, however, a small wrapper script is easier to read, ship, and maintain.
This is a small detail, but small details matter in command-line tools. If the tool is easier to run, people are more likely to use it correctly.