Optional option argument with getopts - bash

while getopts "hd:R:" arg; do
case $arg in
h)
echo "usage"
;;
d)
dir=$OPTARG
;;
R)
if [[ $OPTARG =~ ^[0-9]+$ ]];then
level=$OPTARG
else
level=1
fi
;;
\?)
echo "WRONG" >&2
;;
esac
done
level refers to the parameter of -R, dir refers to parameters of -d
when I input ./count.sh -R 1 -d test/ it works correctly
when I input ./count.sh -d test/ -R 1 it works correctly
but I want to have it work when I input ./count.sh -d test/ -R or ./count.sh -R -d test/
This means that I want -R to have a default value and for the sequence of commands to be more flexible.

This workaround defines 'R' with no argument (no ':'), tests for any argument after the '-R' (manage last option on the command line) and tests if an existing argument starts with a dash.
# No : after R
while getopts "hd:R" arg; do
case $arg in
(...)
R)
# Check next positional parameter
eval nextopt=\${$OPTIND}
# existing or starting with dash?
if [[ -n $nextopt && $nextopt != -* ]] ; then
OPTIND=$((OPTIND + 1))
level=$nextopt
else
level=1
fi
;;
(...)
esac
done

getopts doesn't really support this; but it's not hard to write your own replacement.
while true; do
case $1 in
-R) level=1
shift
case $1 in
*[!0-9]* | "") ;;
*) level=$1; shift ;;
esac ;;
# ... Other options ...
-*) echo "$0: Unrecognized option $1" >&2
exit 2;;
*) break ;;
esac
done

Wrong. Actually getopts does support optional arguments! From the bash man page:
If a required argument is not found, and getopts is not silent,
a question mark (?) is placed in name, OPTARG is unset, and a diagnostic
message is printed. If getopts is silent, then a colon (:) is placed in name
and OPTARG is set to the option character found.
When the man page says "silent" it means silent error reporting. To enable it, the first character of optstring needs to be a colon:
while getopts ":hd:R:" arg; do
# ...rest of iverson's loop should work as posted
done
Since Bash's getopt does not recognize -- to end the options list, it may not work when -R is the last option, followed by some path argument.
P.S.: Traditionally, getopt.c uses two colons (::) to specify an optional argument. However, the version used by Bash doesn't.

I agree with tripleee, getopts does not support optional argument handling.
The compromised solution I have settled on is to use the upper case/lower case combination of the same option flag to differentiate between the option that takes an argument and the other that does not.
Example:
COMMAND_LINE_OPTIONS_HELP='
Command line options:
-I Process all the files in the default dir: '`pwd`'/input/
-i DIR Process all the files in the user specified input dir
-h Print this help menu
Examples:
Process all files in the default input dir
'`basename $0`' -I
Process all files in the user specified input dir
'`basename $0`' -i ~/my/input/dir
'
VALID_COMMAND_LINE_OPTIONS="i:Ih"
INPUT_DIR=
while getopts $VALID_COMMAND_LINE_OPTIONS options; do
#echo "option is " $options
case $options in
h)
echo "$COMMAND_LINE_OPTIONS_HELP"
exit $E_OPTERROR;
;;
I)
INPUT_DIR=`pwd`/input
echo ""
echo "***************************"
echo "Use DEFAULT input dir : $INPUT_DIR"
echo "***************************"
;;
i)
INPUT_DIR=$OPTARG
echo ""
echo "***************************"
echo "Use USER SPECIFIED input dir : $INPUT_DIR"
echo "***************************"
;;
\?)
echo "Usage: `basename $0` -h for help";
echo "$COMMAND_LINE_OPTIONS_HELP"
exit $E_OPTERROR;
;;
esac
done

This is actually pretty easy. Just drop the trailing colon after the R and use OPTIND
while getopts "hRd:" opt; do
case $opt in
h) echo -e $USAGE && exit
;;
d) DIR="$OPTARG"
;;
R)
if [[ ${#:$OPTIND} =~ ^[0-9]+$ ]];then
LEVEL=${#:$OPTIND}
OPTIND=$((OPTIND+1))
else
LEVEL=1
fi
;;
\?) echo "Invalid option -$OPTARG" >&2
;;
esac
done
echo $LEVEL $DIR
count.sh -d test
test
count.sh -d test -R
1 test
count.sh -R -d test
1 test
count.sh -d test -R 2
2 test
count.sh -R 2 -d test
2 test

Inspired in #calandoa's answer (the only one that actually works!), I've made a simple function that can make it easy to be used multiple times.
getopts_get_optional_argument() {
eval next_token=\${$OPTIND}
if [[ -n $next_token && $next_token != -* ]]; then
OPTIND=$((OPTIND + 1))
OPTARG=$next_token
else
OPTARG=""
fi
}
An example usage:
while getopts "hdR" option; do
case $option in
d)
getopts_get_optional_argument $#
dir=${OPTARG}
;;
R)
getopts_get_optional_argument $#
level=${OPTARG:-1}
;;
h)
show_usage && exit 0
;;
\?)
show_usage && exit 1
;;
esac
done
This gives us a practical way to get "that missing feature" in getopts :)
NOTE that nevertheless command-line options with optional args seems to be discouraged explicitly
Guideline 7: Option-arguments should not be optional.
but I have no intuitive way to implement my case without this: I have 2 modes that are activated by either using one flag or other, and those have both an argument with a clear default. Introducing a third flag just to disambiguate makes it look a bad CLI style.
I've tested this with many combinations, including all in #aaron-sua's answer and works well.

I think there are two way.
First is calandoa's answer, Using OPTIND and no silent mode.
Second is Using OPTIND and silent mode.
while getopts ":Rr:" name; do
case ${name} in
R)
eval nextArg=\${$OPTIND}
# check option followed by nothing or other option.
if [[ -z ${nextArg} || $nextArg =~ ^-.* ]]; then
level=1
elif [[ $nextArg =~ ^[0-9]+$ ]]; then
level=$nextArg
OPTIND=$((OPTIND + 1))
else
level=1
fi
;;
r)
# check option followed by other option.
if [[ $OPTARG =~ ^-.* ]]; then
OPTIND=$((OPTIND - 1))
level2=2
elif [[ $OPTARG =~ ^[0-9]+$ ]]; then
level2="$OPTARG"
else
level2=2
fi
;;
:)
# check no argument
case $OPTARG in
r)
level2=2
;;
esac
esac
done
echo "Level 1 : $level"
echo "Level 2 : $level2"

