An open API service indexing awesome lists of open source software.

https://github.com/juanibiapina/sub

Shell scripts with superpowers πŸ¦Έβ€β™‚οΈ
https://github.com/juanibiapina/sub

bash cli linux macos shell terminal zsh

Last synced: over 1 year ago
JSON representation

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

Awesome Lists containing this project

README

          

# Sub

![GitHub top language](https://img.shields.io/github/languages/top/juanibiapina/sub)
![GitHub branch check runs](https://img.shields.io/github/check-runs/juanibiapina/sub/master)
![GitHub Release](https://img.shields.io/github/v/release/juanibiapina/sub)
![GitHub License](https://img.shields.io/github/license/juanibiapina/sub)

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](#key-features)
* [Demo](#demo)
* [Installation](#installation)
* [Setup](#setup)
* [Examples](#examples)
* [As an alias](#as-an-alias)
* [As an executable](#as-an-executable)
* [Usage](#usage)
* [Documenting commands](#documenting-commands)
* [Validating arguments](#validating-arguments)
* [Parsing arguments](#parsing-arguments)
* [Completions](#completions)
* [Nested subcommands](#nested-subcommands)
* [Aliases](#aliases)
* [Sharing code between scripts](#sharing-code-between-scripts)
* [Caching](#caching)
* [Migrating to Sub 2.x](#migrating-to-sub-2x)
* [change --bin to --executable](#change---bin-to---executable)
* [Usage comments](#usage-comments)
* [Help, commands and completions](#help-commands-and-completions)
* [Inspiration](#inspiration)

## 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](https://asciinema.org/a/664235.svg)](https://asciinema.org/a/664235)

## Installation

Homebrew

```sh
brew install juanibiapina/tap/sub
```

Nix with Flakes (install the `sub` binary)

Add sub to your flake inputs:

```nix
{
inputs = {
sub = {
url = "github:juanibiapina/sub";
inputs.nixpkgs.follows = "nixpkgs"; # Optional
};
};

# ...
}
```

Then add it to your packages:

```nix
{
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](#setup).

For an example on how to use it, check out https://github.com/ggazzi/dev-cli-utils

Thanks [@ggazzi](https://github.com/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](https://github.com/juanibiapina/sub/tree/master/examples/hello) project.

For a complete example with lots of features, check out the
[complete](https://github.com/juanibiapina/sub/tree/master/examples/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:

```sh
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:

```sh
#!/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:

```sh
$ 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 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:

```sh
hat --help
```
or
```sh
hat --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:

```sh
#!/usr/bin/env bash
#
# Summary: Say hello
#
# Usage: {cmd} [--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](#validating-arguments)
and [Parsing arguments](#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](#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} [optional] [-u] [--long] [--value=VALUE] [--exclusive]! [rest]...
```

- `{cmd}`: This special token represents the name of the command and is always required.
- ``: 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:

```sh
$ hat hello
```

```
error: the following required arguments were not provided:

Usage: hat hello --spanish

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:

```sh
declare -A args="($_HAT_ARGS)"
```

Which can then be used to access argument values:

```sh
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:

```sh
# Usage: {cmd}
# Options:
# file (`ls -1`): File or directory

# script logic
# ...
```

- `script`: Invokes the current script to generate completions. This allows for
more complex completions:

```sh
# Usage: {cmd}
# 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:

```sh
$ 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:

```sh
# 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:

```sh
ln -s user-script1 us1
```

Aliases can also point to scripts in subdirectories:
```sh
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`:

```sh
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

- [sub from basecamp](https://github.com/basecamp/sub)
- [sd](https://github.com/cv/sd)