aboutsummaryrefslogtreecommitdiff
path: root/args.bash
blob: 464e73c39655c2a46f01c9a94e5dd5463f12e191 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
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[@]}"