[pacman-dev] [PATCH v4] libmakepkg: add optional argument support to parseopts

Ethan Sommer e5ten.arch at gmail.com
Mon Nov 4 00:45:04 UTC 2019


Adds a "?" suffix that can be used to indicate that an option's argument is
optional.

This allows options to have a default behaviour when the user doesn't
specify one, e.g.: --color=[when] being able to behave like --color=auto
when only --color is passed

Options with optional arguments given on the command line will be returned
in the form "--opt=optarg" and "-o=optarg". Despite that not being the
syntax for passing an argument with a shortopt (trying to pass -o=foo
would make -o's argument "=foo"), this is done to allow the caller to split
the option and its optarg easily

Signed-off-by: Ethan Sommer <e5ten.arch at gmail.com>
---
 scripts/libmakepkg/util/parseopts.sh.in | 116 +++++++++++++++---------
 test/scripts/parseopts_test.sh          |  12 ++-
 2 files changed, 83 insertions(+), 45 deletions(-)

diff --git a/scripts/libmakepkg/util/parseopts.sh.in b/scripts/libmakepkg/util/parseopts.sh.in
index c056cb1e..42540438 100644
--- a/scripts/libmakepkg/util/parseopts.sh.in
+++ b/scripts/libmakepkg/util/parseopts.sh.in
@@ -18,16 +18,23 @@
 #   along with this program.  If not, see <http://www.gnu.org/licenses/>.
 #
 # A getopt_long-like parser which portably supports longopts and
-# shortopts with some GNU extensions. It does not allow for options
-# with optional arguments. For both short and long opts, options
-# requiring an argument should be suffixed with a colon. After the
-# first argument containing the short opts, any number of valid long
-# opts may be be passed. The end of the options delimiter must then be
-# added, followed by the user arguments to the calling program.
+# shortopts with some GNU extensions. For both short and long opts,
+# options requiring an argument should be suffixed with a colon, and
+# options with optional arguments should be suffixed with a question
+# mark. After the first argument containing the short opts, any number
+# of valid long opts may be be passed. The end of the options delimiter
+# must then be added, followed by the user arguments to the calling
+# program.
+#
+# Options with optional arguments will be returned as "--longopt=optarg"
+# for longopts, or "-o=optarg" for shortopts. This isn't actually a valid
+# way to pass an optional argument with a shortopt on the command line,
+# but is done by parseopts to enable the caller script to split the option
+# and its optarg easily.
 #
 # Recommended Usage:
-#   OPT_SHORT='fb:z'
-#   OPT_LONG=('foo' 'bar:' 'baz')
+#   OPT_SHORT='fb:zq?'
+#   OPT_LONG=('foo' 'bar:' 'baz' 'qux?')
 #   if ! parseopts "$OPT_SHORT" "${OPT_LONG[@]}" -- "$@"; then
 #     exit 1
 #   fi
@@ -49,29 +56,30 @@ parseopts() {
 	longoptmatch() {
 		local o longmatch=()
 		for o in "${longopts[@]}"; do
-			if [[ ${o%:} = "$1" ]]; then
+			if [[ ${o%[:?]} = "$1" ]]; then
 				longmatch=("$o")
 				break
 			fi
-			[[ ${o%:} = "$1"* ]] && longmatch+=("$o")
+			[[ ${o%[:?]} = "$1"* ]] && longmatch+=("$o")
 		done
 
 		case ${#longmatch[*]} in
 			1)
-				# success, override with opt and return arg req (0 == none, 1 == required)
-				opt=${longmatch%:}
-				if [[ $longmatch = *: ]]; then
-					return 1
-				else
-					return 0
-				fi ;;
+				# success, override with opt and return arg req (0 == none, 1 == required, 2 == optional)
+				opt=${longmatch%[:?]}
+				case $longmatch in
+					*:)  return 1 ;;
+					*\?) return 2 ;;
+					*)   return 0 ;;
+				esac
+				;;
 			0)
 				# fail, no match found
 				return 255 ;;
 			*)
 				# fail, ambiguous match
 				printf "${0##*/}: $(gettext "option '%s' is ambiguous; possibilities:")" "--$1"
-				printf " '%s'" "${longmatch[@]%:}"
+				printf " '%s'" "${longmatch[@]%[:?]}"
 				printf '\n'
 				return 254 ;;
 		esac >&2
