Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Implement the subcommand pattern in PowerShell

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?

like image 581
craig Avatar asked Jun 11 '15 22:06

craig


People also ask

What does %% mean in PowerShell?

% is an alias for the ForEach-Object cmdlet.

What is IEX command in PowerShell?

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.


1 Answers

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.


splat.ps1 (as such app.ps1) - pattern with splatting

Pros:

  • Minimum code and overhead.
  • Positional parameters work.
  • -? works for help as it is (short help).

Cons:

  • PowerShell v3+, splatting works funny in v2.

dynamic.ps1 (as such app.ps1) - pattern with dynamic parameters

Pros:

  • PowerShell v2+.
  • TabExpansion works for parameters.

Cons:

  • More code, more runtime work.
  • Only named parameters.
  • Help as -help.

Scripts

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
}
like image 142
Roman Kuzmin Avatar answered Oct 05 '22 23:10

Roman Kuzmin