Parse arguments in scripts
A script can have --help, parsed arguments, and tab completion without a line of parsing code in it. The spec lives in comments at the top of the file, and a usage shebang runs the script through the parser first. Each flag and argument reaches the script as an environment variable named usage_<name>.
Enabling autocompletion
Tab completion for shebang scripts is one line of setup: source <(usage g completion-init bash) in ~/.bashrc enables <Tab> on every usage-shebang script on $PATH. See Shell completions for zsh and fish.
Bash
Save this example as mycli:
#!/usr/bin/env -S usage bash
#USAGE flag "-f --force" help="Overwrite existing <file>"
#USAGE flag "-u --user <user>" help="User to run as"
#USAGE arg "<file>" help="The file to write" default="file.txt"
if [ "$usage_force" = "true" ]; then
rm -f "$usage_file"
fi
if [ -n "$usage_user" ]; then
echo "Hello, $usage_user" >> "$usage_file"
else
echo "Hello, world" >> "$usage_file"
fiSave the script as mycli, then make it executable:
chmod +x mycliWith usage on PATH, run it:
$ ./mycli --help
Usage: mycli [-f --force] [-u --user <user>] [file]
Arguments:
[file] The file to write
(default: file.txt)
Flags:
-f, --force Overwrite existing <file>
-u, --user <user> User to run as
-h, --help Print help
$ ./mycli -f --user=alice output.txt
$ cat output.txt
Hello, aliceThe synopsis, the two sections, and -h/--help itself are all built from those three comment lines. Nothing in the script prints them.
Other interpreters
A language without a dedicated command goes through usage exec, which names the interpreter to run. The comment prefix follows the language, so JavaScript uses //USAGE. Save this CommonJS example as mycli.cjs:
#!/usr/bin/env -S usage exec node
//USAGE flag "-f --force" help="Overwrite existing <file>"
//USAGE flag "-u --user <user>" help="User to run as"
//USAGE arg "<file>" help="The file to write" default="file.txt"
const fs = require("fs");
const { usage_user, usage_force, usage_file } = process.env;
if (usage_force === "true") {
fs.rmSync(usage_file, { force: true });
}
const user = usage_user ?? "world";
fs.appendFileSync(usage_file, `Hello, ${user}\n`);Interpreter arguments
Quote the interpreter and its arguments as one word when they must come before the script, as Deno's run and permission flags do. The quoted text is split like a shell would, and the script and its arguments follow:
#!/usr/bin/env -S usage exec "deno run --allow-env=usage_*"
//USAGE arg "<name>" help="Name to greet"
console.log(`Hello, ${Deno.env.get("usage_name")}!`);This runs deno run --allow-env=usage_* <script> <name>. A path to an existing file is never split, and neither is a name found on PATH, so an interpreter with spaces in its name still works.
Short flag chaining
Single-character flags can be bundled into one word, so -abc means -a -b -c:
#!/usr/bin/env -S usage bash
#USAGE flag "-a" help="Option A"
#USAGE flag "-b" help="Option B"
#USAGE flag "-c" help="Option C"
if [ "$usage_a" = "true" ]; then
echo "Option A is set"
fi
if [ "$usage_b" = "true" ]; then
echo "Option B is set"
fi
if [ "$usage_c" = "true" ]; then
echo "Option C is set"
fi$ ./mycli -abc
Option A is set
Option B is set
Option C is setSubcommands
The subcommand that was chosen arrives as usage_cmd: its canonical name, with the names of nested subcommands joined by spaces, so a script can dispatch on it without reading argv itself:
#!/usr/bin/env -S usage bash
#USAGE cmd "deploy" help="Deploy the app" {
#USAGE arg "<env>"
#USAGE }
#USAGE cmd "db" help="Manage the database" {
#USAGE cmd "migrate" help="Run migrations"
#USAGE cmd "seed" help="Load fixtures"
#USAGE }
case "$usage_cmd" in
deploy) echo "Deploying to $usage_env" ;;
"db migrate") echo "Migrating" ;;
"db seed") echo "Seeding" ;;
esac$ ./mycli deploy prod
Deploying to prod
$ ./mycli db migrate
MigratingAn alias reaches the script as the name it stands for, so case needs no arm for it. At the top level, with no subcommand chosen, usage_cmd is unset. It is unset even when the script was run from another usage script that had one, so a nested script never dispatches on its caller's subcommand.
A flag or argument that the spec itself names cmd, declared on the chosen subcommand or any command above it (clauses included), keeps usage_cmd for its own value, and the path is not exported. This holds even when that flag or argument is left out.
Shell escaping
var=#true
An environment variable holds one string, so a flag or argument declared var=#true arrives as its values joined with spaces. A value that itself contains a space is quoted, as shell_words::join() quotes it, so eval "set -- $usage_files" recovers the list in a POSIX shell. The joining is not configurable yet; issue 189 tracks alternatives.
Windows
usage bash ./mycli runs whatever bash Windows resolves to, and the executable search order there puts the system directory ahead of PATH. Installing WSL puts bash.exe in that directory, so on such a machine bash is the WSL launcher no matter what else is installed — and WSL cannot open a Windows path:
$ usage bash C:/work/mycli
/bin/bash: C:/work/mycli: No such file or directoryTwo ways out. Passing the script by a relative path works, because the launcher translates the working directory. Or name the shell you actually meant:
:: Command Prompt
set USAGECLI_SHELL_BASH=C:\Program Files\Git\bin\bash.exe# PowerShell
$env:USAGECLI_SHELL_BASH = 'C:\Program Files\Git\bin\bash.exe'Each shell subcommand reads the variable for the program it runs:
| Command | Variable |
|---|---|
usage bash | USAGECLI_SHELL_BASH |
usage zsh | USAGECLI_SHELL_ZSH |
usage fish | USAGECLI_SHELL_FISH |
usage powershell | USAGECLI_SHELL_PWSH |
usage powershell runs pwsh, so its variable is named for that — which also lets you point it at powershell.exe on a machine that only has Windows PowerShell.
The value is a program: an absolute path, or a name to look up on PATH. It is not a command line, so it takes no arguments and needs no quoting even where the path contains spaces. An empty or whitespace-only value reads the same as an unset one. The variable is inherited by the script, so a script that invokes usage again gets the same shell.
Why not USAGE_SHELL_BASH
That was the original spelling and is still read, so nothing that set it needs changing. The USAGECLI_ one exists because USAGE_ is not usage's to take: a spec's values reach a script as usage_<arg>, and Windows environment variable names are case-insensitive, so USAGE_SHELL_BASH and a spec's own shell_bash argument are one variable there. The same applies to USAGECLI_DEBUG, USAGECLI_TRACE and USAGECLI_LOG, whose old names collide with the very ordinary argument names debug, trace and log.
It also matters under mise, which clears usage_* from a task's environment so its own parsed arguments cannot leak in — comparing the first six characters, case-insensitively, which is why no USAGE_… spelling escapes it. A USAGE_SHELL_BASH set for mise run never reaches the task; a USAGECLI_SHELL_BASH does.
usage exec needs none of this — it already names the interpreter, so a shebang can point straight at one:
#!/usr/bin/env -S usage exec "C:/msys64/usr/bin/bash.exe"