Skip to content

Shell scripts with superpowers πŸ¦Έβ€β™‚οΈ

License

Notifications You must be signed in to change notification settings

juanibiapina/sub

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Sub

GitHub top language GitHub branch check runs GitHub Release GitHub License

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.

Table of Contents

Key features

  • 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.

Demo

asciicast

Installation

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.

Setup

This section explains how to set up a CLI called hat using sub.

Examples

For a simple example, check out a hello world project.

For a complete example with lots of features, check out the complete example project.

As an alias

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

As an executable

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.

Usage

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

Documenting commands

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.

Validating arguments

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'.

Parsing arguments

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

Completions

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]}!"

Nested subcommands

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.

Aliases

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.

Sharing code between scripts

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"

Caching

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.

Migrating to Sub 2.x

change --bin to --executable

The --bin argument was renamed to --executable to better reflect its purpose.

Usage comments

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

Help, commands and completions

If you used the help, commands or completions subcommands, they are now --help, --commands and --completions flags respectively.

Inspiration