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:

  • -InputFile becomes --input-file
  • -OutputFolder becomes --output-folder
  • -Overwrite becomes --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.