How To Implement Subcommands Without using a CLI Command Package in 2 Steps
CLI is king!
Your IT infrastructure wants to be managed, and while UIs are fancy and convenient, the CLI rules when quick action is required. For ultimate control over your infrastructure, you envision a tool named srv
that provides numerous subcommands, like srv status
, srv start
, srv stop
, and so forth. Just like Git does it!
Of course, you want to write all these subcommands in Go. You know that there are several CLI command packages available that support subcommands, like Cobra, urfave/cli
, Kong, and many more.
But which one to choose?
“None” is an option
If you need to decide quickly, opt for none of them. Postpone this decision until you find some time to evaluate available CLI packages. You can still bundle all your tools as subcommands under a main command.
The trick? A simple shell function.
A simple Bash/Zsh/Powershell function can deliver a simple command/subcommand hierarchy in 2 steps.
Step 1: Create stand-alone commands
Start by writing all your sub-commands as stand-alone binaries. If we stick with the above example, you would name these binaries srv-status
, srv-start
, srv-stop
, and so on.
Step 2: Add a function to your shell config file
Here is a rather simple function that can be called as srv <command>
and invokes srv-command
. Sounds trivial? Maybe, but you get a few advantages from this approach:
- All commands have a common entrypoint. All users know that all server management commands are called as
srv <command>
. No need to keep a list of commands in one's head or on stickies. - All available subcommands are discoverable. If called as
srv
or assrv help
, the script prints out all available commands. - Everyone can easily contribute new commands. Adding a new binary with a
srv-
prefix to a directory listed in $PATH is sufficient to extend the list of subcommands forsrv
. The script automatically picks up the new command, no manual changes required.
So here is the script for several shell flavors.
For Bash, add this script to .bashrc
and restart the shell or start a new one:
srv() {
print_usage() {
echo "Usage: srv <subcommand>"
echo "Available subcommands:"
printf ' %s\n' $(compgen -c srv- | sed 's/^srv-//' | sort | uniq)
echo " help"
}
if [ $# -eq 0 ] || [ "$1" = "help" ]; then
print_usage
return 0
fi
command srv-"$@"
}
Zsh users can re-use this script after swapping out the printf
line for:
printf ' %s\n' ${(f)"$(print -l ${(ok)commands[(I)srv-*]} | sed 's/^srv-//')"}
The reason for this change is that compgen
is a Bash-internal function. The Zsh script uses Zsh's associative commands
array instead for finding all srv
subcommands.
Fish syntax is different. Create ~/.config/fish/functions/srv.fish
and insert this script:
function srv
function print_usage
echo "Usage: srv <subcommand>"
echo "Available subcommands:"
for cmd in (string replace -r '^.*/srv-' '' (command -s srv-*))
echo " $cmd"
end
echo " help"
end
if test (count $argv) -eq 0; or test "$argv[1]" = "help"
print_usage
return 0
end
command srv-$argv[1] $argv[2..-1]
end
PowerShell users can put the following script into $PROFILE
:
function srv {
function Print-Usage {
Write-Host "Usage: srv <subcommand>"
Write-Host "Available subcommands:"
Get-Command srv-* | ForEach-Object { $_.Name -replace '^srv-' } | Sort-Object -Unique | ForEach-Object { Write-Host " $_" }
Write-Host " help"
}
if ($args.Count -eq 0 -or $args[0] -eq "help") {
Print-Usage
return
}
$command = "srv-$($args -join ' ')"
Invoke-Expression $command
}
Finito
With almost no effort, you now can create and use subcommands. Calling srv start --now server1 server2
translates directly to srv-start --now server1 server2
, while srv
and srv help
list all available commands.
Feel free to play around and extend the script. For example, if srv
is called without a subcommand, make it invoke a default command instead of listing subcommands. Or improve the help
command to turn the call to srv help <subcommand>
into srv-<subcommand> --help
.