aboutsummaryrefslogtreecommitdiff
path: root/args.bash
diff options
context:
space:
mode:
authorChloƩ Vulquin <code@toast.bunkerlabs.net>2026-01-14 22:22:22 +0100
committerChloƩ Vulquin <code@toast.bunkerlabs.net>2026-01-15 06:37:04 +0100
commit74e70ef6e33cc57a1077892deccf6424199ac7ab (patch)
tree3ba24598a6bb4226b188f060154092d0db4313b0 /args.bash
initial importshlib
Diffstat (limited to 'args.bash')
-rw-r--r--args.bash166
1 files changed, 166 insertions, 0 deletions
diff --git a/args.bash b/args.bash
new file mode 100644
index 0000000..464e73c
--- /dev/null
+++ b/args.bash
@@ -0,0 +1,166 @@
+#!/bin/bash
+# a schemaless argument parser
+
+# this file is licensed under any of the following SPDX licenses
+# to be chosen by the user:
+# * 0BSD (https://spdx.org/licenses/0BSD.html)
+# * BlueOak-1.0.0 (https://blueoakcouncil.org/license/1.0.0)
+# * CC0-1.0 (https://creativecommons.org/publicdomain/zero/1.0/)
+# * Unlicense (https://unlicense.org/)
+
+# things you can do with it:
+# -fval | -f=val | -f val: set "f" to "val"
+# --foo=val | --foo val: set "foo" to "val"
+# @oval | @o=val | @o val: add "val" to the "o" array
+# @@opt=val | @@opt val: add "val" to the "opt" array
+# --: stop parsing options, put the rest into $@
+
+# you may have noticed:
+# * there are no booleans, long or short. you can emulate them by =yes|no, or have a dedicated options|o array.
+# * you cannot reset arrays: you actually can, by using -oval and --opt=val, but you can never make them empty
+
+# something you may not notice without reading the code:
+# * values may contain spaces, but may *not* contain literal 's, arg names may not contain spaces, which will break horribly
+# * this leaks __die(), __verify(), and $__options. Technically also parse().
+# * this is not compatible with numerical args, like -9
+
+# reading notes:
+# let NUM is true if num > 0
+# this is the same as (( NUM ))
+
+# usage:
+# 1. save this to some file (let's say ./args.bash)
+# 2. optionally, define default values for any of your arguments
+# 3. optionally, define any namerefs to have long/short variants (e.g declare -n r=repo, which will make @r and @@repo the same)
+# 4. . ./args.bash "$@"
+
+__die() {
+ echo "$@" >&2
+ exit
+}
+
+# $1 is arg, $2 is val
+__verify() {
+ [[ ${1% *} != $1 ]] && __die "args may not contain spaces, but `$1` does"
+ [[ "${2%\'*}" != $2 ]] && __die "values may not contain 's, but `$2` does"
+ true
+}
+
+parse() {
+ declare one two val arg
+ declare -g __options
+
+ while let $#; do
+ arg="$1"
+ one="${arg::1}"
+ two="${arg::2}"
+ # needed because we check -v
+ unset val; declare val
+ shift
+
+ case "$arg" in
+ # literal --, stop processing options
+ --) __options+=("$@"); return ;;
+ # long option
+ --*)
+ # the arg without the --
+ arg=${arg:2}
+
+ # if there's a = in there, we know the value
+ if [[ ${arg%%=*} != $arg ]]; then
+ val=${arg#*=}
+ arg=${arg%%=*}
+ fi
+
+ # else, use the next arg
+ if [[ ! -v val ]]; then
+ let $# || __die "--$arg needs a value, found none"
+ val=$1
+ shift
+ fi
+
+ __verify "$arg" "$val"
+ eval $arg="'$val'"
+ ;;
+ # short option
+ -*)
+ # the arg without the -
+ arg=${arg:1}
+
+ # we're dealing with -f=val
+ if [[ ${arg:1:1} = '=' ]]; then
+ val=${arg:2}
+ arg=${arg::1}
+ fi
+
+ # we haven't found an =, and there's space left - that must be the value
+ if [[ ! -v val ]] && (( ${#arg} > 1 )); then
+ val=${arg:1}
+ arg=${arg::1}
+ fi
+
+ # we still haven't found the value, use the next arg
+ if [[ ! -v val ]]; then
+ let $# || __die "-$arg needs a value, found none"
+ val=$1
+ shift
+ fi
+
+ __verify "$arg" "$val"
+ eval $arg="'$val'"
+ ;;
+ # long array
+ @@*)
+ # the arg without the @@
+ arg=${arg:2}
+
+ # if there's a = in there, we know the value
+ if [[ ${arg%%=*} != $arg ]]; then
+ val=${arg#*=}
+ arg=${arg%%=*}
+ fi
+
+ # else, use the next arg
+ if [[ ! -v val ]]; then
+ let $# || __die "@@$arg needs a value, found none"
+ val=$1
+ shift
+ fi
+
+ __verify "$arg" "$val"
+ eval $arg+="('$val')"
+ ;;
+ # short array
+ @*)
+ # the arg without the @
+ arg=${arg:1}
+
+ # we're dealing with @f=val
+ if [[ ${arg:1:1} = '=' ]]; then
+ val=${arg:2}
+ arg=${arg::1}
+ fi
+
+ # we haven't found an =, and there's space left - that must be the value
+ if [[ ! -v val ]] && (( ${#arg} > 1 )); then
+ val=${arg:1}
+ arg=${arg::1}
+ fi
+
+ # we still haven't found the value, use the next arg
+ if [[ ! -v val ]]; then
+ let $# || __die "@$arg needs a value, found none"
+ val=$1
+ shift
+ fi
+
+ __verify "$arg" "$val"
+ eval $arg+="('$val')"
+ ;;
+ *) __options+=("$arg") ;;
+ esac
+ done
+}
+
+parse "$@"
+set -- "${__options[@]}"