All solutions presented so far put code in case ... in ... esac, but in my opinion, it would be much more natural to have a modified getopts command, thus I wrote this function:
EDIT:
Now, you can specify the type of an optional arg (see usage info).
Furthermore, instead of testing if $nextArg "looks like" an option(s) arg, the function now checks if $nextArg contains a letter from $optstring.
This way, an option letter not contained in $optstring can be used as optional arg, as with getopts' mandatory args.
Latest changes:
Fixed test if $nextArg is an option arg:
Test if $nextArg begins with a dash.
Without this test, optional args that contain a letter
from $optstring are not recognised as such.
Added regexp type specifier (see usage info).
Fixed: 0 not recognised as optional arg specified to be an int.
Simplified test if $nextArg is an int.
Type specifier ::/.../: Use perl to test if $nextArg matches the regexp.
This way, you benefit from (almost (*)) the full power of Perl regexps.
(*): See last paragraph of usage info.
Fixed: Doesn't work with more than one regexp type specifier:
Use perl instead of grep/sed constructs because non-greedy matching is needed.
Usage:
Invocation: getopts-plus optstring name "$#"
optstring: Like normal getopts, but you may specify options with optional argument by appending :: to the option letter.
However, if your script supports an invocation with an option with optional argument as the only option argument, followed by a non-option argument, the non-option argument will be considered to be the argument for the option.
If you're lucky and the optional argument is expected to be an integer, whereas the non-option argument is a string or vice versa, you may specify the type by appending :::i for an integer or :::s for a string to solve that issue.
If that doesn't apply, you may specify a Perl regexp for the optional arg by appending ::/.../ to the option letter.
See here for an introduction to Perl regexps: https://perldoc.perl.org/perlretut
Please note: ATM, only /.../ will be recognised as a regexp after ::, i. e. neither other delimiters, nor modifiers may be used, so e. g. m#...#a will not be recognised.
If there is a non-option argument after the option with optional argument, it will be considered to be the optional argument only if it matches the regexp.
To be clear: ::/.../ is not meant for argument validation but solely to discriminate between arguments for options with optional argument and non-option arguments.
#!/bin/bash
# Invocation: getopts-plus optstring name "$#"\
# \
# optstring: Like normal getopts, but you may specify options with optional argument
# by appending :: to the option letter.\
# \
# However, if your script supports an invocation with an option with optional
# argument as the only option argument, followed by a non-option argument,
# the non-option argument will be considered to be the argument for the option.\
# \
# If you're lucky and the optional argument is expected to be an integer, whereas
# the non-option argument is a string or vice versa, you may specify the type by
# appending :::i for an integer or :::s for a string to solve that issue.\
# \
# If that doesn't apply, you may specify a Perl regexp for the optional arg by appending
# ::/.../ to the option letter.\
# See here for an introduction to Perl regexps: https://perldoc.perl.org/perlretut
# Please note: ATM, only /.../ will be recognised as a regexp after ::,\
# i. e. neither other delimiters, nor modifiers may be used, so e. g. m#...#a will
# not be recognised.\
# If there is a non-option argument after the option with optional argument, it will
# be considered to be the optional argument only if it matches the regexp.\
# To be clear: ::/.../ is not meant for argument validation but solely to discriminate
# between arguments for options with optional argument and non-option arguments.
function getopts-plus
{
local optstring=$1
local -n name=$2
shift 2
local optionalArgSuffixRE='::(?::[si]|/.*?/)?'
local optionalArgTypeCaptureRE=':::([si])|::(/.*?/)'
# If we pass 'opt' for 'name' (as I always do when using getopts) and there is
# also a local variable 'opt', the "outer" 'opt' will always be empty.
# I don't understand why a local variable interferes with caller's variable with
# same name in this case; however, we can easily circumvent this.
local opt_
# Extract options with optional arg
local -A isOptWithOptionalArg
while read opt_; do
# Using an associative array as set
isOptWithOptionalArg[$opt_]=1
done <<<$(perlGetCaptures "$optstring" "([a-zA-Z])$optionalArgSuffixRE")
# Extract all option letters (used to weed out possible optional args that are option args)
local optLetters=$(perlGetCaptures "$optstring" "([a-zA-Z])(?:$optionalArgSuffixRE|:)?")
# Save original optstring, then remove our suffix(es)
local optstringOrg=$optstring
optstring=$(perl -pe "s#$optionalArgSuffixRE##g" <<<$optstring)
getopts $optstring name "$#" || return # Return value is getopts' exit value.
# If current option is an option with optional arg and if an arg has been provided,
# check if that arg is not an option and if it isn't, check if that arg matches(*)
# the specified type, if any, and if it does or no type has been specified,
# assign it to OPTARG and inc OPTIND.
#
# (*) We detect an int because it's easy, but we assume a string if it's not an int
# because detecting a string would be complicated.
# So it sounds strange to call it a match if we know that the optional arg is specified
# to be a string, but merely that the provided arg is not an int, but in this context,
# "not an int" is equivalent to "string". At least I think so, but I might be wrong.
if ((isOptWithOptionalArg[$name])) && [[ ${!OPTIND} ]]; then
local nextArg=${!OPTIND} foundOpt=0
# Test if $nextArg is an option arg
if [[ $nextArg == -* ]]; then
# Check if $nextArg contains a letter from $optLetters.
# This way, an option not contained in $optstring can be
# used as optional arg, as with getopts' mandatory args.
local i
# Start at char 1 to skip the leading dash
for ((i = 1; i < ${#nextArg}; i++)); do
while read opt_; do
[[ ${nextArg:i:1} == $opt_ ]] && foundOpt=1 && break 2
done <<<$optLetters
done
((foundOpt)) && return
fi
# Extract type of optional arg if specified
local optArgType=$(perlGetCaptures "$optstringOrg" "$name(?:$optionalArgTypeCaptureRE)" '$1$2')
local nextArgIsOptArg=0
case $optArgType in
/*/) # Check if $nextArg matches regexp
perlMatch "$nextArg" "$optArgType" && nextArgIsOptArg=1
;;
[si]) # Check if $nextArg is an int
local nextArgIsInt=0
[[ $nextArg =~ ^[0-9]+$ ]] && nextArgIsInt=1
# Test if specified type and arg type match (see (*) above).
# N.B.: We need command groups since && and || between commands have same precedence.
{ [[ $optArgType == i ]] && ((nextArgIsInt)) || { [[ $optArgType == s ]] && ((! nextArgIsInt)); }; } && nextArgIsOptArg=1
;;
'') # No type or regexp specified => Assume $nextArg is optional arg.
nextArgIsOptArg=1
;;
esac
if ((nextArgIsOptArg)); then
OPTARG=$nextArg && ((OPTIND++))
fi
fi
}
# Uses perl to match \<string\> against \<regexp\>.\
# Returns with code 0 on a match and 1 otherwise.
function perlMatch # Args: <string> <regexp>
{
perl -e 'q('"$1"') =~ '"$2"' and exit 0; exit 1;'
}
# Uses perl to match \<string\> against \<regexp\>
# and prints each capture on a separate line.\
# If \<regexp\> contains more than one capture group,
# you must specify the \<line format\> which is an
# arbitrary Perl string containing your desired backrefs.\
# By default, merely $1 will be printed.
function perlGetCaptures # Args: <string> <regexp> [<line format>]
{
local lineFmt=${3:-\$1}
# Matching repeatedly with g option gives one set of captures at a time.
perl -e 'while (q('"$1"') =~ m#'"$2"'#g) { print(qq('"$lineFmt"') . "\n"); }'
}
The same script without comments inside function bodies in case you don't need them:
#!/bin/bash
# Invocation: getopts-plus optstring name "$#"\
# \
# optstring: Like normal getopts, but you may specify options with optional argument
# by appending :: to the option letter.\
# \
# However, if your script supports an invocation with an option with optional
# argument as the only option argument, followed by a non-option argument,
# the non-option argument will be considered to be the argument for the option.\
# \
# If you're lucky and the optional argument is expected to be an integer, whereas
# the non-option argument is a string or vice versa, you may specify the type by
# appending :::i for an integer or :::s for a string to solve that issue.\
# \
# If that doesn't apply, you may specify a Perl regexp for the optional arg by appending
# ::/.../ to the option letter.\
# See here for an introduction to Perl regexps: https://perldoc.perl.org/perlretut
# Please note: ATM, only /.../ will be recognised as a regexp after ::,\
# i. e. neither other delimiters, nor modifiers may be used, so e. g. m#...#a will
# not be recognised.\
# If there is a non-option argument after the option with optional argument, it will
# be considered to be the optional argument only if it matches the regexp.\
# To be clear: ::/.../ is not meant for argument validation but solely to discriminate
# between arguments for options with optional argument and non-option arguments.
function getopts-plus
{
local optstring=$1
local -n name=$2
shift 2
local optionalArgSuffixRE='::(?::[si]|/.*?/)?'
local optionalArgTypeCaptureRE=':::([si])|::(/.*?/)'
local opt_
local -A isOptWithOptionalArg
while read opt_; do
isOptWithOptionalArg[$opt_]=1
done <<<$(perlGetCaptures "$optstring" "([a-zA-Z])$optionalArgSuffixRE")
local optLetters=$(perlGetCaptures "$optstring" "([a-zA-Z])(?:$optionalArgSuffixRE|:)?")
local optstringOrg=$optstring
optstring=$(perl -pe "s#$optionalArgSuffixRE##g" <<<$optstring)
getopts $optstring name "$#" || return
if ((isOptWithOptionalArg[$name])) && [[ ${!OPTIND} ]]; then
local nextArg=${!OPTIND} foundOpt=0
if [[ $nextArg == -* ]]; then
local i
for ((i = 1; i < ${#nextArg}; i++)); do
while read opt_; do
[[ ${nextArg:i:1} == $opt_ ]] && foundOpt=1 && break 2
done <<<$optLetters
done
((foundOpt)) && return
fi
local optArgType=$(perlGetCaptures "$optstringOrg" "$name(?:$optionalArgTypeCaptureRE)" '$1$2')
local nextArgIsOptArg=0
case $optArgType in
/*/)
perlMatch "$nextArg" "$optArgType" && nextArgIsOptArg=1
;;
[si])
local nextArgIsInt=0
[[ $nextArg =~ ^[0-9]+$ ]] && nextArgIsInt=1
{ [[ $optArgType == i ]] && ((nextArgIsInt)) || { [[ $optArgType == s ]] && ((! nextArgIsInt)); }; } && nextArgIsOptArg=1
;;
'')
nextArgIsOptArg=1
;;
esac
if ((nextArgIsOptArg)); then
OPTARG=$nextArg && ((OPTIND++))
fi
fi
}
# Uses perl to match \<string\> against \<regexp\>.\
# Returns with code 0 on a match and 1 otherwise.
function perlMatch # Args: <string> <regexp>
{
perl -e 'q('"$1"') =~ '"$2"' and exit 0; exit 1;'
}
# Uses perl to match \<string\> against \<regexp\>
# and prints each capture on a separate line.\
# If \<regexp\> contains more than one capture group,
# you must specify the \<line format\> which is an
# arbitrary Perl string containing your desired backrefs.\
# By default, merely $1 will be printed.
function perlGetCaptures # Args: <string> <regexp> [<line format>]
{
local lineFmt=${3:-\$1}
perl -e 'while (q('"$1"') =~ m#'"$2"'#g) { print(qq('"$lineFmt"') . "\n"); }'
}
Some tests using the latest version:
Optional arg type of -g specified as integer, no int passed but followed by a non-option string arg.
$ . ./getopts-plus.sh
$ while getopts-plus 'b:c::de::f::g:::ia' opt -ab 99 -c 11 -def 55 -g "hello you"; do e opt OPTARG; echo; printf "%.0s-" $(seq 1 25); echo -e "\n"; done
opt == 'a'
OPTARG == ''
-------------------------
opt == 'b'
OPTARG == '99'
-------------------------
opt == 'c'
OPTARG == '11'
-------------------------
opt == 'd'
OPTARG == ''
-------------------------
opt == 'e'
OPTARG == ''
-------------------------
opt == 'f'
OPTARG == '55'
-------------------------
opt == 'g'
OPTARG == '' <-- Empty because "hello you" is not an int
Like above, but with int arg.
$ OPTIND=1
$ while getopts-plus 'b:c::de::f::g:::ia' opt -ab 99 -c 11 -def 55 -g 7 "hello you"; do e opt OPTARG; echo; printf "%.0s-" $(seq 1 25); echo -e "\n"; done
opt == 'a'
OPTARG == ''
-------------------------
opt == 'b'
OPTARG == '99'
-------------------------
opt == 'c'
OPTARG == '11'
-------------------------
opt == 'd'
OPTARG == ''
-------------------------
opt == 'e'
OPTARG == ''
-------------------------
opt == 'f'
OPTARG == '55'
-------------------------
opt == 'g'
OPTARG == '7' <-- The passed int
Added optional option -h with regexp /^(a|b|ab|ba)$/, no arg passed.
$ OPTIND=1
$ while getopts-plus 'b:c::de::f::g:::ih::/^(a|b|ab|ba)$/a' opt -ab 99 -c 11 -def 55 -gh "hello you"; do e opt OPTARG; echo; printf "%.0s-" $(seq 1 25); echo -e "\n"; done
opt == 'a'
OPTARG == ''
-------------------------
opt == 'b'
OPTARG == '99'
-------------------------
opt == 'c'
OPTARG == '11'
-------------------------
opt == 'd'
OPTARG == ''
-------------------------
opt == 'e'
OPTARG == ''
-------------------------
opt == 'f'
OPTARG == '55'
-------------------------
opt == 'g'
OPTARG == ''
-------------------------
opt == 'h'
OPTARG == '' <-- Empty because "hello you" does not match the regexp
Like above, but with an arg matching the regexp.
$ OPTIND=1
$ while getopts-plus 'b:c::de::f::g:::ih::/^(a|b|ab|ba)$/a' opt -ab 99 -c 11 -def 55 -gh ab "hello you"; do e opt OPTARG; echo; printf "%.0s-" $(seq 1 25); echo -e "\n"; done
opt == 'a'
OPTARG == ''
-------------------------
opt == 'b'
OPTARG == '99'
-------------------------
opt == 'c'
OPTARG == '11'
-------------------------
opt == 'd'
OPTARG == ''
-------------------------
opt == 'e'
OPTARG == ''
-------------------------
opt == 'f'
OPTARG == '55'
-------------------------
opt == 'g'
OPTARG == ''
-------------------------
opt == 'h'
OPTARG == 'ab' <-- The arg that matches the regexp
Added another regexp-typed optional option -i with regexp /^\w+$/ (using the Perl token \w which means alphanumeric or underscore), no arg passed.
$ OPTIND=1
$ while getopts-plus 'b:c::de::f::g:::ih::/^(a|b|ab|ba)$/ai::/^\w+$/' opt -ab 99 -c 11 -def 55 -gh ab -i "hello you"; do e opt OPTARG; echo; printf "%.0s-" $(seq 1 25); echo -e "\n"; done
[23:10:49]
opt == 'a'
OPTARG == ''
-------------------------
opt == 'b'
OPTARG == '99'
-------------------------
opt == 'c'
OPTARG == '11'
-------------------------
opt == 'd'
OPTARG == ''
-------------------------
opt == 'e'
OPTARG == ''
-------------------------
opt == 'f'
OPTARG == '55'
-------------------------
opt == 'g'
OPTARG == ''
-------------------------
opt == 'h'
OPTARG == 'ab'
-------------------------
opt == 'i'
OPTARG == '' <-- Empty because "hello you" contains a space.
Like above, but with an arg matching the regexp.
$ OPTIND=1
$ while getopts-plus 'b:c::de::f::g:::ih::/^(a|b|ab|ba)$/ai::/^\w+$/' opt -ab 99 -c 11 -def 55 -gh ab -i foo_Bar_1 "hello you"; do e opt OPTARG; echo; printf "%.0s-" $(seq 1 25); echo -e "\n"; done
[23:15:23]
opt == 'a'
OPTARG == ''
-------------------------
opt == 'b'
OPTARG == '99'
-------------------------
opt == 'c'
OPTARG == '11'
-------------------------
opt == 'd'
OPTARG == ''
-------------------------
opt == 'e'
OPTARG == ''
-------------------------
opt == 'f'
OPTARG == '55'
-------------------------
opt == 'g'
OPTARG == ''
-------------------------
opt == 'h'
OPTARG == 'ab'
-------------------------
opt == 'i'
OPTARG == 'foo_Bar_1' <-- Matched because it contains only alphanumeric chars and underscores.

