In this blog post, we'll discuss how to implement the subcommand pattern in PowerShell. This pattern allows you to create a single script that provides multiple commands, which can be invoked by specifying the command name as an argument to the script. We'll explore two different approaches to implementing this pattern, one using splatting and the other using dynamic parameters.
The subcommand pattern
The subcommand pattern is a classic command line interface (CLI) pattern that allows you to invoke multiple commands from a single script. This is achieved by specifying the command name as an argument to the script, followed by any additional arguments that the command requires.
app <command> [parameters]
In PowerShell, we can implement this pattern by creating a single script that serves as the entry point for the application. This script will then load and execute the appropriate command based on the command name specified as an argument.
Two Approaches
There are two main approaches to implementing the subcommand pattern in PowerShell:
- Using splatting
- Using dynamic parameters
In this post, we'll explore both approaches and provide code examples for each.
Splatting Approach
The splatting approach is a simple and straightforward way to implement the subcommand pattern. It involves using the splatting operator (@) to pass all of the arguments specified on the command line to the command script.
Here's an example of how to implement the subcommand pattern using splatting:
#requires -Version 3
param(
$_Command
)
if (!$_Command) {
foreach($_ in Get-ChildItem $PSScriptRoot\Command -Name) {
[System.IO.Path]::GetFileNameWithoutExtension($_)
}
return
}
& "$PSScriptRoot\Command\$_Command.ps1" @args
In this example, the splat.ps1 script is the entry point for the application. It accepts a single parameter, $_Command, which specifies the name of the command to be executed. If no command name is specified, the script will display a list of available commands.
When a command name is specified, the script uses the splatting operator (@) to pass all of the arguments specified on the command line to the command script. This allows the command script to access the arguments as if they had been passed directly to it.
Dynamic Parameters Approach
The dynamic parameters approach is a more flexible and powerful way to implement the subcommand pattern. It involves using dynamic parameters to define the parameters that the command script accepts. This allows you to specify the parameters that each command requires, and to provide help and validation for each parameter.
Here's an example of how to implement the subcommand pattern using dynamic parameters:
param(
[Parameter()]$Command,
[switch]$Help
)
dynamicparam {
${private:*pn} = 'Verbose', 'Debug', 'ErrorAction', 'WarningAction', 'ErrorVariable', 'WarningVariable', 'OutVariable', 'OutBuffer', 'PipelineVariable', 'InformationAction', 'InformationVariable', 'ProgressAction'
$PSScriptRoot = Split-Path $MyInvocation.MyCommand.Definition
$Command = $PSBoundParameters['Command']
if (!$Command) {return}
$_ = Get-Command -Name "$PSScriptRoot\Command\$Command.ps1" -CommandType ExternalScript -ErrorAction 1
if (!($_ = $_.Parameters) -or !$_.Count) {return}
${private:*r} = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
(${private:*a} = New-Object System.Collections.ObjectModel.Collection[Attribute]).Add((New-Object System.Management.Automation.ParameterAttribute))
foreach($_ in $_.Values) {
if (${*pn} -notcontains $_.Name) {
${*r}.Add($_.Name, (New-Object System.Management.Automation.RuntimeDefinedParameter $_.Name, $_.ParameterType, ${*a}))
}
}
${*r}
}
end {
if (!$Command) {
foreach($_ in Get-ChildItem $PSScriptRoot\Command -Name) {
[System.IO.Path]::GetFileNameWithoutExtension($_)
}
return
}
if ($Help) {
Get-Help "$PSScriptRoot\Command\$Command.ps1" -Full
return
}
$null = $PSBoundParameters.Remove('Command')
& "$PSScriptRoot\Command\$Command.ps1" @PSBoundParameters
}
In this example, the dynamic.ps1 script is the entry point for the application. It accepts two parameters:
- Command: The name of the command to be executed.
- Help: A switch parameter that specifies whether to display help for the specified command.
The dynamicparam block defines the dynamic parameters that the command script accepts. These parameters are based on the parameters that are defined in the command script itself. This allows the script to provide help and validation for each parameter.
When a command name is specified, the script uses the PSBoundParameters variable to access the parameters that were specified on the command line. The script then removes the Command parameter from the PSBoundParameters variable and passes the remaining parameters to the command script.