Skip to content

Instantly share code, notes, and snippets.

@mattmc3
Last active December 14, 2024 19:50
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...>]
@markjaquith
Copy link

This was helpful to me. Thanks!

Line 70 should be:

{f,-filename}:=arg_filename ||

(--filename missing its second dash)

@mattmc3
Copy link
Author

mattmc3 commented Jul 30, 2022

Thanks @markjaquith. Good catch!

@PostgreSqlStan
Copy link

I wouldn't have had the patience to learn about option parsing without this resource. Thanks.

@mattmc3
Copy link
Author

mattmc3 commented Sep 20, 2023

Thanks @PostgreSqlStan! This was the only article I found on the topic, and this gist was born from what I learned after reading that. The official zparseopts docs are pretty tough to read, as is most Zsh documentation. Glad you found this helpful.

@Goosegit11
Copy link

Goosegit11 commented Feb 18, 2024

Thanks, that was helpful for me as well.

Another helpful video I found:
https://johnlindquist.com/parsing-arguments-with-zparseopts-in-zsh-functions

btw I use antidote

@WickyNilliams
Copy link

i wanted to test for the presence of a flag, when using an associative array:

zparseopts -A opts -force

i found many different ways to do this, all of them felt a bit complex. but i realised i can test for the presence of a key in the associative array:

if [[ -v opts[--force] ]]; then
  echo "got --force flag"
else
  echo "no --force flag"
fi

which felt clear and simple. hope this helps someone

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