@@ -87,32 +95,47 @@ parseopts() {
 				for (( i = 1; i < ${#1}; i++ )); do
 					opt=${1:i:1}
 
-					# option doesn't exist
-					if [[ $shortopts != *$opt* ]]; then
-						printf "${0##*/}: $(gettext "invalid option") -- '%s'\n" "$opt" >&2
-						OPTRET=(--)
-						return 1
-					fi
-
-					OPTRET+=("-$opt")
-					# option requires optarg
-					if [[ $shortopts = *$opt:* ]]; then
-						# if we're not at the end of the option chunk, the rest is the optarg
-						if (( i < ${#1} - 1 )); then
-							OPTRET+=("${1:i+1}")
-							break
-						# if we're at the end, grab the the next positional, if it exists
-						elif (( i == ${#1} - 1 )) && [[ $2 ]]; then
-							OPTRET+=("$2")
-							shift
-							break
-						# parse failure
-						else
-							printf "${0##*/}: $(gettext "option requires an argument") -- '%s'\n" "$opt" >&2
+					case $shortopts in
+						# option requires optarg
+						*$opt:*)
+							# if we're not at the end of the option chunk, the rest is the optarg
+							if (( i < ${#1} - 1 )); then
+								OPTRET+=("-$opt" "${1:i+1}")
+								break
+							# if we're at the end, grab the the next positional, if it exists
+							elif (( i == ${#1} - 1 )) && [[ $2 ]]; then
+								OPTRET+=("-$opt" "$2")
+								shift
+								break
+							# parse failure
+							else
+								printf "${0##*/}: $(gettext "option requires an argument") -- '%s'\n" "$opt" >&2
+								OPTRET=(--)
+								return 1
+							fi
+							;;
+						# option's optarg is optional
+						*$opt\?*)
+							# if we're not at the end of the option chunk, the rest is the optarg
+							if (( i < ${#1} - 1 )); then
+								OPTRET+=("-$opt=${1:i+1}")
+								break
+							# option has no optarg
+							else
+								OPTRET+=("-$opt")
+							fi
+							;;
+						# option has no optarg
+						*$opt*)
+							OPTRET+=("-$opt")
+							;;
+						# option doesn't exist
+						*)
+							printf "${0##*/}: $(gettext "invalid option") -- '%s'\n" "$opt" >&2
 							OPTRET=(--)
 							return 1
-						fi
-					fi
+							;;
+					esac
 				done
 				;;
 			--?*=*|--?*) # long option
@@ -145,6 +168,15 @@ parseopts() {
 							return 1
 						fi
 						;;
+					2)
+						# --longopt=optarg
+						if [[ $1 = *=* ]]; then
+							OPTRET+=("--$opt=$optarg")
+						# --longopt
+						else
+							OPTRET+=("--$opt")
+						fi
+						;;
 					254)
 						# ambiguous option -- error was reported for us by longoptmatch()
 						OPTRET=(--)
diff --git a/test/scripts/parseopts_test.sh b/test/scripts/parseopts_test.sh
index 9674c6a6..8f1ea1f3 100755
--- a/test/scripts/parseopts_test.sh
+++ b/test/scripts/parseopts_test.sh
@@ -16,12 +16,12 @@ if ! type -t parseopts &>/dev/null; then
 fi
 
 # borrow opts from makepkg
-OPT_SHORT="AcdefFghiLmop:rRsV"
+OPT_SHORT="AcdefFghiLmop:rRsVb?"
 OPT_LONG=('allsource' 'asroot' 'ignorearch' 'check' 'clean:' 'cleanall' 'nodeps'
           'noextract' 'force' 'forcever:' 'geninteg' 'help' 'holdver'
           'install' 'key:' 'log' 'nocolor' 'nobuild' 'nocheck' 'noprepare' 'nosign' 'pkg:' 'rmdeps'
           'repackage' 'skipinteg' 'sign' 'source' 'syncdeps' 'version' 'config:'
-          'noconfirm' 'noprogressbar')
+          'noconfirm' 'noprogressbar' 'opt?')
 
 tap_parse() {
 	local result=$1 tokencount=$2; shift 2
@@ -31,7 +31,7 @@ tap_parse() {
 	unset OPTRET
 }
 
-tap_plan 50
+tap_plan 54
 
 # usage: tap_parse <expected result> <token count> test-params...
 # a failed tap_parse will match only the end of options marker '--'
@@ -111,4 +111,10 @@ tap_parse '--force --' 2 --force
 # exact match on possible stem (opt has optarg)
 tap_parse '--clean foo --' 3 --clean=foo
 
+# long opt with empty, non-empty, and no optional arg
+tap_parse '--opt= --opt=foo --opt --' 4 --opt= --opt=foo --opt
+
+# short opt with and without optional arg, and non-option arg
+tap_parse '-b=foo -A -b -- foo' 5 -bfoo -Ab foo
+
 tap_finish
-- 
2.23.0


More information about the pacman-dev mailing list