The following code solves this problem by checking for a leading dash and if found decrements OPTIND to point back to the skipped option for processing. This generally works fine except that you do not know the order the user will place options on the command line - if your optional argument option is last and does not provide an argument getopts will want to error out.
To fix the problem of the final argument missing, the "$#" array simply has an empty string "$# " appended so that getopts will be satisfied that it has gobbled up yet another option argument. To fix this new empty argument a variable is set that holds the total count of all options to be processed - when the last option is being processed a helper function called trim is called and removes the empty string prior to the value being utilized.
This is not working code, it has only place holders but you can easily modify it and with a little bit of care it can be useful to build a robust system.
#!/usr/bin/env bash
declare -r CHECK_FLOAT="%f"
declare -r CHECK_INTEGER="%i"
## <arg 1> Number - Number to check
## <arg 2> String - Number type to check
## <arg 3> String - Error message
function check_number() {
local NUMBER="${1}"
local NUMBER_TYPE="${2}"
local ERROR_MESG="${3}"
local FILTERED_NUMBER=$(sed 's/[^.e0-9+\^]//g' <<< "${NUMBER}")
local -i PASS=1
local -i FAIL=0
if [[ -z "${NUMBER}" ]]; then
echo "Empty number argument passed to check_number()." 1>&2
echo "${ERROR_MESG}" 1>&2
echo "${FAIL}"
elif [[ -z "${NUMBER_TYPE}" ]]; then
echo "Empty number type argument passed to check_number()." 1>&2
echo "${ERROR_MESG}" 1>&2
echo "${FAIL}"
elif [[ ! "${#NUMBER}" -eq "${#FILTERED_NUMBER}" ]]; then
echo "Non numeric characters found in number argument passed to check_number()." 1>&2
echo "${ERROR_MESG}" 1>&2
echo "${FAIL}"
else
case "${NUMBER_TYPE}" in
"${CHECK_FLOAT}")
if ((! $(printf "${CHECK_FLOAT}" "${NUMBER}" &>/dev/random;echo $?))); then
echo "${PASS}"
else
echo "${ERROR_MESG}" 1>&2
echo "${FAIL}"
fi
;;
"${CHECK_INTEGER}")
if ((! $(printf "${CHECK_INTEGER}" "${NUMBER}" &>/dev/random;echo $?))); then
echo "${PASS}"
else
echo "${ERROR_MESG}" 1>&2
echo "${FAIL}"
fi
;;
*)
echo "Invalid number type format: ${NUMBER_TYPE} to check_number()." 1>&2
echo "${FAIL}"
;;
esac
fi
}
## Note: Number can be any printf acceptable format and includes leading quotes and quotations,
## and anything else that corresponds to the POSIX specification.
## E.g. "'1e+03" is valid POSIX float format, see http://mywiki.wooledge.org/BashFAQ/054
## <arg 1> Number - Number to print
## <arg 2> String - Number type to print
function print_number() {
local NUMBER="${1}"
local NUMBER_TYPE="${2}"
case "${NUMBER_TYPE}" in
"${CHECK_FLOAT}")
printf "${CHECK_FLOAT}" "${NUMBER}" || echo "Error printing Float in print_number()." 1>&2
;;
"${CHECK_INTEGER}")
printf "${CHECK_INTEGER}" "${NUMBER}" || echo "Error printing Integer in print_number()." 1>&2
;;
*)
echo "Invalid number type format: ${NUMBER_TYPE} to print_number()." 1>&2
;;
esac
}
## <arg 1> String - String to trim single ending whitespace from
function trim_string() {
local STRING="${1}"
echo -En $(sed 's/ $//' <<< "${STRING}") || echo "Error in trim_string() expected a sensible string, found: ${STRING}" 1>&2
}
## This a hack for getopts because getopts does not support optional
## arguments very intuitively. E.g. Regardless of whether the values
## begin with a dash, getopts presumes that anything following an
## option that takes an option argument is the option argument. To fix
## this the index variable OPTIND is decremented so it points back to
## the otherwise skipped value in the array option argument. This works
## except for when the missing argument is on the end of the list,
## in this case getopts will not have anything to gobble as an
## argument to the option and will want to error out. To avoid this an
## empty string is appended to the argument array, yet in so doing
## care must be taken to manage this added empty string appropriately.
## As a result any option that doesn't exit at the time its processed
## needs to be made to accept an argument, otherwise you will never
## know if the option will be the last option sent thus having an empty
## string attached and causing it to land in the default handler.
function process_options() {
local OPTIND OPTERR=0 OPTARG OPTION h d r s M R S D
local ERROR_MSG=""
local OPTION_VAL=""
local EXIT_VALUE=0
local -i NUM_OPTIONS
let NUM_OPTIONS=${##}+1
while getopts “:h?d:DM:R:S:s:r:” OPTION "$#";
do
case "$OPTION" in
h)
help | more
exit 0
;;
r)
OPTION_VAL=$(((${NUM_OPTIONS}==${OPTIND})) && trim_string "${OPTARG##*=}" || echo -En "${OPTARG##*=}")
ERROR_MSG="Invalid input: Integer or floating point number required."
if [[ -z "${OPTION_VAL}" ]]; then
## can set global flags here
:;
elif [[ "${OPTION_VAL}" =~ ^-. ]]; then
let OPTIND=${OPTIND}-1
## can set global flags here
elif [ "${OPTION_VAL}" = "0" ]; then
## can set global flags here
:;
elif (($(check_number "${OPTION_VAL}" "${CHECK_FLOAT}" "${ERROR_MSG}"))); then
:; ## do something really useful here..
else
echo "${ERROR_MSG}" 1>&2 && exit -1
fi
;;
d)
OPTION_VAL=$(((${NUM_OPTIONS}==${OPTIND})) && trim_string "${OPTARG##*=}" || echo -En "${OPTARG##*=}")
[[ ! -z "${OPTION_VAL}" && "${OPTION_VAL}" =~ ^-. ]] && let OPTIND=${OPTIND}-1
DEBUGMODE=1
set -xuo pipefail
;;
s)
OPTION_VAL=$(((${NUM_OPTIONS}==${OPTIND})) && trim_string "${OPTARG##*=}" || echo -En "${OPTARG##*=}")
if [[ ! -z "${OPTION_VAL}" && "${OPTION_VAL}" =~ ^-. ]]; then ## if you want a variable value that begins with a dash, escape it
let OPTIND=${OPTIND}-1
else
GLOBAL_SCRIPT_VAR="${OPTION_VAL}"
:; ## do more important things
fi
;;
M)
OPTION_VAL=$(((${NUM_OPTIONS}==${OPTIND})) && trim_string "${OPTARG##*=}" || echo -En "${OPTARG##*=}")
ERROR_MSG=$(echo "Error - Invalid input: ${OPTION_VAL}, Integer required"\
"retry with an appropriate option argument.")
if [[ -z "${OPTION_VAL}" ]]; then
echo "${ERROR_MSG}" 1>&2 && exit -1
elif [[ "${OPTION_VAL}" =~ ^-. ]]; then
let OPTIND=${OPTIND}-1
echo "${ERROR_MSG}" 1>&2 && exit -1
elif (($(check_number "${OPTION_VAL}" "${CHECK_INTEGER}" "${ERROR_MSG}"))); then
:; ## do something useful here
else
echo "${ERROR_MSG}" 1>&2 && exit -1
fi
;;
R)
OPTION_VAL=$(((${NUM_OPTIONS}==${OPTIND})) && trim_string "${OPTARG##*=}" || echo -En "${OPTARG##*=}")
ERROR_MSG=$(echo "Error - Invalid option argument: ${OPTION_VAL},"\
"the value supplied to -R is expected to be a "\
"qualified path to a random character device.")
if [[ -z "${OPTION_VAL}" ]]; then
echo "${ERROR_MSG}" 1>&2 && exit -1
elif [[ "${OPTION_VAL}" =~ ^-. ]]; then
let OPTIND=${OPTIND}-1
echo "${ERROR_MSG}" 1>&2 && exit -1
elif [[ -c "${OPTION_VAL}" ]]; then
:; ## Instead of erroring do something useful here..
else
echo "${ERROR_MSG}" 1>&2 && exit -1
fi
;;
S)
STATEMENT=$(((${NUM_OPTIONS}==${OPTIND})) && trim_string "${OPTARG##*=}" || echo -En "${OPTARG##*=}")
ERROR_MSG="Error - Default text string to set cannot be empty."
if [[ -z "${STATEMENT}" ]]; then
## Instead of erroring you could set a flag or do something else with your code here..
elif [[ "${STATEMENT}" =~ ^-. ]]; then ## if you want a statement that begins with a dash, escape it
let OPTIND=${OPTIND}-1
echo "${ERROR_MSG}" 1>&2 && exit -1
echo "${ERROR_MSG}" 1>&2 && exit -1
else
:; ## do something even more useful here you can modify the above as well
fi
;;
D)
## Do something useful as long as it is an exit, it is okay to not worry about the option arguments
exit 0
;;
*)
EXIT_VALUE=-1
;&
?)
usage
exit ${EXIT_VALUE}
;;
esac
done
}
process_options "$# " ## extra space, so getopts can find arguments

Try:
while getopts "hd:R:" arg; do
case $arg in
h)
echo "usage"
;;
d)
dir=$OPTARG
;;
R)
if [[ $OPTARG =~ ^[0-9]+$ ]];then
level=$OPTARG
elif [[ $OPTARG =~ ^-. ]];then
level=1
let OPTIND=$OPTIND-1
else
level=1
fi
;;
\?)
echo "WRONG" >&2
;;
esac
done
I think the above code will work for your purposes while still using getopts. I've added the following three lines to your code when getopts encounters -R:
elif [[ $OPTARG =~ ^-. ]];then
level=1
let OPTIND=$OPTIND-1
If -R is encountered and the first argument looks like another getopts parameter, level is set to the default value of 1, and then the $OPTIND variable is reduced by one. The next time getopts goes to grab an argument, it will grab the correct argument instead of skipping it.
Here is similar example based on the code from Jan Schampera's comment at this tutorial:
#!/bin/bash
while getopts :abc: opt; do
case $opt in
a)
echo "option a"
;;
b)
echo "option b"
;;
c)
echo "option c"
if [[ $OPTARG = -* ]]; then
((OPTIND--))
continue
fi
echo "(c) argument $OPTARG"
;;
\?)
echo "WTF!"
exit 1
;;
esac
done
When you discover that OPTARG von -c is something beginning with a hyphen, then reset OPTIND and re-run getopts (continue the while loop). Oh, of course, this isn't perfect and needs some more robustness. It's just an example.

You can always decide to differentiate the option with lowercase or uppercase.
However my idea is to call getopts twice and 1st time parse without arguments ignoring them (R) then 2nd time parse only that option with argument support (R:). The only trick is that OPTIND (index) needs to be changed during processing, as it keeps pointer to the current argument.
Here is the code:
#!/usr/bin/env bash
while getopts ":hd:R" arg; do
case $arg in
d) # Set directory, e.g. -d /foo
dir=$OPTARG
;;
R) # Optional level value, e.g. -R 123
OI=$OPTIND # Backup old value.
((OPTIND--)) # Decrease argument index, to parse -R again.
while getopts ":R:" r; do
case $r in
R)
# Check if value is in numeric format.
if [[ $OPTARG =~ ^[0-9]+$ ]]; then
level=$OPTARG
else
level=1
fi
;;
:)
# Missing -R value.
level=1
;;
esac
done
[ -z "$level" ] && level=1 # If value not found, set to 1.
OPTIND=$OI # Restore old value.
;;
\? | h | *) # Display help.
echo "$0 usage:" && grep " .)\ #" $0
exit 0
;;
esac
done
echo Dir: $dir
echo Level: $level
Here are few tests for scenarios which works:
$ ./getopts.sh -h
./getopts.sh usage:
d) # Set directory, e.g. -d /foo
R) # Optional level value, e.g. -R 123
\? | h | *) # Display help.
$ ./getopts.sh -d /foo
Dir: /foo
Level:
$ ./getopts.sh -d /foo -R
Dir: /foo
Level: 1
$ ./getopts.sh -d /foo -R 123
Dir: /foo
Level: 123
$ ./getopts.sh -d /foo -R wtf
Dir: /foo
Level: 1
$ ./getopts.sh -R -d /foo
Dir: /foo
Level: 1
Scenarios which doesn't work (so the code needs a bit of more tweaks):
$ ./getopts.sh -R 123 -d /foo
Dir:
Level: 123
More information about getopts usage can be found in man bash.
See also: Small getopts tutorial at Bash Hackers Wiki

I just ran into this myself and felt that none of the existing solutions were really clean. After working on it a bit and trying various things, I found that leveraging getopts SILENT mode with :) ... appears to have done the trick along with keeping OPTIND in sync.
usage: test.sh [-abst] [-r [DEPTH]] filename
*NOTE: -r (recursive) with no depth given means full recursion
#!/usr/bin/env bash
depth='-d 1'
while getopts ':abr:st' opt; do
case "${opt}" in
a) echo a;;
b) echo b;;
r) if [[ "${OPTARG}" =~ ^[0-9]+$ ]]; then
depth="-d ${OPTARG}"
else
depth=
(( OPTIND-- ))
fi
;;
s) echo s;;
t) echo t;;
:) [[ "${OPTARG}" = 'r' ]] && depth=;;
*) echo >&2 "Invalid option: ${opt}"; exit 1;;
esac
done
shift $(( OPTIND - 1 ))
filename="$1"
...

