Scripts with superpowers.
sub
is a tool for organizing scripts into a unified command-line interface.
It allows the dynamic creation of a CLI from a directory (and subdirectories)
of scripts with support for documentation, argument validation, and
completions.
- Key features
- Demo
- Installation
- Setup
- Usage
- Documenting commands
- Validating arguments
- Parsing arguments
- Completions
- Nested subcommands
- Aliases
- Sharing code between scripts
- Caching
- Migrating to Sub 2.x
- Inspiration
- Display help: Display usage and documentation for scripts.
- Validate arguments: Validate arguments to scripts based on documentation.
- Parse arguments: Automatically parse arguments to scripts so
getopts
is not needed. - Nested subcommands: Supports nested directories for hierarchical command structures.
- Aliases: Supports aliases for subcommands.
- Completions: Supports auto completion of subcommands.
- Cross-platform: Works on Linux and macOS.
Homebrew
brew install juanibiapina/tap/sub
Nix with Flakes (install the `sub` binary)
Add sub to your flake inputs:
{
inputs = {
sub = {
url = "github:juanibiapina/sub";
inputs.nixpkgs.follows = "nixpkgs"; # Optional
};
};
# ...
}
Then add it to your packages:
{
environment.systemPackages = with pkgs; [
inputs.sub.packages."${pkgs.system}".sub
# ...
];
}
Nix with Flakes (including setup)
This repository is a flake that exports a function
lib.${system}.mkSubDerivation
. This function creates a package for your cli
that uses sub
as the entry point and already includes the Setup.
For an example on how to use it, check out https://github.com/ggazzi/dev-cli-utils
Thanks @ggazzi for writing this module.
This section explains how to set up a CLI called hat
using sub
.
For a simple example, check out a hello world project.
For a complete example with lots of features, check out the complete example project.
The quickest way to get started with sub
is to define an alias for your CLI
in your shell:
alias hat='sub --name hat --absolute /path/to/cli/root --'
Where /path/to/cli/root
contains a libexec
directory with executable
scripts, for example:
.
βββ libexec
βββ user-script1
βββ user-script2
Β Β βββ user-script3
A more reliable way is to use an executable script as the entry point. Given the following directory structure:
.
βββ bin
βΒ Β βββ hat
βββ libexec
βββ user-script1
βββ user-script2
Β Β βββ user-script3
The entry point in bin/hat
is then:
#!/usr/bin/env bash
sub --name hat --executable "${BASH_SOURCE[0]}" --relative ".." -- "$@"
The --executable
argument tells sub
where the CLI entry point is located.
This will almost always be ${BASH_SOURCE[0]}
. The --relative
argument tells
sub
how to find the root of the CLI starting from the CLI entry point. In the
line above, just replace hat
with the name of your CLI.
Once you have set up your CLI (we called it hat
), you can get help by running:
$ hat --help
Usage: hat [OPTIONS] [commands_with_args]...
Arguments:
[commands_with_args]...
Options:
--usage Print usage
-h, --help Print help
--completions Print completions
--commands Print subcommands
--extension <extension> Filter subcommands by extension
Available subcommands:
user-script1
user-script2
user-script3
To invoke a subcommand, use:
$ hat user-script1
To get help for a command, use the built in --help
flag:
hat --help <commandname>
or
hat <commandname> --help
In order to display help information, sub
looks for special comments in the
corresponding script. A fully documented hello
script could look like this:
#!/usr/bin/env bash
#
# Summary: Say hello
#
# Usage: {cmd} <name> [--spanish]
#
# Say hello to a user by name.
#
# With the --spanish flag, the greeting will be in Spanish.
set -e
declare -A args="($_HAT_ARGS)"
if [[ "${args[spanish]}" == "true" ]]; then
echo "Β‘Hola, ${args[name]}!"
else
echo "Hello, ${args[name]}!"
fi
sub
looks for special comments in a comment block in the beginning of the
file. The special comments are:
Summary:
A short description of the script.Usage:
A description of the arguments the script accepts. Note that the Usage comment, when present, has specific syntactic rules and is used to parse command line arguments. See Validating arguments and Parsing arguments for more information.Options:
A description of the options the script accepts. This is used to display help information and generate completions. See Completions for more details.- Extended documentation: Any other comment lines in this initial block will be considered part of the extended documentation.
sub
automatically validates arguments to scripts based on the Usage
comment when it is present. The syntax for the Usage
comment is:
# Usage: {cmd} <positional> [optional] [-u] [--long] [--value=VALUE] [--exclusive]! [rest]...
{cmd}
: This special token represents the name of the command and is always required.<positional>
: A required positional argument.[optional]
: An optional positional argument.[-u]
: An optional short flag.[--long]
: An optional long flag.[--value=VALUE]
: An optional long flag that takes a value.[--exclusive]!
: An optional long flag that cannot be used with other flags.[rest]...
: A rest argument that consumes all remaining arguments.
Short and long flags can also be made required by omitting the brackets.
When invoking a script with invalid arguments, sub
will display an error. For
example, invoking the hello
script from the previous section with invalid
arguments:
$ hat hello
error: the following required arguments were not provided:
<name>
Usage: hat hello --spanish <name>
For more information, try '--help'.
When arguments to a script are valid, sub
sets an environment variable called
_HAT_ARGS
(where HAT
is the capitalized name of your CLI). This variable
holds the parsed arguments as a list of key value pairs. The value of this
variable is a string that can be evaluated to an associative array in bash
scripts:
declare -A args="($_HAT_ARGS)"
Which can then be used to access argument values:
echo "${args[positional]}"
if [[ "${args[long]}" == "true" ]]; then
# ...
fi
sub automatically provides completions for subcommand names.
To enable completions for positional arguments in the Usage
comment, add an
Options:
comment with a list of arguments. An option must have the format:
name (completion_type): description
. Completion type is optional.
The following completion types are supported:
`command`
: Runs a command to generate completions. The command should print completions to stdout:
# Usage: {cmd} <file>
# Options:
# file (`ls -1`): File or directory
# script logic
# ...
script
: Invokes the current script to generate completions. This allows for more complex completions:
# Usage: {cmd} <name>
# Options:
# name (script): A name
# check if we're being requested completions
if [[ "$_HAT_COMPLETE" == "true" ]]; then
if [[ "$_HAT_COMPLETE_ARG" == "name" ]]; then
echo "Alice"
echo "Bob"
echo "Charlie"
# note that you can have any logic here to generate completions
fi
# make sure to exit when generating completions to prevent the script from running
exit 0
fi
# read arguments
declare -A args="($_HAT_ARGS)"
# say hello
echo "Hello, ${args[name]}!"
sub
supports nested directories for hierarchical command structures. For
example, given the following directory structure:
.
βββ libexec
βββ nested
βββ README
βββ user-script2
user-script2
can be invoked with:
$ hat nested user-script2
Directories can be nested arbitrarily deep.
A README
file can be placed in a directory to provide a description of the
subcommands in that directory. The README
file should be formatted like a
script, with a special comment block at the beginning:
# Summary: A collection of user scripts
#
# This directory contains scripts that do magic.
# This help can be as long as you want.
# The Usage comment is ignored in README files.
To define an alias, simply create a symlink. For example, in the libexec
directory:
ln -s user-script1 us1
Aliases can also point to scripts in subdirectories:
ln -s nested/user-script2 us2
The full power of symlinks can be used to create complex command structures.
When invoking subcommands, sub
sets an environment variable called
_HAT_ROOT
(where HAT
is the capitalized name of your CLI. This variable
holds the path to the root of your CLI. It can be used, for instance, for
sourcing shared scripts from a lib
directory next to libexec
:
source "$_CLINAME_ROOT/lib/shared.sh"
When invoking subcommands, sub
sets an environment variable called
_HAT_CACHE
(where HAT
is the capitalized name of your CLI. This variable
points to an XDG compliant cache directory that can be used for storing
temporary files shared between subcommands.
The --bin
argument was renamed to --executable
to better reflect its purpose.
Sub 2.x introduces automatic validation and parsing of command line arguments
based on special Usage comments in scripts. If you previously used arbitrary
Usage comments in sub 1.x for the purpose of documenting, you can run sub
with the --validate
flag to check if your scripts are compatible with the new
version.
Example:
$ sub --name hat --absolute /path/to/cli/root -- --validate
If you used the help
, commands
or completions
subcommands, they are now
--help
, --commands
and --completions
flags respectively.