SHell Interface Framework Utility, shifu, is a declarative framework that makes creating powerful CLIs from shell scripts simple. Shifu provides:
- argument parsing
- subcommand dispatch
- help string formatting
- tab completion for interactive shells
- compatibility with POSIX-based shells; tested with:
- ash, bash, dash, ksh, zsh
all in a single POSIX shell file with no dependencies.
Shell scripts are great for gluing terminal programs together. But adding subcommands, scoped options, help strings, and tab completion means a lot of boilerplate that's hard to understand and maintain. Shifu offers an API to wire up your CLI succinctly, letting you focus on real functionality.
Since shifu is just a single POSIX-compatible script, all you need to do is get a copy of it and either put it in a location on your PATH or in the same directory as your CLI script.
curl -O https://raw.githubusercontent.com/Ultramann/shifu/main/shifuThe core building block in shifu is a command. A command is a function, by convention ending in _cmd, that only contains calls to shifu cmd functions. Together, these functions form a DSL that shifu uses to build your CLI. Commands are passed to shifu's command runner, shifu_run, or referenced as subcommands.
Below is a very minimal, introductory shifu CLI script.
. "${0%/*}"/shifu || exit 1
intro_cmd() {
shifu_cmd_name intro
shifu_cmd_func intro_function
shifu_cmd_help "An introduction shifu cli"
shifu_cmd_long "This command function will invoke intro_function which prints
an option value provided by '-o' or '--option', defaults to none"
shifu_cmd_optd -o --option -- OPTION none "Example option to echo"
}
intro_function() {
echo "$OPTION"
}
shifu_run intro_cmd "$@"Calling this CLI, we can see how shifu_run parses -o shifu into the variable OPTION and calls intro_function; and also automatically generates help strings.
$ examples/intro
none
$ examples/intro -o shifu
shifu
$ examples/intro --help
An introduction shifu cli
This command function will invoke intro_function which prints
an option value provided by '-o' or '--option', defaults to none
Options
-o, --option [OPTION]
Example option to echo
Default: none
-h, --help
Show this helpThe diagram below shows how shifu connects this CLI script to parse the command line arguments and print the value shifu in intro_function.
examples/intro -o shifu ───────────────┐
▲ ▲ │
│ └────────────────┐ │
└──────────┐ │ │
intro_cmd() { │ │ │
shifu_cmd_name intro ──┘ │ │
┌──── shifu_cmd_func intro_function │ │
│ shifu_cmd_optd -o --option -- \ ──┘ │
│ OPTION none "Example option to echo" │
│ } ▲ │
│ └───────────────────────────────────┘
│
└─► intro_function() {
echo "$OPTION"
}
This example only demonstrates how to parse one option with a default value, but shifu supports several option and argument types: binary options, options with defaults, required options, positional arguments, and remaining arguments. See the Option and argument functions API section for details.
Shifu supports subcommands for grouping related functionality. Use shifu_cmd_subs instead of shifu_cmd_func to reference subcommand, _cmd, functions by name. When called, shifu_run recursively matches command line arguments against the names declared with shifu_cmd_name in each subcommand. Once a command is found using shifu_cmd_func, shifu_run calls the function by name as shown in the quickstart.
Here's what the minimal structure of a subcommand CLI looks like, with help strings omitted to highlight the subcommand and function references.
root_cmd() {
shifu_cmd_name root
shifu_cmd_subs sub_cmd # -┐
} # │
# │
sub_cmd() { # <──────────────┘
shifu_cmd_name sub
shifu_cmd_func sub_func # -┐
} # │
# │
sub_func() { # <──────────────┘
echo "Hello from sub_func"
}
shifu_run root_cmd "$@"If this script were saved as root and called with root sub, shifu_run would match sub against the name declared in sub_cmd and dispatch to sub_func.
$ root sub
Hello from sub_funcArguments and help strings are scoped to each subcommand. Parent commands can also declare shared options once instead of repeating them in each subcommand, and control when those options are parsed. See the Option and argument functions notes API section for details.
Below is a demo of examples/dispatch, a CLI with two subcommands, hello and echo, each with their own arguments. Annotated source code of the CLI can be found in the expandable section below the demo.
Source code and walkthrough
Note, this example calls shifu_less after sourcing shifu to provide a version of the shifu_cmd functions without the shifu_ prefixes.
#! /bin/sh
# Source, "import", shifu
. "${0%/*}"/shifu && shifu_less || exit 1
# Write root command
dispatch_cmd() {
# Name the command
cmd_name dispatch
# Add subcommands
cmd_subs hello_cmd echo_cmd
# Add help for the command
cmd_help "A dispatch shifu example"
# Add long help for the command
cmd_long "An example shifu cli demonstrating
* subcommand dispatch
* argument parsing
* scoped help generation"
# Add deferred binary option, inherited by subcommands
cmd_optb :defer: -D --deferred -- DEFERRED false true "Deferred binary option"
}
# Write first subcommand, referenced in `cmd_subs` above
hello_cmd() {
cmd_name hello
# Add target function
cmd_func dispatch_hello
cmd_help "A hello world subcommand"
cmd_long "A subcommand that prints greeting with arguments"
# Add option, will populate variable `NAME` when parsing cli args
# NAME defaults to 'mysterious user' if -n/--name aren't provided
cmd_optd -n --name -- NAME "mysterious user" "Name to greet"
}
# Write first subcommand target function
dispatch_hello() {
[ "$DEFERRED" = true ] && message="☝ " || message=""
echo "${message}Hello, $NAME!"
}
# Write second subcommand, referenced in `cmd_subs` above
echo_cmd() {
cmd_name echo
cmd_func dispatch_echo
cmd_help "An echo subcommand"
cmd_long "A subcommand that prints results of parsed arguments"
# Add options and positional argument
cmd_optr -r --required -- REQUIRED "Example required option w/ argument"
cmd_optd -d --default -- DEFAULT "default" "Example option w/ argument"
cmd_argr POSITIONAL "Example positional argument"
}
# Write second subcommand target function
dispatch_echo() {
# Use variables populated by option/argument functions
echo "Deferred binary option: $DEFERRED"
echo "Required option: $REQUIRED"
echo "Option w/ default: $DEFAULT"
echo "Positional argument: $POSITIONAL"
}
# Run root command passing all script arguments
shifu_run dispatch_cmd "$@"The diagram below shows how shifu is connecting together this CLI script to print the value ☝ Hello, World! in dispatch_hello.
┌───────────── sets to ─────────────┐
│ ┌──────────── true ──────────────┐│
│ │ ▼│
│ │ examples/dispatch hello -D --name World ──────────────────────┐
│ │ ▲ ▲ ▲ │
│ │ │ │ └─────────────────────────────┐ │
│ │ │ └────────────────────────────────┐ │ │
│ │ dispatch_cmd() { │ ┌──► hello_cmd() { │ │ │
│ │ cmd_name dispatch ┘ │ cmd_name hello ───┘ │ │
│ │ cmd_subs hello_cmd echo_cmd ─────┘ ┌── cmd_func dispatch_hello │ │
│ └── cmd_optb :defer: -D --deferred \ ┌──┘ cmd_optd -n --name \ ──────┘ │
└────► -- DEFERRED false true \ │ ┌──► -- NAME "mysterious \ │
"Deferred binary option" │ │ user" "Name to greet" │
} │ │ } │
┌────────────────────────────────┘ └──────────────────────────────┘
│
└─► dispatch_hello() {
[ "$DEFERRED" = true ] && message="☝ " || message=""
echo "${message}Hello, $NAME!"
}
Since shifu knows all about the structure of your CLI, it can generate tab completion code for interactive shells that support it, bash and zsh.
By default, subcommand and option names can be tab completed. Shifu also provides cmd functions for completing option values and positional arguments with static enumerations, dynamic functions, or file system paths, see the Completion functions API section for details.
Below is a demo of examples/tab showing tab completion capabilities. Source code and instructions to run the example can be found in the expandable section below the demo.
Source code and running instructions
#! /bin/sh
. "${0%/*}"/shifu && shifu_less || exit 1
tab_cmd() {
cmd_name tab
cmd_help "A tab completion shifu example"
cmd_long "An example shifu cli demonstrating completions for
* subcommand names
* option names
* option values with
* enum completions
* function completions
* path completions"
cmd_subs completion_cmd demo_cmd
}
completion_cmd() {
cmd_name completion
cmd_help "Main command with example options and tab completion capabilities"
cmd_func no_op
cmd_optd -e --enum -- ENUM_COMP enum_comp "Enum completion"
cmd_cpte magic value
cmd_optd -f --func -- FUNC_COMP func_comp "Function completion, file extensions"
cmd_cptf file_extension_completions
cmd_argr PATH_COMP "Path completion"
cmd_cptp :files:
}
file_extension_completions() {
# dynamically complete with extensions from files in current directory
shifu_add_cpts "$(ls -1 | grep '\.' | sed 's/.*\.//' | sort -u)"
}
demo_cmd() {
cmd_name demo
cmd_help "No-op command to show multiple subcommand completion options"
cmd_func no_op
}
no_op() { :; }
shifu_run tab_cmd "$@"If you'd like to test the tab completion from this example you can easily do so from a bash or zsh (requires autoloading compinit) terminal by running
export PATH="$PATH:$(pwd)/examples"so your shell can find the example tab CLI, and
eval "$(examples/tab --tab-completion bash)"
# or, choose for your shell
eval "$(examples/tab --tab-completion zsh)"then tabbing along to the beat.
- Ensure your CLI is in a directory on your shell's
PATH - Ensure your CLI has access to shifu; either by putting shifu in the same
PATHdirectory as your CLI or adding shifu to anotherPATHdirectory - If you're using zsh, ensure that you've loaded and run
compinitbefore the following eval call in your zshrc file - Add the following line to your shell's rc file, replacing
<your-cli>with the name of your CLI and<shell>with a supported shell: bash or zsheval "$(<your-cli> --tab-completion <shell>)"
These instructions can also be found by running
<your-cli> --tab-completion help-
Why? This isn't what shell scripts are for.
-
Fair. However, sometimes a shell is all you want to require your users to have while still enabling a sophisticated CLI UX; shifu can help deal with the CLI boilerplate in those situations and let you focus on real functionality
-
Plus. Consider the following quote
If you only do what you can do, then you will never be better than what you are.
- Master Shifu, Kung Fu Panda
Shifu gives CLI shell scripts the opportunity to be better than they are
-
Finally. I want to use something like shifu, maybe others do too
-
-
What else is out there? So you vibe with the problem that shifu is solving but not with its implementation or limitations and want to know what alternatives are available. No worries, there are some great projects in this space that approach it very differently.
- argc: parses CLI specification from comments in script
- bashly: generates bash script from a YAML configuration
- getoptions: sophisticated POSIX shell option parser
argc bashly getoptions shifu Features Argument parsing ✓ ✓ ✓ ✓ Subcommand dispatch ✓ ✓ ✓ Tab completion ✓ ✓ ✓ Help generation ✓ ✓ ✓ ✓ Man page generation ✓ ✓ Input validation ✓ ✓ ✓ Approach Implementation rust ruby shell shell Framework form binary gem script script Target shell bash bash POSIX POSIX Framework required at runtime optional no optional yes -
How does shifu name its variables/functions, will they collide with those in my script?
- Shifu takes special care to prefix all variables/functions with
shifuor_shifu - Calling
shifu_lessafter sourcing shifu will create versions of all thecmdfunctions without theshifuprefix. This makes command code less busy, but adds function names that are more likely to cause a collision with those in your script
- Shifu takes special care to prefix all variables/functions with
-
What's with the
. "${0%/*}"/shifu || exit 1?.is the POSIX source command; it executes a file in the current shell, making shifu's functions available to your script, akin to importing"${0%/*}"is parameter expansion that strips the filename from$0(the script path), leaving just the directory. This lets your script find shifu relative to itself rather than relying onPATH|| exit 1exits the script if sourcing fails (e.g., shifu not found), preventing cryptic errors later- If shifu is on your
PATH, you can simply use. shifu || exit 1
- Command runner
- Command definition functions
- Option and argument functions
- Completion functions
- Configuration
- Miscellaneous
- Called at the end of a CLI script
- Takes the name of a command function, those ones that end in
_cmdby convention, and all script arguments,"$@" - Dispatches call by parsing arguments in
"$@"based on information in command function - Parses arguments that match subcommand names until the subcommand specifies a function to call with
shifu_cmd_func - Parses all unparsed arguments into variables declared in option and argument function calls
- Calls the function in
shifu_cmd_funcpassing any still unparsed arguments - Example
shifu_run root_cmd "$@"
- Name used to reference command from command line arguments
- When the command is passed to
shifu_runthis name must match the name of the program for tab completion to work - When the command is a subcommand the name is used to parse command line arguments
- Example
shifu_cmd_name shifu
- Subcommand function names to which the current command can route
- Example
shifu_cmd_subs subcommand_one_cmd subcommand_two_cmd
- Name of function to run when command is invoked
- The function will be passed all command line arguments that weren't parsed while identifying the command
- Example
shifu_cmd_func function_to_run
- Brief help string for the command
- Added in help above the help for command arguments
- Added to help when listing a command's subcommands
- Example
shifu_cmd_help "Terse string to help users"
- Long help string for the command
- Added in help after brief help string
- Example
shifu_cmd_long "Verbose string to really help users"
There are five option and argument declaration functions:
| Type | Function | Parses |
|---|---|---|
| Option | shifu_cmd_optb |
Binary option |
| Option | shifu_cmd_optd |
Option with default |
| Option | shifu_cmd_optr |
Required option |
| Argument | shifu_cmd_argr |
Required positional argument |
| Argument | shifu_cmd_args |
Remaining arguments |
Option functions (shifu_cmd_optb, shifu_cmd_optd, shifu_cmd_optr) parse flagged arguments into variables. They take one or more flags (e.g. -v, --verbose) before a required -- separator, followed by parsing configuration. Argument functions (shifu_cmd_argr, shifu_cmd_args) parse positional arguments by order of declaration.
All option and argument functions accept a variable argument, the shell variable name that will be set when parsing, and a help string used in generated help output.
- Binary option
- Variable is assigned a value depending on whether or not the option flag is set
- Signature
shifu_cmd_optb <flags> -- <variable> <default> <set_value> <help>
- Example
shifu_cmd_optb -v --verbose -- VERBOSE false true "Verbose output"
cli # VERBOSE=false cli --verbose # VERBOSE=true
- Option with default
- Variable has a default value which can be overwritten with the option flag and a following argument
- Signature
shifu_cmd_optd <flags> -- <variable> <default> <help>
- Example
shifu_cmd_optd -o --output -- OUTPUT "out" "Output file"
cli # OUTPUT="out" cli --output result # OUTPUT="result"
- Required option
- Variable must be set with the option flag and a following argument, error if not provided
- Signature
shifu_cmd_optr <flags> -- <variable> <help>
- Example
shifu_cmd_optr -e --env -- ENV "Operating environment"cli # error: missing required option cli --env dev # ENV="dev"
- Required positional argument
- Variable is set from the next unparsed argument
- Signature
shifu_cmd_argr <variable> <help>
- Example
shifu_cmd_argr TARGET "Target to process"cli # error: missing required argument cli myfile.txt # TARGET="myfile.txt"
- Remaining arguments
- Zero or more unparsed arguments passed to the target function (specified by
shifu_cmd_func) via$@ - Signature
shifu_cmd_args <help>
- Example
shifu_cmd_args "Additional arguments"cli # $@ is empty cli one two three # $@ = one two three
The signatures and examples above are for leaf commands (those using shifu_cmd_func). When you have options that are shared across subcommands, like a --verbose flag, you can declare them once in a parent command (those using shifu_cmd_subs) instead of repeating them in every subcommand.
Option functions called in a parent command require a mode as the first argument. The mode changes when the option will be parsed, aka when it will be provided by the CLI user. The two available modes are:
:defer:- option parsing is deferred until the leaf command, so the option can be provided alongside subcommand optionsshifu_cmd_optb :defer: -v --verbose -- VERBOSE false true "Verbose output"
cli sub --verbose
:eager:- option parsing happens before subcommand dispatch, so the option must be provided before the subcommand nameshifu_cmd_optd :eager: -c --config -- CONFIG "default" "Config file"
cli --config myconfig sub
Positional and remaining argument functions (shifu_cmd_argr, shifu_cmd_args) can only be used in leaf commands.
The option and argument declaration order in a command function matters:
- Help is generated in declaration order
- Help strings from parent commands' deferred options are similarly deferred to the end of the generated help string
- Positional arguments are parsed in declaration order
- Options must be declared before any positional arguments, and positional arguments before remaining arguments
- Enumeration completion
- Static list of tab completions for the preceding option or argument
- Example
shifu_cmd_cpte debug info warn error
- Function completion
- Function to dynamically generate tab completions for the preceding option or argument
- The function should call
shifu_add_cptsto register completions - Example
shifu_cmd_cptf file_ext_completions file_ext_completions() { shifu_add_cpts "$(ls -1 | sed 's/.*\.//' | sort -u)" }
- Path completion
- Enable path completions for the preceding option or argument
- Takes a required mode argument:
:files:- complete files and directories:dirs:- complete directories only. Note: in zsh, after navigating into a directory with no subdirectories, the completion system falls back to showing files. This is standard zsh behavior and differs from bash, which strictly shows only directories.:glob: "pattern"- complete files matching a glob pattern
- Examples
shifu_cmd_cptp :files: shifu_cmd_cptp :dirs: shifu_cmd_cptp :glob: "*.txt"
Shifu has a few variables that can be set after sourcing to change default behavior. Typically they are set just before calling shifu_run.
- Controls whether arguments starting with a dash are treated as errors
- Type: bool
false: options (arguments starting with-) are not allowed after positional arguments, shifu will error if it encounters onetrue: allows positional and remaining arguments that begin with a dash. Useful if flags need to be passed through cli arguments
- Default:
false - Example
shifu_allow_options_anywhere=true
- Controls tab completion behavior when current word is a single
- - Type: bool
false, completing-only shows long options (--option-name)true, completing-shows both short (-o) and long (--option-name) options
- Default:
false - Example
shifu_complete_single_dash_options=true
- Space-separated list of flags that trigger help output, override to change which flags show help
- Type: string
- Default:
"-h --help" - Example
shifu_help_flags="--help"
- Registers one or more strings to add as completions
- Must only be called within functions passed to
shifu_cmd_cptf - Example
dynamic_completions() { shifu_add_cpts "$(func_to_get_completions)" }
- Creates shorthand aliases for all
shifu_cmd_*functions without theshifu_prefix (akacmd_nameinstead ofshifu_cmd_name) - Called after sourcing shifu, typically on the same line
- Makes command definitions less verbose, but the shorter names are more likely to collide with functions in your script
- Example
. "${0%/*}"/shifu && shifu_less || exit 1 cli_cmd() { cmd_name cli cmd_optd -o -- OPTION default "An option" cmd_func cli_func }

