Creating CLIs with `sub`

Some time ago, I wanted to add a custom CLI to my `~/.config/box` project, where I automated the configuration of various computers with a boatload of YAML on top of Ansible.

I can’t open source that project because it references clients and personal stuff Michel de Montagne rightfully identified as part of a separate sphere of life, but I can share the technique I used1 to set up this autocompleting, helpful CLI.

 box help
Usage: box <command> [<args>]

Some useful box commands are:
   attach                  Attach to a convenient tmux session
   clj-kondo-copy-configs  Copies configurations provided by all dependencies into the current project
   clj-upgrade             Upgrade Clojure dependencies
   colors                  Taste the rainbow of Terminal colours
   commands                List all box commands
   creds                   Dump public credentials for the provided `format`
   git-fetch               Fetch upstream changes in many Git repositories
   gpg-debug               Dumps relevant GPG and SSH configuration
   gpg-lookup              Look up the given email address bypassing any local cache
   gpg-tty                 Tell the GPG agent to update TTY
   https                   Pull down HTTPS certs
   imapnotify              Enables/disables user's imapnotify processes
   launch                  Fire up a local project, fast
   mfa                     Generate an MFA token for a Yubikey oath
   network                 Print useful network configuration and diagnostics
   nixup                   Upgrade Nix channels, packages and configuration
   pacup                   Update packages via both AUR and Pacman
   ping                    Checks reachability via Ansible defaulting to all hosts
   play                    Runs Ansible with named hosts
   play-all                Runs Ansible with ALL known hosts
   pull                    Quickly pull my repos
   pull-router-config      Copy config.boot from router to destination, defaulting to ./config.boot
   recompose               Reloads ~/.Xcompose with minimal fuss
   redoom                  Sync Doom and restart the Emacs server
   repo-list               Lists directories hosted on Files.
   sound                   Switches Pulseaudio inputs and outputs
   status                  Report on things we like to change regularly
   switch                  Rebuilds the system via nix-darwin and home-manager
   sync-iphone             Sync images on USB-connected iPhone to ~/ios
   truecolors              Taste the rainbow of true colours
   upgrade                 Upgrade Emacs, ports, and kegs
   vlc-screen              Start VLC with the video from one monitor
   wake                    Sends a wake-on-lan packet to <host>
   which-emacs             Return the path to the preferred version Emacs
   yubisshkey              Print the public key associated with Yubikey
   zprof                   Profile startup time of ZSH

See 'box help <command>' for information on a specific command.

The sub project came out of the 37Signals team back when they created rbenvand it makes adding subcommands to a single entry point a breeze.

One starts by cloning the sub repo and running the provided prepare.sh script to set your own name. I wont reproduce all of the documentation in the project README here, but will provide an example from my own dotfiles of a self-documented command with nested autocompletion.

#!/usr/bin/env -S zsh --login
# Usage: box mfa <id>
# Summary: Generate an MFA token for a Yubikey oath

declare -A accounts=(
  [jcf-root]=arn:aws:iam::000000000000:mfa/jcf
  [jcf]=arn:aws:iam::000000000000:mfa/jcf
  [root]=arn:aws:iam::000000000000:mfa/root-account-mfa-device
  [vouch]=arn:aws:iam::000000000000:mfa/jcf
)

# Provide box completions
if [[ "$1" = "--complete" ]]; then
  print -l "${(@k)accounts}"
  ykman oath accounts list
  exit
fi

usage() {
  echo >&2 "box mfa <id>"
  echo >&2 ""
  echo >&2 "id:"
  ykman oath accounts list | sed 's/^/  /' >&2
}

clip() {
  if command -v xclip &>/dev/null; then
    xclip -selection clipboard -in "$@"
  elif command -v pbcopy &>/dev/null; then
    pbcopy
  fi
}

oath() {
  local pattern="$1"
  ykman oath accounts code -s "${accounts[$pattern]:-$pattern}" \
    | tee /dev/tty \
    | tr -d "\n" \
    | clip
}

if [[ $# -eq 1 ]]; then
  oath "$1"
else
  usage
fi

The script above wraps ykman and both prints and copies my MFA codes with support for both macOS and Linux’s clipboards.

The use of the Usage comment and the --complete arguments provide helpful output and autocompletion with very little effort.

Footnotes

  1. I’m in the process of retiring the Box project in favour of using Nix for everything I can.