Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 33 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@
* subcommand dispatch
* help string formatting
* tab completion for interactive shells
* a single, dependency-free, pure POSIX shell file
* 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 describe CLI structure, letting you focus on real functionality.

## Table of contents
Expand Down Expand Up @@ -167,7 +168,7 @@ dispatch_cmd() {
* argument parsing
* scoped help generation"
# Add deferred binary option, inherited by subcommands
cmd_optb :defer: -g --global -- GLOBAL false true "Global binary option"
cmd_optb :defer: -D --deferred -- DEFERRED false true "Deferred binary option"
}

# Write first subcommand, referenced in `cmd_subs` above
Expand All @@ -184,7 +185,7 @@ hello_cmd() {

# Write first subcommand target function
dispatch_hello() {
[ "$GLOBAL" = true ] && message="🌐 " || message=""
[ "$DEFERRED" = true ] && message=" " || message=""
echo "${message}Hello, $NAME!"
}

Expand All @@ -204,37 +205,37 @@ echo_cmd() {
# Write second subcommand target function
dispatch_echo() {
# Use variables populated by option/argument functions
echo "Global binary option: $GLOBAL"
echo "Required option: $REQUIRED"
echo "Option w/ default: $DEFAULT"
echo "Positional argument: $POSITIONAL"
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`.
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 -g --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: -g --global \ ┌───┘ cmd_optd -n --name \ ────┘ │
└────► -- GLOBAL false true \ │ ┌──► -- NAME "mysterious \ │
"Global binary option" │ user" "Name to greet" │
} │ │ }
┌──────────────────────────────┘ └────────────────────────────────┘
│ │ 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() {
[ "$GLOBAL" = true ] && message="🌐 " || message=""
[ "$DEFERRED" = true ] && message=" " || message=""
echo "${message}Hello, $NAME!"
}
```
Expand Down Expand Up @@ -563,7 +564,7 @@ The option and argument declaration order in a command function matters:
#### `shifu_cmd_cptf`
* Function completion
* Function to dynamically generate tab completions for the preceding option or argument
* The function should call `shifu_add_cpts` to register completions
* The function should call [`shifu_add_cpts`](#shifu_add_cpts) to register completions
* Example
```sh
shifu_cmd_cptf file_ext_completions
Expand Down Expand Up @@ -624,6 +625,16 @@ Shifu has a few variables that can be set after sourcing to change default behav

### Miscellaneous

#### `shifu_add_cpts`
* Registers one or more strings to add as completions
* Must only be called within functions passed to `shifu_cmd_cptf`
* Example
```sh
dynamic_completions() {
shifu_add_cpts "$(func_to_get_completions)"
}
```

#### `shifu_less`
* Creates shorthand aliases for all `shifu_cmd_*` functions without the `shifu_` prefix (aka `cmd_name` instead of `shifu_cmd_name`)
* Called after sourcing shifu, typically on the same line
Expand Down
Binary file modified assets/dispatch_demo.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions assets/dispatch_demo.tape
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ Sleep 200ms
Enter
Sleep 500ms

Type "examples/dispatch hello -g -n World"
Type "examples/dispatch hello -D -n World"
Sleep 200ms
Enter
Sleep 3000ms
Expand All @@ -49,7 +49,7 @@ Sleep 200ms
Enter
Sleep 500ms

Type "examples/dispatch echo -g --required 'provided' \"
Type "examples/dispatch echo -D --required 'provided' \"
Enter
Type "-d 'not default' example"
Sleep 200ms
Expand Down
12 changes: 6 additions & 6 deletions examples/dispatch
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ dispatch_cmd() {
* argument parsing
* scoped help generation"

cmd_optb :defer: -g --global -- GLOBAL false true "Global binary option"
cmd_optb :defer: -D --deferred -- DEFERRED false true "Deferred binary option"
}

hello_cmd() {
Expand All @@ -24,7 +24,7 @@ hello_cmd() {
}

dispatch_hello() {
[ "$GLOBAL" = true ] && message="🌐 " || message=""
[ "$DEFERRED" = true ] && message=" " || message=""
echo "${message}Hello, $NAME!"
}

Expand All @@ -40,10 +40,10 @@ echo_cmd() {
}

dispatch_echo() {
echo "Global binary option: $GLOBAL"
echo "Required option: $REQUIRED"
echo "Option w/ default: $DEFAULT"
echo "Positional argument: $POSITIONAL"
echo "Deferred binary option: $DEFERRED"
echo "Required option: $REQUIRED"
echo "Option w/ default: $DEFAULT"
echo "Positional argument: $POSITIONAL"
}

shifu_run dispatch_cmd "$@"
27 changes: 17 additions & 10 deletions shifu
Original file line number Diff line number Diff line change
Expand Up @@ -627,7 +627,9 @@ _shifu_complete() {
"$shifu_parent"
_shifu_set_for_looping shifu_cmds shifu_cmd_subs
while [ $# -ne 0 -a -n "${shifu_cmd_subs:-}" ]; do
_shifu_complete_func_args true "$shifu_parent" "$@"
_shifu_complete_func_args true "$shifu_parent" "$@" || _shifu_args_parsed=0
shift $_shifu_args_parsed
[ $# -eq 0 ] && break
shifu_build_comp_defer_option_names="${shifu_comp_option_names:-}"
shifu_mode=$shifu_mode_cmds_comp
shifu_arg_matched=''
Expand All @@ -647,14 +649,15 @@ _shifu_complete() {
done
shifu_comp_option_names="${shifu_build_comp_defer_option_names:-}"
_shifu_set_completing_option
[ "$shifu_completing_option" = true ] && shifu_completions=""
if [ "$shifu_completing_option" = true -a -n "${shifu_cmd_func:-}" ]; then
_shifu_complete_option_names
elif [ "$shifu_completing_option" = true -a -n "${shifu_cmd_subs:-}" ]; then
shifu_comp_option_names=""
_shifu_complete_option_names
elif [ "$shifu_completing_option" = false -a -n "${shifu_cmd_func:-}" ]; then
_shifu_complete_func_args false "$shifu_cmd" "$@"
elif [ "$shifu_completing_option" = false -a -n "${shifu_cmd_subs:-}" -a $# -eq 0 ]; then
elif [ "$shifu_completing_option" = false -a -n "${shifu_cmd_subs:-}" -a $# -eq 0 -a -z "${shifu_completions:-}" ]; then
_shifu_complete_subcmd_names
fi
[ -n "${shifu_completions:-}" ] && echo "$shifu_completions"
Expand Down Expand Up @@ -701,6 +704,7 @@ _shifu_complete_func_args() {
shifu_mode=$shifu_mode_args_comp
shifu_parse_stage=0
shifu_parse_eager="$1"; shifu_cmd="$2"; shift 2
_shifu_args_parsed=0
shifu_case_stmt="case \"\${1:-}\" in "
if [ "$shifu_parse_eager" = false -a -n "${shifu_defer_comp_case:-}" ]; then
shifu_case_stmt="${shifu_case_stmt}
Expand All @@ -710,27 +714,30 @@ ${shifu_defer_comp_case:-}"
shifu_last_arg_is_eager=true
"$shifu_cmd"
case "${shifu_current_word:-}" in
-*) return 0 ;;
-*) [ "$shifu_parse_eager" = false ] && return 0 ;;
esac
if [ $shifu_parse_stage -eq 0 -a $shifu_arg_completion_set = false ]; then
shifu_case_stmt="$shifu_case_stmt shift ;;"
fi
if [ "$shifu_parse_eager" = true ]; then
_shifu_append_on_new_line shifu_case_stmt " *) break ;;"
_shifu_append_on_new_line shifu_case_stmt " *) _shifu_comp_done=true ;;"
elif [ $shifu_parse_stage -ne 0 ]; then
_shifu_append_on_new_line shifu_case_stmt " break ;;"
_shifu_append_on_new_line shifu_case_stmt " _shifu_comp_done=true ;;"
elif [ $shifu_arg_completion_set = true ]; then
_shifu_append_on_new_line shifu_case_stmt " *) break ;;"
_shifu_append_on_new_line shifu_case_stmt " *) _shifu_comp_done=true ;;"
else
_shifu_append_on_new_line shifu_case_stmt " break ;;"
_shifu_append_on_new_line shifu_case_stmt " _shifu_comp_done=true ;;"
fi
_shifu_append_on_new_line shifu_case_stmt "esac"
_shifu_args_parsed=$#
_shifu_n_args=$#
while true; do
eval "$shifu_case_stmt" 2>/dev/null || exit 1
[ $# -eq $_shifu_n_args ] && exit 1
_shifu_comp_done=false
while [ "$_shifu_comp_done" = false ]; do
eval "$shifu_case_stmt" 2>/dev/null || return 1
[ $# -eq $_shifu_n_args ] && _shifu_comp_done=true
_shifu_n_args=$#
done
_shifu_args_parsed=$((_shifu_args_parsed - $#))
}

_shifu_filter_matching_options() {
Expand Down
Loading
Loading