Old thread, but figured I'd share what I did anyway (which is also mostly older than this thread). I got fed up with trying to make getopt do what I wanted, and in a fit of frustration did this so that both short and long opts with optional arguments are supported. It's the long long way, somebody is sure to laugh, but it works exactly how I want it to - overly commented examples of all three cases are below:
#!/usr/bin/bash
# Begin testme.sh
shopt -s extglob;
VERSION="1.0"
function get_args(){
while test -n "${1}" ; do
case "${1}" in
-a | --all)
# dumb single argument example
PROCESS_ALL="yes"
shift 1
;;
-b | --buildnum)
# requires a second argument so use check_arg() below
check_arg $1 $2
BUILD_NUM=${2}
shift 2
;;
-c | --cache)
# Example where argument is not required, don't use check_arg()
if [ echo ${2} | grep -q "^-" ]; then
# no argument given, use default cache value
CACHEDIR=~/mycachedir
# Note: this could have been set upon entering the script
# and used the negative above as well
shift 1
else
cache=${2}
shift 2
fi
;;
-h | --help)
showhelp
exit 0
;;
-v | --version)
echo -e "$(basename ${0}) ${VERSION}\n"
exit 0
;;
# Handle getopt style short args (reason for shopts above)
-+([a-z,A-Z]))
# split up the arguments and call recursively with trailing break
arg="${1}"
newargs=$( echo ${1} | sed 's#-##' | \
sed 's/.\{1\}/& /g' | \
sed 's/[^ ]* */-&/g')
newargs="${newargs} $(echo ${#} | sed "s#${arg}##")"
get_args ${newargs}
break;
;;
*)
echo -e "Invalid argument ${1}!\n\n"
showhelp
exit 1
;;
esac
done
}
# Super lazy, but I didn't want an if/then/else for every required arg
function check_arg(){
if [ echo "${2}" | grep -q "^-" ]; then
echo "Error: $1 requires a valid argument."
exit 1
fi
}
function showhelp(){
echo ""
echo "`basename ${0}` is a utility to..."
}
# Process command line arguments
get_args $#
...
# End testme.sh
I've never run into it, but I suppose there might be a case where I'd need the second argument to begin with a '-' character, in which case, I'd strip it out before calling get_args(). I have used with with fixed position arguments as well, and in that case they are at the end, but same solution. Also, I suppose a portable version could just handle the combined short args in *), but I figure if bash is too heavy a requirement, the you are on your own.

Just use /usr/bin/getopt, it is much better. It is compliant with libc getopt, so you'll get optional argument with ::. Check man getopt.
getopt bash template:
#!/bin/bash
optstring_long="foo,bar:,baz::,verbose,dry-run,help"
optstring_short="fb:z::vnh"
opts=$(getopt -o "${optstring_short}" --long "${optstring_long}" --name "$0" -- "$#") ||
exit $?
eval set -- "$opts"
unset foo
unset bar
unset baz
unset verbose_on
unset verbose_off
unset verbose
unset dryrun
while true
do
case "$1" in
-f|--foo)
foo=true
echo "Foo!"
shift;;
-b|--bar)
bar=$2
echo "Bar: ${bar}"
shift 2;;
-z|--baz)
baz=${2:-default!}
echo "Baz: ${baz}"
shift 2;;
-v|--verbose)
verbose_on="set -x"
verbose_off="{ set +x; } 2>/dev/null"
verbose=verbose_run
shift;;
-n|--dry-run)
dryrun=echo
shift;;
-h|--help)
cat <<EOF
This is help!
EOF
exit;;
--) shift; break;;
esac
done
verbose_run() { set -x; eval "$#"; { set +x; } 2>/dev/null; }
Note that for optional argument it must be supplied without space (same like with getopts trick):
$ ./a -b 1 -z -f
Bar: 1
Baz: default!
Foo!
$ ./a -b 2 -z bad -f
Bar: 2
Baz: default!
Foo!
$ ./a -b 2 -zgood -f
Bar: 2
Baz: good
Foo!
Check more bash tricks here.

while getopts "hd:R" arg; do
case $arg in
h) echo "usage" ;;
d) dir=$OPTARG ;;
R)
if [[ ${!OPTIND} =~ ^[0-9]+$ ]]; then
eval level=\$$(( OPTIND++ ))
else
level=1
fi
;;
*) echo "WRONG" >&2 ;;
esac
done
if (( OPTIND != ($# + 1) )); then
echo "WRONG" >&2
fi
This is similar to #calandoa's answer, but is a little more concise. Also, it still verifies the argument is a decimal integer, as was done in the original question. I also added the important check that all the arguments were consumed by the while loop. Supporting both optional option arguments and non-option arguments would be more difficult.

Related

Bash script using getopts to store strings as an array

I am working on a Bash script that needs to take zero to multiple strings as an input but I am unsure how to do this because of the lack of a flag before the list.
The script usage:
script [ list ] [ -t <secs> ] [ -n <count> ]
The list takes zero, one, or multiple strings as input. When a space is encountered, that acts as the break between the strings in a case of two or more. These strings will eventually be input for a grep command, so my idea is to save them in an array of some kind. I currently have the -t and -n working correctly. I have tried looking up examples but have been unable to find anything that is similar to what I want to do. My other concern is how to ignore string input after a flag is set so no other strings are accepted.
My current script:
while getopts :t:n: arg; do
case ${arg} in
t)
seconds=${OPTARG}
if ! [[ $seconds =~ ^[1-9][0-9]*$ ]] ; then
exit
fi
;;
n)
count=${OPTARG}
if ! [[ $count =~ ^[1-9][0-9]*$ ]] ; then
exit
fi
;;
:)
echo "$0: Must supply an argument to -$OPTARG" >&2
exit
;;
?)
echo "Invalid option: -${OPTARG}"
exit
;;
esac
done
Edit: This is for a homework assignment and am unsure if the order of arguments can change
Edit 2: Options can be in any order
Would you please try the following:
#!/bin/bash
# parse the arguments before getopts
for i in "$#"; do
if [[ $i = "-"* ]]; then
break
else # append the arguments to "list" as long as it does not start with "-"
list+=("$1")
shift
fi
done
while getopts :t:n: arg; do
: your "case" code here
done
# see if the variables are properly assigned
echo "seconds=$seconds" "count=$count"
echo "list=${list[#]}"
Try:
#! /bin/bash -p
# Set defaults
count=10
seconds=20
args=( "$#" )
end_idx=$(($#-1))
# Check for '-n' option at the end
if [[ end_idx -gt 0 && ${args[end_idx-1]} == -n ]]; then
count=${args[end_idx]}
end_idx=$((end_idx-2))
fi
# Check for '-t' option at the (possibly new) end
if [[ end_idx -gt 0 && ${args[end_idx-1]} == -t ]]; then
seconds=${args[end_idx]}
end_idx=$((end_idx-2))
fi
# Take remaining arguments up to the (possibly new) end as the list of strings
strings=( "${args[#]:0:end_idx+1}" )
declare -p strings seconds count
The basic idea is to process the arguments right-to-left instead of left-to-right.
The code assumes that the only acceptable order of arguments is the one given in the question. In particular, it assumes that the -t and -n options must be at the end if they are present, and they must be in that order if both are present.
It makes no attempt to handle option arguments combined with options (e.g. -t5 instead of -t 5). That could be done fairly easily if required.
It's OK for strings in the list to begin with -.
My shorter version
Some remarks:
Instead of loop over all argument**, then break if argument begin by -, I simply use a while loop.
From How do I test if a variable is a number in Bash?, added efficient is_int test function
As any output (echo) done in while getopts ... loop would be an error, redirection do STDERR (>&2) could be addressed to the whole loop instead of repeated on each echo line.
** Note doing a loop over all argument could be written for varname ;do. as $# stand for default arguments, in "$#" are implicit in for loop.
#!/bin/bash
is_int() { case ${1#[-+]} in
'' | *[!0-9]* ) echo "Argument '$1' is not a number"; exit 3;;
esac ;}
while [[ ${1%%-*} ]];do
args+=("$1")
shift
done
while getopts :t:n: arg; do
case ${arg} in
t ) is_int "${OPTARG}" ; seconds=${OPTARG} ;;
n ) is_int "${OPTARG}" ; count=${OPTARG} ;;
: ) echo "$0: Must supply an argument to -$OPTARG" ; exit 2;;
? ) echo "Invalid option: -${OPTARG}" ; exit 1;;
esac
done >&2
declare -p seconds count args
Standard practice is to place option arguments before any non-option arguments or variable arguments.
getopts natively recognizes -- as the end of option switches delimiter.
If you need to pass arguments that starts with a dash -, you use the -- delimiter, so getopts stops trying to intercept option arguments.
Here is an implementation:
#!/usr/bin/env bash
# SYNOPSIS
# script [-t<secs>] [-n<count>] [string]...
# Counter of option arguments
declare -i opt_arg_count=0
while getopts :t:n: arg; do
case ${arg} in
t)
seconds=${OPTARG}
if ! [[ $seconds =~ ^[1-9][0-9]*$ ]] ; then
exit
fi
opt_arg_count+=1
;;
n)
count=${OPTARG}
if ! [[ $count =~ ^[1-9][0-9]*$ ]] ; then
exit 1
fi
opt_arg_count+=1
;;
?)
printf 'Invalid option: -%s\n' "${OPTARG}" >&2
exit 1
;;
esac
done
shift "$opt_arg_count" # Skip all option arguments
[[ "$1" == -- ]] && shift # Skip option argument delimiter if any
# Variable arguments strings are all remaining arguments
strings=("$#")
declare -p count seconds strings
Example usages
With strings not starting with a dash:
$ ./script -t45 -n10 foo bar baz qux
declare -- count="10"
declare -- seconds="45"
declare -a strings=([0]="foo" [1]="bar" [2]="baz" [3]="qux")
With string starting with a dash, need -- delimiter:
$ ./script -t45 -n10 -- '-dashed string' foo bar baz qux
declare -- count="10"
declare -- seconds="45"
declare -a strings=([0]="-dashed string" [1]="foo" [2]="bar" [3]="baz" [4]="qux")

shift and getopts [duplicate]

