Is it possible to implement the subcommand pattern in PowerShell? Something like:
command [subcommand] [options] [files]
Examples: Git, svn, Homebrew
What would be the general architecture? A single function that delegates the actual work to script blocks? Each subcommand isolated in its own PS1
file that is dot-sources by the primary script? Would PowerShell's various meta-data functions (e.g. Get-Command
) be able to 'inspect' the subcommands?
I thought of this pattern and found two ways of doing this. I did not find real applications in my practice, so the research is rather academic. But the scripts below work fine.
An existing tool which implements this pattern (in its own way) is scoop.
The pattern subcommand implements the classic command line interface
app <command> [parameters]
This pattern introduces a single script app.ps1
which provides commands
instead of providing multiple scripts or functions in a script library or
module. Each command is a script in the special subdirectory, e.g. ./Command.
Get available commands
app
Invoke a command
app c1 [parameters of Command\c1.ps1]
Get command help
app c1 -? # works with splatting approach
app c1 -help # works with dynamic parameters
The script app.ps1 may contain common functions used by commands.
Pros:
-?
works for help as it is (short help).Cons:
Pros:
Cons:
-help
.splat.ps1
UPDATE: Used $_Command
instead of $Command
to avoid conflicts with partial parameter names like -c
, see comments.
#requires -Version 3
param(
$_Command
)
if (!$_Command) {
foreach($_ in Get-ChildItem $PSScriptRoot\Command -Name) {
[System.IO.Path]::GetFileNameWithoutExtension($_)
}
return
}
& "$PSScriptRoot\Command\$_Command.ps1" @args
dynamic.ps1
UPDATE: Added more known common 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
}