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?
% is an alias for the ForEach-Object cmdlet.
iex is an alias for Invoke-Expression . Here the two backticks don't make any difference, but just obfuscates the command a little. iex executes a string as an expression, even from pipe. Here Start-Process is a cmdlet that starts processes.
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
#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
param(
[Parameter()]$Command,
[switch]$Help
)
dynamicparam {
${private:*pn} = 'Verbose', 'Debug', 'ErrorAction', 'WarningAction', 'ErrorVariable', 'WarningVariable', 'OutVariable', 'OutBuffer', 'PipelineVariable'
$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
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With