while getopts "hd:R:" arg; do
case $arg in
h)
echo "usage"
;;
d)
dir=$OPTARG
;;
R)
if [[ $OPTARG =~ ^[0-9]+$ ]];then
level=$OPTARG
else
level=1
fi
;;
\?)
echo "WRONG" >&2
;;
esac
done
level refers to the parameter of -R, dir refers to parameters of -d
when I input ./count.sh -R 1 -d test/ it works correctly
when I input ./count.sh -d test/ -R 1 it works correctly
but I want to have it work when I input ./count.sh -d test/ -R or ./count.sh -R -d test/
This means that I want -R to have a default value and for the sequence of commands to be more flexible.
This workaround defines 'R' with no argument (no ':'), tests for any argument after the '-R' (manage last option on the command line) and tests if an existing argument starts with a dash.
# No : after R
while getopts "hd:R" arg; do
case $arg in
(...)
R)
# Check next positional parameter
eval nextopt=\${$OPTIND}
# existing or starting with dash?
if [[ -n $nextopt && $nextopt != -* ]] ; then
OPTIND=$((OPTIND + 1))
level=$nextopt
else
level=1
fi
;;
(...)
esac
done
getopts doesn't really support this; but it's not hard to write your own replacement.
while true; do
case $1 in
-R) level=1
shift
case $1 in
*[!0-9]* | "") ;;
*) level=$1; shift ;;
esac ;;
# ... Other options ...
-*) echo "$0: Unrecognized option $1" >&2
exit 2;;
*) break ;;
esac
done
Wrong. Actually getopts does support optional arguments! From the bash man page:
If a required argument is not found, and getopts is not silent,
a question mark (?) is placed in name, OPTARG is unset, and a diagnostic
message is printed. If getopts is silent, then a colon (:) is placed in name
and OPTARG is set to the option character found.
When the man page says "silent" it means silent error reporting. To enable it, the first character of optstring needs to be a colon:
while getopts ":hd:R:" arg; do
# ...rest of iverson's loop should work as posted
done
Since Bash's getopt does not recognize -- to end the options list, it may not work when -R is the last option, followed by some path argument.
P.S.: Traditionally, getopt.c uses two colons (::) to specify an optional argument. However, the version used by Bash doesn't.
I agree with tripleee, getopts does not support optional argument handling.
The compromised solution I have settled on is to use the upper case/lower case combination of the same option flag to differentiate between the option that takes an argument and the other that does not.
Example:
COMMAND_LINE_OPTIONS_HELP='
Command line options:
-I Process all the files in the default dir: '`pwd`'/input/
-i DIR Process all the files in the user specified input dir
-h Print this help menu
Examples:
Process all files in the default input dir
'`basename $0`' -I
Process all files in the user specified input dir
'`basename $0`' -i ~/my/input/dir
'
VALID_COMMAND_LINE_OPTIONS="i:Ih"
INPUT_DIR=
while getopts $VALID_COMMAND_LINE_OPTIONS options; do
#echo "option is " $options
case $options in
h)
echo "$COMMAND_LINE_OPTIONS_HELP"
exit $E_OPTERROR;
;;
I)
INPUT_DIR=`pwd`/input
echo ""
echo "***************************"
echo "Use DEFAULT input dir : $INPUT_DIR"
echo "***************************"
;;
i)
INPUT_DIR=$OPTARG
echo ""
echo "***************************"
echo "Use USER SPECIFIED input dir : $INPUT_DIR"
echo "***************************"
;;
\?)
echo "Usage: `basename $0` -h for help";
echo "$COMMAND_LINE_OPTIONS_HELP"
exit $E_OPTERROR;
;;
esac
done
This is actually pretty easy. Just drop the trailing colon after the R and use OPTIND
while getopts "hRd:" opt; do
case $opt in
h) echo -e $USAGE && exit
;;
d) DIR="$OPTARG"
;;
R)
if [[ ${#:$OPTIND} =~ ^[0-9]+$ ]];then
LEVEL=${#:$OPTIND}
OPTIND=$((OPTIND+1))
else
LEVEL=1
fi
;;
\?) echo "Invalid option -$OPTARG" >&2
;;
esac
done
echo $LEVEL $DIR
count.sh -d test
test
count.sh -d test -R
1 test
count.sh -R -d test
1 test
count.sh -d test -R 2
2 test
count.sh -R 2 -d test
2 test
Inspired in #calandoa's answer (the only one that actually works!), I've made a simple function that can make it easy to be used multiple times.
getopts_get_optional_argument() {
eval next_token=\${$OPTIND}
if [[ -n $next_token && $next_token != -* ]]; then
OPTIND=$((OPTIND + 1))
OPTARG=$next_token
else
OPTARG=""
fi
}
An example usage:
while getopts "hdR" option; do
case $option in
d)
getopts_get_optional_argument $#
dir=${OPTARG}
;;
R)
getopts_get_optional_argument $#
level=${OPTARG:-1}
;;
h)
show_usage && exit 0
;;
\?)
show_usage && exit 1
;;
esac
done
This gives us a practical way to get "that missing feature" in getopts :)
NOTE that nevertheless command-line options with optional args seems to be discouraged explicitly
Guideline 7: Option-arguments should not be optional.
but I have no intuitive way to implement my case without this: I have 2 modes that are activated by either using one flag or other, and those have both an argument with a clear default. Introducing a third flag just to disambiguate makes it look a bad CLI style.
I've tested this with many combinations, including all in #aaron-sua's answer and works well.
I think there are two way.
First is calandoa's answer, Using OPTIND and no silent mode.
Second is Using OPTIND and silent mode.
while getopts ":Rr:" name; do
case ${name} in
R)
eval nextArg=\${$OPTIND}
# check option followed by nothing or other option.
if [[ -z ${nextArg} || $nextArg =~ ^-.* ]]; then
level=1
elif [[ $nextArg =~ ^[0-9]+$ ]]; then
level=$nextArg
OPTIND=$((OPTIND + 1))
else
level=1
fi
;;
r)
# check option followed by other option.
if [[ $OPTARG =~ ^-.* ]]; then
OPTIND=$((OPTIND - 1))
level2=2
elif [[ $OPTARG =~ ^[0-9]+$ ]]; then
level2="$OPTARG"
else
level2=2
fi
;;
:)
# check no argument
case $OPTARG in
r)
level2=2
;;
esac
esac
done
echo "Level 1 : $level"
echo "Level 2 : $level2"
All solutions presented so far put code in case ... in ... esac, but in my opinion, it would be much more natural to have a modified getopts command, thus I wrote this function:
EDIT:
Now, you can specify the type of an optional arg (see usage info).
Furthermore, instead of testing if $nextArg "looks like" an option(s) arg, the function now checks if $nextArg contains a letter from $optstring.
This way, an option letter not contained in $optstring can be used as optional arg, as with getopts' mandatory args.
Latest changes:
Fixed test if $nextArg is an option arg:
Test if $nextArg begins with a dash.
Without this test, optional args that contain a letter
from $optstring are not recognised as such.
Added regexp type specifier (see usage info).
Fixed: 0 not recognised as optional arg specified to be an int.
Simplified test if $nextArg is an int.
Type specifier ::/.../: Use perl to test if $nextArg matches the regexp.
This way, you benefit from (almost (*)) the full power of Perl regexps.
(*): See last paragraph of usage info.
Fixed: Doesn't work with more than one regexp type specifier:
Use perl instead of grep/sed constructs because non-greedy matching is needed.
Usage:
Invocation: getopts-plus optstring name "$#"
optstring: Like normal getopts, but you may specify options with optional argument by appending :: to the option letter.
However, if your script supports an invocation with an option with optional argument as the only option argument, followed by a non-option argument, the non-option argument will be considered to be the argument for the option.
If you're lucky and the optional argument is expected to be an integer, whereas the non-option argument is a string or vice versa, you may specify the type by appending :::i for an integer or :::s for a string to solve that issue.
If that doesn't apply, you may specify a Perl regexp for the optional arg by appending ::/.../ to the option letter.
See here for an introduction to Perl regexps: https://perldoc.perl.org/perlretut
Please note: ATM, only /.../ will be recognised as a regexp after ::, i. e. neither other delimiters, nor modifiers may be used, so e. g. m#...#a will not be recognised.
If there is a non-option argument after the option with optional argument, it will be considered to be the optional argument only if it matches the regexp.
To be clear: ::/.../ is not meant for argument validation but solely to discriminate between arguments for options with optional argument and non-option arguments.
#!/bin/bash
# Invocation: getopts-plus optstring name "$#"\
# \
# optstring: Like normal getopts, but you may specify options with optional argument
# by appending :: to the option letter.\
# \
# However, if your script supports an invocation with an option with optional
# argument as the only option argument, followed by a non-option argument,
# the non-option argument will be considered to be the argument for the option.\
# \
# If you're lucky and the optional argument is expected to be an integer, whereas
# the non-option argument is a string or vice versa, you may specify the type by
# appending :::i for an integer or :::s for a string to solve that issue.\
# \
# If that doesn't apply, you may specify a Perl regexp for the optional arg by appending
# ::/.../ to the option letter.\
# See here for an introduction to Perl regexps: https://perldoc.perl.org/perlretut
# Please note: ATM, only /.../ will be recognised as a regexp after ::,\
# i. e. neither other delimiters, nor modifiers may be used, so e. g. m#...#a will
# not be recognised.\
# If there is a non-option argument after the option with optional argument, it will
# be considered to be the optional argument only if it matches the regexp.\
# To be clear: ::/.../ is not meant for argument validation but solely to discriminate
# between arguments for options with optional argument and non-option arguments.
function getopts-plus
{
local optstring=$1
local -n name=$2
shift 2
local optionalArgSuffixRE='::(?::[si]|/.*?/)?'
local optionalArgTypeCaptureRE=':::([si])|::(/.*?/)'
# If we pass 'opt' for 'name' (as I always do when using getopts) and there is
# also a local variable 'opt', the "outer" 'opt' will always be empty.
# I don't understand why a local variable interferes with caller's variable with
# same name in this case; however, we can easily circumvent this.
local opt_
# Extract options with optional arg
local -A isOptWithOptionalArg
while read opt_; do
# Using an associative array as set
isOptWithOptionalArg[$opt_]=1
done <<<$(perlGetCaptures "$optstring" "([a-zA-Z])$optionalArgSuffixRE")
# Extract all option letters (used to weed out possible optional args that are option args)
local optLetters=$(perlGetCaptures "$optstring" "([a-zA-Z])(?:$optionalArgSuffixRE|:)?")
# Save original optstring, then remove our suffix(es)
local optstringOrg=$optstring
optstring=$(perl -pe "s#$optionalArgSuffixRE##g" <<<$optstring)
getopts $optstring name "$#" || return # Return value is getopts' exit value.
# If current option is an option with optional arg and if an arg has been provided,
# check if that arg is not an option and if it isn't, check if that arg matches(*)
# the specified type, if any, and if it does or no type has been specified,
# assign it to OPTARG and inc OPTIND.
#
# (*) We detect an int because it's easy, but we assume a string if it's not an int
# because detecting a string would be complicated.
# So it sounds strange to call it a match if we know that the optional arg is specified
# to be a string, but merely that the provided arg is not an int, but in this context,
# "not an int" is equivalent to "string". At least I think so, but I might be wrong.
if ((isOptWithOptionalArg[$name])) && [[ ${!OPTIND} ]]; then
local nextArg=${!OPTIND} foundOpt=0
# Test if $nextArg is an option arg
if [[ $nextArg == -* ]]; then
# Check if $nextArg contains a letter from $optLetters.
# This way, an option not contained in $optstring can be
# used as optional arg, as with getopts' mandatory args.
local i
# Start at char 1 to skip the leading dash
for ((i = 1; i < ${#nextArg}; i++)); do
while read opt_; do
[[ ${nextArg:i:1} == $opt_ ]] && foundOpt=1 && break 2
done <<<$optLetters
done
((foundOpt)) && return
fi
# Extract type of optional arg if specified
local optArgType=$(perlGetCaptures "$optstringOrg" "$name(?:$optionalArgTypeCaptureRE)" '$1$2')
local nextArgIsOptArg=0
case $optArgType in
/*/) # Check if $nextArg matches regexp
perlMatch "$nextArg" "$optArgType" && nextArgIsOptArg=1
;;
[si]) # Check if $nextArg is an int
local nextArgIsInt=0
[[ $nextArg =~ ^[0-9]+$ ]] && nextArgIsInt=1
# Test if specified type and arg type match (see (*) above).
# N.B.: We need command groups since && and || between commands have same precedence.
{ [[ $optArgType == i ]] && ((nextArgIsInt)) || { [[ $optArgType == s ]] && ((! nextArgIsInt)); }; } && nextArgIsOptArg=1
;;
'') # No type or regexp specified => Assume $nextArg is optional arg.
nextArgIsOptArg=1
;;
esac
if ((nextArgIsOptArg)); then
OPTARG=$nextArg && ((OPTIND++))
fi
fi
}
# Uses perl to match \<string\> against \<regexp\>.\
# Returns with code 0 on a match and 1 otherwise.
function perlMatch # Args: <string> <regexp>
{
perl -e 'q('"$1"') =~ '"$2"' and exit 0; exit 1;'
}
# Uses perl to match \<string\> against \<regexp\>
# and prints each capture on a separate line.\
# If \<regexp\> contains more than one capture group,
# you must specify the \<line format\> which is an
# arbitrary Perl string containing your desired backrefs.\
# By default, merely $1 will be printed.
function perlGetCaptures # Args: <string> <regexp> [<line format>]
{
local lineFmt=${3:-\$1}
# Matching repeatedly with g option gives one set of captures at a time.
perl -e 'while (q('"$1"') =~ m#'"$2"'#g) { print(qq('"$lineFmt"') . "\n"); }'
}
The same script without comments inside function bodies in case you don't need them:
#!/bin/bash
# Invocation: getopts-plus optstring name "$#"\
# \
# optstring: Like normal getopts, but you may specify options with optional argument
# by appending :: to the option letter.\
# \
# However, if your script supports an invocation with an option with optional
# argument as the only option argument, followed by a non-option argument,
# the non-option argument will be considered to be the argument for the option.\
# \
# If you're lucky and the optional argument is expected to be an integer, whereas
# the non-option argument is a string or vice versa, you may specify the type by
# appending :::i for an integer or :::s for a string to solve that issue.\
# \
# If that doesn't apply, you may specify a Perl regexp for the optional arg by appending
# ::/.../ to the option letter.\
# See here for an introduction to Perl regexps: https://perldoc.perl.org/perlretut
# Please note: ATM, only /.../ will be recognised as a regexp after ::,\
# i. e. neither other delimiters, nor modifiers may be used, so e. g. m#...#a will
# not be recognised.\
# If there is a non-option argument after the option with optional argument, it will
# be considered to be the optional argument only if it matches the regexp.\
# To be clear: ::/.../ is not meant for argument validation but solely to discriminate
# between arguments for options with optional argument and non-option arguments.
function getopts-plus
{
local optstring=$1
local -n name=$2
shift 2
local optionalArgSuffixRE='::(?::[si]|/.*?/)?'
local optionalArgTypeCaptureRE=':::([si])|::(/.*?/)'
local opt_
local -A isOptWithOptionalArg
while read opt_; do
isOptWithOptionalArg[$opt_]=1
done <<<$(perlGetCaptures "$optstring" "([a-zA-Z])$optionalArgSuffixRE")
local optLetters=$(perlGetCaptures "$optstring" "([a-zA-Z])(?:$optionalArgSuffixRE|:)?")
local optstringOrg=$optstring
optstring=$(perl -pe "s#$optionalArgSuffixRE##g" <<<$optstring)
getopts $optstring name "$#" || return
if ((isOptWithOptionalArg[$name])) && [[ ${!OPTIND} ]]; then
local nextArg=${!OPTIND} foundOpt=0
if [[ $nextArg == -* ]]; then
local i
for ((i = 1; i < ${#nextArg}; i++)); do
while read opt_; do
[[ ${nextArg:i:1} == $opt_ ]] && foundOpt=1 && break 2
done <<<$optLetters
done
((foundOpt)) && return
fi
local optArgType=$(perlGetCaptures "$optstringOrg" "$name(?:$optionalArgTypeCaptureRE)" '$1$2')
local nextArgIsOptArg=0
case $optArgType in
/*/)
perlMatch "$nextArg" "$optArgType" && nextArgIsOptArg=1
;;
[si])
local nextArgIsInt=0
[[ $nextArg =~ ^[0-9]+$ ]] && nextArgIsInt=1
{ [[ $optArgType == i ]] && ((nextArgIsInt)) || { [[ $optArgType == s ]] && ((! nextArgIsInt)); }; } && nextArgIsOptArg=1
;;
'')
nextArgIsOptArg=1
;;
esac
if ((nextArgIsOptArg)); then
OPTARG=$nextArg && ((OPTIND++))
fi
fi
}
# Uses perl to match \<string\> against \<regexp\>.\
# Returns with code 0 on a match and 1 otherwise.
function perlMatch # Args: <string> <regexp>
{
perl -e 'q('"$1"') =~ '"$2"' and exit 0; exit 1;'
}
# Uses perl to match \<string\> against \<regexp\>
# and prints each capture on a separate line.\
# If \<regexp\> contains more than one capture group,
# you must specify the \<line format\> which is an
# arbitrary Perl string containing your desired backrefs.\
# By default, merely $1 will be printed.
function perlGetCaptures # Args: <string> <regexp> [<line format>]
{
local lineFmt=${3:-\$1}
perl -e 'while (q('"$1"') =~ m#'"$2"'#g) { print(qq('"$lineFmt"') . "\n"); }'
}
Some tests using the latest version:
Optional arg type of -g specified as integer, no int passed but followed by a non-option string arg.
$ . ./getopts-plus.sh
$ while getopts-plus 'b:c::de::f::g:::ia' opt -ab 99 -c 11 -def 55 -g "hello you"; do e opt OPTARG; echo; printf "%.0s-" $(seq 1 25); echo -e "\n"; done
opt == 'a'
OPTARG == ''
-------------------------
opt == 'b'
OPTARG == '99'
-------------------------
opt == 'c'
OPTARG == '11'
-------------------------
opt == 'd'
OPTARG == ''
-------------------------
opt == 'e'
OPTARG == ''
-------------------------
opt == 'f'
OPTARG == '55'
-------------------------
opt == 'g'
OPTARG == '' <-- Empty because "hello you" is not an int
Like above, but with int arg.
$ OPTIND=1
$ while getopts-plus 'b:c::de::f::g:::ia' opt -ab 99 -c 11 -def 55 -g 7 "hello you"; do e opt OPTARG; echo; printf "%.0s-" $(seq 1 25); echo -e "\n"; done
opt == 'a'
OPTARG == ''
-------------------------
opt == 'b'
OPTARG == '99'
-------------------------
opt == 'c'
OPTARG == '11'
-------------------------
opt == 'd'
OPTARG == ''
-------------------------
opt == 'e'
OPTARG == ''
-------------------------
opt == 'f'
OPTARG == '55'
-------------------------
opt == 'g'
OPTARG == '7' <-- The passed int
Added optional option -h with regexp /^(a|b|ab|ba)$/, no arg passed.
$ OPTIND=1
$ while getopts-plus 'b:c::de::f::g:::ih::/^(a|b|ab|ba)$/a' opt -ab 99 -c 11 -def 55 -gh "hello you"; do e opt OPTARG; echo; printf "%.0s-" $(seq 1 25); echo -e "\n"; done
opt == 'a'
OPTARG == ''
-------------------------
opt == 'b'
OPTARG == '99'
-------------------------
opt == 'c'
OPTARG == '11'
-------------------------
opt == 'd'
OPTARG == ''
-------------------------
opt == 'e'
OPTARG == ''
-------------------------
opt == 'f'
OPTARG == '55'
-------------------------
opt == 'g'
OPTARG == ''
-------------------------
opt == 'h'
OPTARG == '' <-- Empty because "hello you" does not match the regexp
Like above, but with an arg matching the regexp.
$ OPTIND=1
$ while getopts-plus 'b:c::de::f::g:::ih::/^(a|b|ab|ba)$/a' opt -ab 99 -c 11 -def 55 -gh ab "hello you"; do e opt OPTARG; echo; printf "%.0s-" $(seq 1 25); echo -e "\n"; done
opt == 'a'
OPTARG == ''
-------------------------
opt == 'b'
OPTARG == '99'
-------------------------
opt == 'c'
OPTARG == '11'
-------------------------
opt == 'd'
OPTARG == ''
-------------------------
opt == 'e'
OPTARG == ''
-------------------------
opt == 'f'
OPTARG == '55'
-------------------------
opt == 'g'
OPTARG == ''
-------------------------
opt == 'h'
OPTARG == 'ab' <-- The arg that matches the regexp
Added another regexp-typed optional option -i with regexp /^\w+$/ (using the Perl token \w which means alphanumeric or underscore), no arg passed.
$ OPTIND=1
$ while getopts-plus 'b:c::de::f::g:::ih::/^(a|b|ab|ba)$/ai::/^\w+$/' opt -ab 99 -c 11 -def 55 -gh ab -i "hello you"; do e opt OPTARG; echo; printf "%.0s-" $(seq 1 25); echo -e "\n"; done
[23:10:49]
opt == 'a'
OPTARG == ''
-------------------------
opt == 'b'
OPTARG == '99'
-------------------------
opt == 'c'
OPTARG == '11'
-------------------------
opt == 'd'
OPTARG == ''
-------------------------
opt == 'e'
OPTARG == ''
-------------------------
opt == 'f'
OPTARG == '55'
-------------------------
opt == 'g'
OPTARG == ''
-------------------------
opt == 'h'
OPTARG == 'ab'
-------------------------
opt == 'i'
OPTARG == '' <-- Empty because "hello you" contains a space.
Like above, but with an arg matching the regexp.
$ OPTIND=1
$ while getopts-plus 'b:c::de::f::g:::ih::/^(a|b|ab|ba)$/ai::/^\w+$/' opt -ab 99 -c 11 -def 55 -gh ab -i foo_Bar_1 "hello you"; do e opt OPTARG; echo; printf "%.0s-" $(seq 1 25); echo -e "\n"; done
[23:15:23]
opt == 'a'
OPTARG == ''
-------------------------
opt == 'b'
OPTARG == '99'
-------------------------
opt == 'c'
OPTARG == '11'
-------------------------
opt == 'd'
OPTARG == ''
-------------------------
opt == 'e'
OPTARG == ''
-------------------------
opt == 'f'
OPTARG == '55'
-------------------------
opt == 'g'
OPTARG == ''
-------------------------
opt == 'h'
OPTARG == 'ab'
-------------------------
opt == 'i'
OPTARG == 'foo_Bar_1' <-- Matched because it contains only alphanumeric chars and underscores.
The following code solves this problem by checking for a leading dash and if found decrements OPTIND to point back to the skipped option for processing. This generally works fine except that you do not know the order the user will place options on the command line - if your optional argument option is last and does not provide an argument getopts will want to error out.
To fix the problem of the final argument missing, the "$#" array simply has an empty string "$# " appended so that getopts will be satisfied that it has gobbled up yet another option argument. To fix this new empty argument a variable is set that holds the total count of all options to be processed - when the last option is being processed a helper function called trim is called and removes the empty string prior to the value being utilized.
This is not working code, it has only place holders but you can easily modify it and with a little bit of care it can be useful to build a robust system.
#!/usr/bin/env bash
declare -r CHECK_FLOAT="%f"
declare -r CHECK_INTEGER="%i"
## <arg 1> Number - Number to check
## <arg 2> String - Number type to check
## <arg 3> String - Error message
function check_number() {
local NUMBER="${1}"
local NUMBER_TYPE="${2}"
local ERROR_MESG="${3}"
local FILTERED_NUMBER=$(sed 's/[^.e0-9+\^]//g' <<< "${NUMBER}")
local -i PASS=1
local -i FAIL=0
if [[ -z "${NUMBER}" ]]; then
echo "Empty number argument passed to check_number()." 1>&2
echo "${ERROR_MESG}" 1>&2
echo "${FAIL}"
elif [[ -z "${NUMBER_TYPE}" ]]; then
echo "Empty number type argument passed to check_number()." 1>&2
echo "${ERROR_MESG}" 1>&2
echo "${FAIL}"
elif [[ ! "${#NUMBER}" -eq "${#FILTERED_NUMBER}" ]]; then
echo "Non numeric characters found in number argument passed to check_number()." 1>&2
echo "${ERROR_MESG}" 1>&2
echo "${FAIL}"
else
case "${NUMBER_TYPE}" in
"${CHECK_FLOAT}")
if ((! $(printf "${CHECK_FLOAT}" "${NUMBER}" &>/dev/random;echo $?))); then
echo "${PASS}"
else
echo "${ERROR_MESG}" 1>&2
echo "${FAIL}"
fi
;;
"${CHECK_INTEGER}")
if ((! $(printf "${CHECK_INTEGER}" "${NUMBER}" &>/dev/random;echo $?))); then
echo "${PASS}"
else
echo "${ERROR_MESG}" 1>&2
echo "${FAIL}"
fi
;;
*)
echo "Invalid number type format: ${NUMBER_TYPE} to check_number()." 1>&2
echo "${FAIL}"
;;
esac
fi
}
## Note: Number can be any printf acceptable format and includes leading quotes and quotations,
## and anything else that corresponds to the POSIX specification.
## E.g. "'1e+03" is valid POSIX float format, see http://mywiki.wooledge.org/BashFAQ/054
## <arg 1> Number - Number to print
## <arg 2> String - Number type to print
function print_number() {
local NUMBER="${1}"
local NUMBER_TYPE="${2}"
case "${NUMBER_TYPE}" in
"${CHECK_FLOAT}")
printf "${CHECK_FLOAT}" "${NUMBER}" || echo "Error printing Float in print_number()." 1>&2
;;
"${CHECK_INTEGER}")
printf "${CHECK_INTEGER}" "${NUMBER}" || echo "Error printing Integer in print_number()." 1>&2
;;
*)
echo "Invalid number type format: ${NUMBER_TYPE} to print_number()." 1>&2
;;
esac
}
## <arg 1> String - String to trim single ending whitespace from
function trim_string() {
local STRING="${1}"
echo -En $(sed 's/ $//' <<< "${STRING}") || echo "Error in trim_string() expected a sensible string, found: ${STRING}" 1>&2
}
## This a hack for getopts because getopts does not support optional
## arguments very intuitively. E.g. Regardless of whether the values
## begin with a dash, getopts presumes that anything following an
## option that takes an option argument is the option argument. To fix
## this the index variable OPTIND is decremented so it points back to
## the otherwise skipped value in the array option argument. This works
## except for when the missing argument is on the end of the list,
## in this case getopts will not have anything to gobble as an
## argument to the option and will want to error out. To avoid this an
## empty string is appended to the argument array, yet in so doing
## care must be taken to manage this added empty string appropriately.
## As a result any option that doesn't exit at the time its processed
## needs to be made to accept an argument, otherwise you will never
## know if the option will be the last option sent thus having an empty
## string attached and causing it to land in the default handler.
function process_options() {
local OPTIND OPTERR=0 OPTARG OPTION h d r s M R S D
local ERROR_MSG=""
local OPTION_VAL=""
local EXIT_VALUE=0
local -i NUM_OPTIONS
let NUM_OPTIONS=${##}+1
while getopts “:h?d:DM:R:S:s:r:” OPTION "$#";
do
case "$OPTION" in
h)
help | more
exit 0
;;
r)
OPTION_VAL=$(((${NUM_OPTIONS}==${OPTIND})) && trim_string "${OPTARG##*=}" || echo -En "${OPTARG##*=}")
ERROR_MSG="Invalid input: Integer or floating point number required."
if [[ -z "${OPTION_VAL}" ]]; then
## can set global flags here
:;
elif [[ "${OPTION_VAL}" =~ ^-. ]]; then
let OPTIND=${OPTIND}-1
## can set global flags here
elif [ "${OPTION_VAL}" = "0" ]; then
## can set global flags here
:;
elif (($(check_number "${OPTION_VAL}" "${CHECK_FLOAT}" "${ERROR_MSG}"))); then
:; ## do something really useful here..
else
echo "${ERROR_MSG}" 1>&2 && exit -1
fi
;;
d)
OPTION_VAL=$(((${NUM_OPTIONS}==${OPTIND})) && trim_string "${OPTARG##*=}" || echo -En "${OPTARG##*=}")
[[ ! -z "${OPTION_VAL}" && "${OPTION_VAL}" =~ ^-. ]] && let OPTIND=${OPTIND}-1
DEBUGMODE=1
set -xuo pipefail
;;
s)
OPTION_VAL=$(((${NUM_OPTIONS}==${OPTIND})) && trim_string "${OPTARG##*=}" || echo -En "${OPTARG##*=}")
if [[ ! -z "${OPTION_VAL}" && "${OPTION_VAL}" =~ ^-. ]]; then ## if you want a variable value that begins with a dash, escape it
let OPTIND=${OPTIND}-1
else
GLOBAL_SCRIPT_VAR="${OPTION_VAL}"
:; ## do more important things
fi
;;
M)
OPTION_VAL=$(((${NUM_OPTIONS}==${OPTIND})) && trim_string "${OPTARG##*=}" || echo -En "${OPTARG##*=}")
ERROR_MSG=$(echo "Error - Invalid input: ${OPTION_VAL}, Integer required"\
"retry with an appropriate option argument.")
if [[ -z "${OPTION_VAL}" ]]; then
echo "${ERROR_MSG}" 1>&2 && exit -1
elif [[ "${OPTION_VAL}" =~ ^-. ]]; then
let OPTIND=${OPTIND}-1
echo "${ERROR_MSG}" 1>&2 && exit -1
elif (($(check_number "${OPTION_VAL}" "${CHECK_INTEGER}" "${ERROR_MSG}"))); then
:; ## do something useful here
else
echo "${ERROR_MSG}" 1>&2 && exit -1
fi
;;
R)
OPTION_VAL=$(((${NUM_OPTIONS}==${OPTIND})) && trim_string "${OPTARG##*=}" || echo -En "${OPTARG##*=}")
ERROR_MSG=$(echo "Error - Invalid option argument: ${OPTION_VAL},"\
"the value supplied to -R is expected to be a "\
"qualified path to a random character device.")
if [[ -z "${OPTION_VAL}" ]]; then
echo "${ERROR_MSG}" 1>&2 && exit -1
elif [[ "${OPTION_VAL}" =~ ^-. ]]; then
let OPTIND=${OPTIND}-1
echo "${ERROR_MSG}" 1>&2 && exit -1
elif [[ -c "${OPTION_VAL}" ]]; then
:; ## Instead of erroring do something useful here..
else
echo "${ERROR_MSG}" 1>&2 && exit -1
fi
;;
S)
STATEMENT=$(((${NUM_OPTIONS}==${OPTIND})) && trim_string "${OPTARG##*=}" || echo -En "${OPTARG##*=}")
ERROR_MSG="Error - Default text string to set cannot be empty."
if [[ -z "${STATEMENT}" ]]; then
## Instead of erroring you could set a flag or do something else with your code here..
elif [[ "${STATEMENT}" =~ ^-. ]]; then ## if you want a statement that begins with a dash, escape it
let OPTIND=${OPTIND}-1
echo "${ERROR_MSG}" 1>&2 && exit -1
echo "${ERROR_MSG}" 1>&2 && exit -1
else
:; ## do something even more useful here you can modify the above as well
fi
;;
D)
## Do something useful as long as it is an exit, it is okay to not worry about the option arguments
exit 0
;;
*)
EXIT_VALUE=-1
;&
?)
usage
exit ${EXIT_VALUE}
;;
esac
done
}
process_options "$# " ## extra space, so getopts can find arguments
Try:
while getopts "hd:R:" arg; do
case $arg in
h)
echo "usage"
;;
d)
dir=$OPTARG
;;
R)
if [[ $OPTARG =~ ^[0-9]+$ ]];then
level=$OPTARG
elif [[ $OPTARG =~ ^-. ]];then
level=1
let OPTIND=$OPTIND-1
else
level=1
fi
;;
\?)
echo "WRONG" >&2
;;
esac
done
I think the above code will work for your purposes while still using getopts. I've added the following three lines to your code when getopts encounters -R:
elif [[ $OPTARG =~ ^-. ]];then
level=1
let OPTIND=$OPTIND-1
If -R is encountered and the first argument looks like another getopts parameter, level is set to the default value of 1, and then the $OPTIND variable is reduced by one. The next time getopts goes to grab an argument, it will grab the correct argument instead of skipping it.
Here is similar example based on the code from Jan Schampera's comment at this tutorial:
#!/bin/bash
while getopts :abc: opt; do
case $opt in
a)
echo "option a"
;;
b)
echo "option b"
;;
c)
echo "option c"
if [[ $OPTARG = -* ]]; then
((OPTIND--))
continue
fi
echo "(c) argument $OPTARG"
;;
\?)
echo "WTF!"
exit 1
;;
esac
done
When you discover that OPTARG von -c is something beginning with a hyphen, then reset OPTIND and re-run getopts (continue the while loop). Oh, of course, this isn't perfect and needs some more robustness. It's just an example.
You can always decide to differentiate the option with lowercase or uppercase.
However my idea is to call getopts twice and 1st time parse without arguments ignoring them (R) then 2nd time parse only that option with argument support (R:). The only trick is that OPTIND (index) needs to be changed during processing, as it keeps pointer to the current argument.
Here is the code:
#!/usr/bin/env bash
while getopts ":hd:R" arg; do
case $arg in
d) # Set directory, e.g. -d /foo
dir=$OPTARG
;;
R) # Optional level value, e.g. -R 123
OI=$OPTIND # Backup old value.
((OPTIND--)) # Decrease argument index, to parse -R again.
while getopts ":R:" r; do
case $r in
R)
# Check if value is in numeric format.
if [[ $OPTARG =~ ^[0-9]+$ ]]; then
level=$OPTARG
else
level=1
fi
;;
:)
# Missing -R value.
level=1
;;
esac
done
[ -z "$level" ] && level=1 # If value not found, set to 1.
OPTIND=$OI # Restore old value.
;;
\? | h | *) # Display help.
echo "$0 usage:" && grep " .)\ #" $0
exit 0
;;
esac
done
echo Dir: $dir
echo Level: $level
Here are few tests for scenarios which works:
$ ./getopts.sh -h
./getopts.sh usage:
d) # Set directory, e.g. -d /foo
R) # Optional level value, e.g. -R 123
\? | h | *) # Display help.
$ ./getopts.sh -d /foo
Dir: /foo
Level:
$ ./getopts.sh -d /foo -R
Dir: /foo
Level: 1
$ ./getopts.sh -d /foo -R 123
Dir: /foo
Level: 123
$ ./getopts.sh -d /foo -R wtf
Dir: /foo
Level: 1
$ ./getopts.sh -R -d /foo
Dir: /foo
Level: 1
Scenarios which doesn't work (so the code needs a bit of more tweaks):
$ ./getopts.sh -R 123 -d /foo
Dir:
Level: 123
More information about getopts usage can be found in man bash.
See also: Small getopts tutorial at Bash Hackers Wiki
I just ran into this myself and felt that none of the existing solutions were really clean. After working on it a bit and trying various things, I found that leveraging getopts SILENT mode with :) ... appears to have done the trick along with keeping OPTIND in sync.
usage: test.sh [-abst] [-r [DEPTH]] filename
*NOTE: -r (recursive) with no depth given means full recursion
#!/usr/bin/env bash
depth='-d 1'
while getopts ':abr:st' opt; do
case "${opt}" in
a) echo a;;
b) echo b;;
r) if [[ "${OPTARG}" =~ ^[0-9]+$ ]]; then
depth="-d ${OPTARG}"
else
depth=
(( OPTIND-- ))
fi
;;
s) echo s;;
t) echo t;;
:) [[ "${OPTARG}" = 'r' ]] && depth=;;
*) echo >&2 "Invalid option: ${opt}"; exit 1;;
esac
done
shift $(( OPTIND - 1 ))
filename="$1"
...
Old thread, but figured I'd share what I did anyway (which is also mostly older than this thread). I got fed up with trying to make getopt do what I wanted, and in a fit of frustration did this so that both short and long opts with optional arguments are supported. It's the long long way, somebody is sure to laugh, but it works exactly how I want it to - overly commented examples of all three cases are below:
#!/usr/bin/bash
# Begin testme.sh
shopt -s extglob;
VERSION="1.0"
function get_args(){
while test -n "${1}" ; do
case "${1}" in
-a | --all)
# dumb single argument example
PROCESS_ALL="yes"
shift 1
;;
-b | --buildnum)
# requires a second argument so use check_arg() below
check_arg $1 $2
BUILD_NUM=${2}
shift 2
;;
-c | --cache)
# Example where argument is not required, don't use check_arg()
if [ echo ${2} | grep -q "^-" ]; then
# no argument given, use default cache value
CACHEDIR=~/mycachedir
# Note: this could have been set upon entering the script
# and used the negative above as well
shift 1
else
cache=${2}
shift 2
fi
;;
-h | --help)
showhelp
exit 0
;;
-v | --version)
echo -e "$(basename ${0}) ${VERSION}\n"
exit 0
;;
# Handle getopt style short args (reason for shopts above)
-+([a-z,A-Z]))
# split up the arguments and call recursively with trailing break
arg="${1}"
newargs=$( echo ${1} | sed 's#-##' | \
sed 's/.\{1\}/& /g' | \
sed 's/[^ ]* */-&/g')
newargs="${newargs} $(echo ${#} | sed "s#${arg}##")"
get_args ${newargs}
break;
;;
*)
echo -e "Invalid argument ${1}!\n\n"
showhelp
exit 1
;;
esac
done
}
# Super lazy, but I didn't want an if/then/else for every required arg
function check_arg(){
if [ echo "${2}" | grep -q "^-" ]; then
echo "Error: $1 requires a valid argument."
exit 1
fi
}
function showhelp(){
echo ""
echo "`basename ${0}` is a utility to..."
}
# Process command line arguments
get_args $#
...
# End testme.sh
I've never run into it, but I suppose there might be a case where I'd need the second argument to begin with a '-' character, in which case, I'd strip it out before calling get_args(). I have used with with fixed position arguments as well, and in that case they are at the end, but same solution. Also, I suppose a portable version could just handle the combined short args in *), but I figure if bash is too heavy a requirement, the you are on your own.
Just use /usr/bin/getopt, it is much better. It is compliant with libc getopt, so you'll get optional argument with ::. Check man getopt.
getopt bash template:
#!/bin/bash
optstring_long="foo,bar:,baz::,verbose,dry-run,help"
optstring_short="fb:z::vnh"
opts=$(getopt -o "${optstring_short}" --long "${optstring_long}" --name "$0" -- "$#") ||
exit $?
eval set -- "$opts"
unset foo
unset bar
unset baz
unset verbose_on
unset verbose_off
unset verbose
unset dryrun
while true
do
case "$1" in
-f|--foo)
foo=true
echo "Foo!"
shift;;
-b|--bar)
bar=$2
echo "Bar: ${bar}"
shift 2;;
-z|--baz)
baz=${2:-default!}
echo "Baz: ${baz}"
shift 2;;
-v|--verbose)
verbose_on="set -x"
verbose_off="{ set +x; } 2>/dev/null"
verbose=verbose_run
shift;;
-n|--dry-run)
dryrun=echo
shift;;
-h|--help)
cat <<EOF
This is help!
EOF
exit;;
--) shift; break;;
esac
done
verbose_run() { set -x; eval "$#"; { set +x; } 2>/dev/null; }
Note that for optional argument it must be supplied without space (same like with getopts trick):
$ ./a -b 1 -z -f
Bar: 1
Baz: default!
Foo!
$ ./a -b 2 -z bad -f
Bar: 2
Baz: default!
Foo!
$ ./a -b 2 -zgood -f
Bar: 2
Baz: good
Foo!
Check more bash tricks here.
while getopts "hd:R" arg; do
case $arg in
h) echo "usage" ;;
d) dir=$OPTARG ;;
R)
if [[ ${!OPTIND} =~ ^[0-9]+$ ]]; then
eval level=\$$(( OPTIND++ ))
else
level=1
fi
;;
*) echo "WRONG" >&2 ;;
esac
done
if (( OPTIND != ($# + 1) )); then
echo "WRONG" >&2
fi
This is similar to #calandoa's answer, but is a little more concise. Also, it still verifies the argument is a decimal integer, as was done in the original question. I also added the important check that all the arguments were consumed by the while loop. Supporting both optional option arguments and non-option arguments would be more difficult.

Extract values from an array

I have an array of options and their arguments:
ARGS=('-a' '-c' 'red' 'orange' '--verbose' '-p' 'apple' 'banana')
I need to extract arguments for the option -c and get a list of the rest:
echo "${COLORS[#]}" # returns: red orange
echo "${OPTIONS[#]}" # returns: -a --verbose -p apple banana
I have managed to get a list of colors using getopts (probably not the best approach), but I didn't find a way to extract the rest of the options.
COLORS=()
set_colors() {
while getopts "p:" option 2>/dev/null; do
case ${option} in
p)
COLORS+=("$OPTARG")
while [[ "$OPTIND" -le "$#" ]] && [[ "${!OPTIND:0:1}" != "-" ]]; do
COLORS+=("${!OPTIND}")
((OPTIND++))
done
;;
*) ;;
esac
done
}
set_colors "${ARGS[#]}"
I used the variable $in_colors as a flag when walking over the options and distributing them into two arrays.
#! /bin/bash
ARGS=('-a' '-c' 'red' 'orange' '--verbose' '-p' 'apple' 'banana')
in_colors=0
for arg in "${ARGS[#]}" ; do
if [[ $arg == '-c' ]] ; then
in_colors=1
elif [[ $arg == -* ]] ; then
in_colors=0
fi
if ((in_colors)) ; then
colors+=("$arg")
else
opts+=("$arg")
fi
shift
done
echo "Colors: ${colors[#]}"
echo "Options: ${opts[#]}"

How to pass a long option to a bash script?

./script.sh -abc hello
How can I write my script to use '-abc' as the option and 'hello' as the value to that option?
I should be able to pass this value to all the functions in this script. Lets say I have 2 functions: X and Y.
Use this in your script:
[[ $1 == -abc ]] && value="$2" || echo invalid option
If you don't want to print any messages on wrong option or no option, then omit the || echo ... part, value will be empty.
If you want to make the second argument a must, then:
[[ $1 == -abc ]] && [[ $2 != "" ]] && value="$2" || echo invalid option
Using if else loop will give you complete control over this:
if [[ $1 == -abc ]]; then
#if first option is valid then do something here
if [[ $2 != "" ]]; then
value="$2"
else
#if second option is not given then do something here
echo invalid option
fi
else
echo invalid option
#if first option is invalid then do something here
fi
If you want to make the first argument a must too, then change the first if statement line to
if [[ $1 == -abc && $1 != "" ]]; then
If you want to pass as many arguments as you wish and process them,
then use something like this:
#!/bin/bash
opts=( "$#" )
#if no argument is passed this for loop will be skipped
for ((i=0;i<$#;i++));do
case "${opts[$i]}" in
-abc)
# "${opts[$((i+1))]}" is the immediately follwing option
[[ "${opts[$((i+1))]}" != "" ]] &&
value="${opts[$((i+1))]}"
echo "$value"
((i++))
#skips the nex adjacent argument as it is already taken
;;
-h)
#dummy help option
echo "Options are [-abc value], -h"
;;
*)
#other unknown options
echo invalid option
break
;;
esac
done
This is an example of handling multiple arguments with only two options available -abc value and -h
bash doesn't have a built in command for processing long arguments. In order to parse long options in a shell script, you'll need to iterate over the arguments list yourself.
Here's one approach:
#!/bin/sh
is_option_arg () {
case $1 in
-*)
return 1
;;
*)
return 0
;;
esac
}
usage () {
echo "$(basename "$0") -abc ARG -def ARG -verbose"
}
OPT_ABC=
OPT_DEF=
OPT_VERBOSE=false
while [ "$#" -gt 0 ]; do
case $1 in
-abc)
shift
{ [ "$#" -ne 0 ] && is_option_arg "$1"; } || { usage >&2; exit 1; }
OPT_ABC=$1
;;
-def)
shift
{ [ "$#" -ne 0 ] && is_option_arg "$1"; } || { usage >&2; exit 1; }
OPT_DEF=$1
;;
-verbose)
OPT_VERBOSE=true
;;
*)
break
;;
esac
shift
done
echo "OPT_ABC=$OPT_ABC"
echo "OPT_DEF=$OPT_DEF"
echo "OPT_VERBOSE=$OPT_VERBOSE"
if [ "$#" -gt 0 ]; then
echo "Remaining args:"
for arg in "$#"; do
echo "$arg"
done
fi
You pretty much have to implement it yourself manually. Here's one way:
abc=
while [[ "$1" == -* ]]; do
opt=$1
shift
case "$opt" in
-abc)
if (( ! $# )); then
echo >&2 "$0: option $opt requires an argument."
exit 1
fi
abc="$1"
shift
;;
*)
echo >&2 "$0: unrecognized option $opt."
exit 2
;;
esac
done
echo "abc is '$abc', remaining args: $*"
Some sample runs of the above:
(0)$ ./script.sh
abc is '', remaining args:
(0)$ ./script.sh hello
abc is '', remaining args: hello
(0)$ ./script.sh -abc hello
abc is 'hello', remaining args:
(0)$ ./script.sh -abc hello there
abc is 'hello', remaining args: there
(0)$ ./script.sh -abc
./script.sh: option -abc requires an argument.
(1)$ ./script.sh -bcd
./script.sh: unrecognized option -bcd.
(2)$

How can I parse long-form arguments in shell?

Everything I see uses getopt or the slightly-fancier getopts which only supports one-character options (e.g., -h but not --help). I want to do fancy long options.
I've done something like this:
_setArgs(){
while [ "${1:-}" != "" ]; do
case "$1" in
"-c" | "--configFile")
shift
configFile=$1
;;
"-f" | "--forceUpdate")
forceUpdate=true
;;
"-r" | "--forceRetry")
forceRetry=true
;;
esac
shift
done
}
As you can see, this supports both the single-character and the longer options nicely. It allows for values to be associated with each argument, as in the case of --configFile. It's also quite extensible, with no artificial limitations as to what options can be configured, etc.
As included above, the "${1:-}" prevents an "unbound variable" error when running in bash "strict" mode (set -euo pipefail).
Assuming that you "want to do fancy long options" regardless of the tool, just go with getopt (getopts seems to be mainly used when portability is crucial). Here's an example of about the maximum complexity that you'll get:
params="$(getopt -o e:hv -l exclude:,help,verbose --name "$(basename "$0")" -- "$#")"
if [ $? -ne 0 ]
then
usage
fi
eval set -- "$params"
unset params
while true
do
case $1 in
-e|--exclude)
excludes+=("$2")
shift 2
;;
-h|--help)
usage
;;
-v|--verbose)
verbose='--verbose'
shift
;;
--)
shift
break
;;
*)
usage
;;
esac
done
With this code, you can specify -e/--exclude more than once, and ${excludes[#]} will contain all of the given excludes. After processing (-- is always present) anything remaining is stored in $#.
I have created a bash function that is the easiest to use and needs no customization. Just use the function and pass all long options with or without arguments and the function will set them as variables with the corresponding option arguments as values in your script.
function get_longOpt {
## Pass all the script's long options to this function.
## It will parse all long options with its arguments,
## will convert the option name to a variable and
## convert its option value to the variable's value.
## If the option does not have an argument, the
## resulting variable's value will be set to true.
## Works properly when providing long options, only.
## Arguments to options may not start with two dashes.
##
#### Usage
##
## get_longOpt $#
##
## May expand to:
##
## get_longOpt --myOption optimopti --longNumber 1000 --enableMe --hexNumber 0x16
##
### Results in the bash interpretation of:
## myOption=optimopti
## longNumber=1000
## enableMe=true
## hexNumber=0x16
##
local -a opt_list=( $# )
local -A opt_map
local -i index=0
local next_item
for item in ${opt_list[#]}; do
# Convert arg list to map.
let index++
next_item="${opt_list[$index]}"
if [[ "${item}" == --* ]] \
&& [[ "${next_item}" != --* ]] \
&& [[ ! -z "${next_item}" ]]
then
item="$(printf '%s' "${item##*-}")"
opt_map[${item}]="${next_item}"
elif [[ "${item}" == --* ]] \
&& { [[ "${next_item}" == --* ]] \
|| [[ -z "${next_item}" ]]; }
then
item="$(printf '%s' "${item##*-}")"
opt_map[${item}]=true
fi
done
for item in ${!opt_map[#]}; do
# Convert map keys to shell vars.
value="${opt_map[$item]}"
[[ ! -z "${value}" ]] && \
printf -v "$item" '%s' "$value"
done
}
The up to date original source code is available here:
https://github.com/theAkito/akito-libbash/blob/master/bishy.bash

Resources