Skip to content

Instantly share code, notes, and snippets.

@mattmc3
Last active October 1, 2025 15:52
Show Gist options
  • Save mattmc3/804a8111c4feba7d95b6d7b984f12a53 to your computer and use it in GitHub Desktop.
Save mattmc3/804a8111c4feba7d95b6d7b984f12a53 to your computer and use it in GitHub Desktop.
Zsh option parsing example
# Manual opt parsing example
#
# Features:
# - supports short and long flags (ie: -v|--verbose)
# - supports short and long key/value options (ie: -f <file> | --filename <file>)
# - supports short and long key/value options with equals assignment (ie: -f=<file> | --filename=<file>)
# - does NOT support short option chaining (ie: -vh)
# - everything after -- is positional even if it looks like an option (ie: -f)
# - once we hit an arg that isn't an option flag, everything after that is considered positional
function optparsing_demo() {
local positional=()
local flag_verbose=false
local filename=myfile
local usage=(
"optparsing_demo [-h|--help]"
"optparsing_demo [-v|--verbose] [-f|--filename=<file>] [<message...>]"
)
opterr() { echo >&2 "optparsing_demo: Unknown option '$1'" }
while (( $# )); do
case $1 in
--) shift; positional+=("${@[@]}"); break ;;
-h|--help) printf "%s\n" $usage && return ;;
-v|--verbose) flag_verbose=true ;;
-f|--filename) shift; filename=$1 ;;
-f=*|--filename=*) filename="${1#*=}" ;;
-*) opterr $1 && return 2 ;;
*) positional+=("${@[@]}"); break ;;
esac
shift
done
echo "--verbose: $flag_verbose"
echo "--filename: $filename"
echo "positional: $positional"
}
# zparseopts
#
# Resources:
# - https://xpmo.gitlab.io/post/using-zparseopts/
# - https://zsh.sourceforge.io/Doc/Release/Zsh-Modules.html#index-zparseopts
#
# Features:
# - supports short and long flags (ie: -v|--verbose)
# - supports short and long key/value options (ie: -f <file> | --filename <file>)
# - does NOT support short and long key/value options with equals assignment (ie: -f=<file> | --filename=<file>)
# - supports short option chaining (ie: -vh)
# - everything after -- is positional even if it looks like an option (ie: -f)
# - once we hit an arg that isn't an option flag, everything after that is considered positional
function zparseopts_demo() {
local flag_help flag_verbose
local arg_filename=(myfile) # set a default
local usage=(
"zparseopts_demo [-h|--help]"
"zparseopts_demo [-v|--verbose] [-f|--filename=<file>] [<message...>]"
)
# -D pulls parsed flags out of $@
# -E allows flags/args and positionals to be mixed, which we don't want in this example
# -F says fail if we find a flag that wasn't defined
# -M allows us to map option aliases (ie: h=flag_help -help=h)
# -K allows us to set default values without zparseopts overwriting them
# Remember that the first dash is automatically handled, so long options are -opt, not --opt
zmodload zsh/zutil
zparseopts -D -F -K -- \
{h,-help}=flag_help \
{v,-verbose}=flag_verbose \
{f,-filename}:=arg_filename ||
return 1
[[ -z "$flag_help" ]] || { print -l $usage && return }
if (( $#flag_verbose )); then
print "verbose mode"
fi
echo "--verbose: $flag_verbose"
echo "--filename: $arg_filename[-1]"
echo "positional: $@"
}

Here are some examples of the manual parsing function in action...

Call with no args

$ optparsing_demo
--verbose: false
--filename: myfile
positional:

Call with both short and long options, as well as positional args

$ optparsing_demo --verbose -f test.txt foo
--verbose: true
--filename: test.txt
positional: foo

Call with -- to pass positionals that look like flags

$ optparsing_demo --filename=test.txt -- -v --verbose -f --filename are acceptable options
--verbose: false
--filename: test.txt
positional: -v --verbose -f --filename are acceptable options

Called incorrectly with positionals before intended opts

$ optparsing_demo do not put positionals before opts --verbose --filename=mynewfile
--verbose: false
--filename: myfile
positional: do not put positionals before opts --verbose --filename=mynewfile

This method of opt parsing does not support flag chaining like getopt does

$ optparsing_demo -vh
optparsing_demo: Unknown option '-vh'

Here are some examples of the zparseopt version in action...

Call with no args

$ zparseopts_demo
--verbose:
--filename: myfile
positional:

Call with both short and long options, as well as positional args

$ zparseopts_demo --verbose -f test.txt foo
--verbose: --verbose
--filename: test.txt
positional: foo

Call with -- to pass positionals that look like flags

$ zparseopts_demo --filename test.txt -- -v --verbose -f --filename are acceptable options
--verbose:
--filename: test.txt
positional: -v --verbose -f --filename are acceptable options

Called incorrectly with positionals before intended opts. If you want this, zparseopts supports it with the -E flag.

$ zparseopts_demo do not put positionals before opts --verbose --filename=mynewfile
--verbose:
--filename: myfile
positional: do not put positionals before opts --verbose --filename=mynewfile

This method of opt parsing does supports flag chaining like getopt does

$ zparseopts_demo -vh
zparseopts_demo [-h|--help]
zparseopts_demo [-v|--verbose] [-f|--filename=<file>] [<message...>]
@wyatt-wong
Copy link

However how do I display the help message ONLY when I execute zparseopts_demo -h or zparseopts_demo —help ? If I simply type zparseopts_demo, I want to execute the script or function instead of display the help message.

That's already what it does. In this example, -h/--help displays the contents of $usage, and without them the function executes - its execution just happens to also print output for demo purposes.

I don't quite understand what [[ -z "$flag_help" ]] || { print -l $usage && return } does. When zparseopts_demo execute without any arguments, it called the zparseopts_demo() function and set the local variables flag_help and flag_verbose with a null value, right ?

Then after initialize zmodload and zparseopts, the code ran [[ -z "$flag_help" ]] || { print -l $usage && return }, since $flag_help variable is null so [[ -z "$flag_help" ]] is true, but why wouldn't it continues to execute { print -l $usage && return } ?

Furthermore is it possible to pass an argument without using - or —

Yes, those are called positionals in this demo, and the contents are in "$@". Basically, zparseopts pulls out any -f/--flags that you've mapped and resets your argument array to what's left. If you want to mix positionals with flags (which I do not recommend, but others like), you want to use the -E option. Additionally, you can use subcommands like how the git command does by simply shifting off the first argument $1, using that as a subcommand to determine further behavior with the remaining arguments in the arg array. That lets you do things like zparseopts_demo mysubcommand --foo --bar --baz positional1 positional2 etc. See https://zsh.sourceforge.io/Doc/Release/Zsh-Modules.html#index-zparseopts for further info.

No. What I mean is I don't want to use - or -- in the argument but to execute the script like this:

zparseopts_demo build # Pass build as the argument and the zparseopts_demo script or function will detect there is an argument called build and proceed to do something. If user executre zparseopts_demo without passing the build parameter then the script will perform something else.

If I execute zparseopts_demo -h or zparseopts_demo --help, then it will print out the help message.

Or does it mean I need to use the format like zparseopts_demo -p build if I want to pass build as an argument but CANNOT simply pass the build argument as zparseopts_demo build ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment