Bash : Parse options after arguments with getopts - bash

In a script which request some arguments (arg) and options (-a), I would like to let the script user the possibility to place the options where he wants in the command line.
Here is my code :
while getopts "a" opt; do
echo "$opt"
done
shift $((OPTIND-1))
echo "all_end : $*"
With this order, I have the expected behaviour :
./test.sh -a arg
a
all_end : arg
I would like to get the same result with this order :
./test.sh arg -a
all_end : arg -a

The getopt command (part of the util-linux package and different from getopts) will do what you want. The bash faq has some opinions about using that, but honestly these days most systems will have the modern version of getopt.
Consider the following example:
#!/bin/sh
options=$(getopt -o o: --long option: -- "$#")
eval set -- "$options"
while :; do
case "$1" in
-o|--option)
shift
OPTION=$1
;;
--)
shift
break
;;
esac
shift
done
echo "got option: $OPTION"
echo "remaining args are: $#"
We can call this like this:
$ ./options.sh -o foo arg1 arg2
got option: foo
remaining args are: arg1 arg2
Or like this:
$ ./options.sh arg1 arg2 -o foo
got option: foo
remaining args are: arg1 arg2

You can still go with argument parsing by looking at each of them
#!/bin/bash
for i in "$#"
do
case $i in
-a)
ARG1="set"
shift
;;
*)
# the rest (not -a)
ARGS="${ARGS} $i"
;;
esac
done
if [ -z "$ARG1" ]; then
echo "You haven't passed -a"
else
echo "You have passed -a"
fi
echo "You have also passed: ${ARGS}"
and then you will get:
> ./script.sh arg -a
You have passed -a
You have also passed: arg
> ./script.sh -a arg
You have passed -a
You have also passed: arg
> ./script.sh -a
You have passed -a
You have also passed:
> ./script.sh arg
You haven't passed -a
You have also passed: arg

Consider this approach
#!/bin/bash
opt(){
case $1 in
-o|--option) option="$2";;
-h|--help ) echo "$help"; exit 0;;
*) echo "unknown option: $1"; exit 1;;
esac
}
while [[ $# ]]; do
case $1 in
arg1) var1=$1 ;;
arg2) var2=$1 ;;
-*) opt "$#"; shift;;
*) echo "unknown option: $1"; exit 1;;
esac
shift
done
echo args: $var1 $var2
echo opts: $option

In the many years of bash programming, I've found it useful to get rid of the bone in my head that says functions should look like f(x,y), especially since bash requires clumsy/inefficient code to handle command line arguments.
Option arguments often have default values, and can be more readily passed as environmental variables whose scope is limited to the called script, and save arguments for what must be provided.
Applying this to your example, The script would look like:
OPTION=${OPTION:="fee"}
echo "option: $OPTION"
echo "remaining args are: $*"
And would be called with:
OPTION="foo" ./options.sh arg1 arg2

Related

Is there a way in bash script to have an option to give an argument but it shouldn't a must?

I have a scenario where i would like to assign an option a default value but a user can decide to give it another argument:
Here is an example
check_param() {
for arg in "$#"; do
shift
case "$arg" in
"--force") set -- "$#" "-f" ;;
"--type") set -- "$#" "-t" ;;
"--help") set -- "$#" "-h" ;;
"--"*) echo "Unknown parameter: " $arg; show_help; exit 1 ;;
*) set -- "$#" "$arg"
esac
done
# Standard Variables
force=0
type="daily"
OPTIND=1
while getopts "hft:v" opt
do
case "$opt" in
"f") force=1 ;;
"t") type=${OPTARG} ;;
"h") show_help; exit 0 ;;
"?") show_help; exit 1 ;;
esac
done
shift $(expr $OPTIND - 1) # remove options from positional parameters
From the above example, i would like when the user gives the parameter -t without any argument to apply the default value which is daily , and the user can also use parameter -t with any other argument and that will be checked later in code.
The problem is now the parameter -t must be given an argument due to the colon, but i kinda need for it to do both, with or without argument.
Thanks in advance for any explanations or links to any article that can help.
So according to a suggestion i got Here is the test result
check_param() {
## Standard Variablen der Parameter
force=0
type="daily.0"
## Break down the options in command lines for easy parsing
## -l is to accept the long options too
args=$(getopt -o hft::v -l force,type::,help -- "$#")
eval set -- "$args"
## Debugging mechanism
echo ${args}
echo "Number of parameters $#"
echo "first parameter $1"
echo "Second parameter $2"
echo "third parameter $3"
while (($#)); do
case "$1" in
-f|--force) force=1; ;;
-t|--type) type="${2:-${type}}"; shift; ;;
-h|--help) show_help; exit 0; ;;
--) shift; break; ;;
*) echo "Unbekannter Parameter"; exit 1; ;;
esac
shift
done
echo ${type}
}
check_param $#
echo ${type}
The output:
sh scriptsh -t patch.0
-t '' -- 'patch.0'
Number of parameters 4
first parameter -t
Second parameter
third parameter --
daily.0
daily.0
It still didn't assign the value patch to the variable type
Is there a way in bash script to have an option to give an argument but it shouldn't a must?
Yes, there is a way.
getopts does not supports optional arguments. So... you can:
roll your own bash library for parsing arguments or
use another tool that has support for optional arguments.
A common tool is getopt that should be available on any linux.
args=$(getopt -o hft::v -l force,type::,help -- "$#")
eval set -- "$args"
while (($#)); do
case "$1" in
-f|--force) force=1; ;;
-t|--type) type="${2:-default_value}"; shift; ;;
-h|--help) echo "THis is help"; exit; ;;
--) shift; break; ;;
*) echo "Error parsgin arguments"; exit 1; ;;
esac
shift
done
getopt handles long arguments and reorders arguments, so you can ./prog file1 -t opt and ./prog -t opt file1 with same result.

Is there a way to continue in a Flag when there is no $OPTARG set in Bash, GETOPTS?

I would like to build a script with getopts, that continues in the flag, when an $OPTARG isn't set.
My script looks like this:
OPTIONS=':dBhmtb:P:'
while getopts $OPTIONS OPTION
do
case "$OPTION" in
m ) echo "m"
t ) echo "t"
d ) echo "d";;
h ) echo "h";;
B ) echo "b";;
r ) echo "r";;
b ) echo "b"
P ) echo hi;;
#continue here
\? ) echo "?";;
:) echo "test -$OPTARG requieres an argument" >&2
esac
done
My aim is to continue at my comment, when there is no $OPTARG set for -P.
All I get after running ./test -P is :
test -P requieres an argument
and then it continues after the loop but I want to continue in the -P flag.
All clear?
Any Ideas?
First, fix the missing ;; in some of the case branches.
I don't think you can: you told getopts that -P requires an argument: two error cases
-P without an argument is the last option. In this case getops sees that nothing follows -P and sets the OPTION variable to :, which you handle in the case statement.
-P is followed by another option: getopts will simply take the next word, even if the next word is another option, as OPTARG.
Change the case branch to
P ) echo "P: '$OPTARG'";;
Then:
invoking the script like bash script.sh -P -m -t, the output is
P: '-m'
t
invoking the script like bash script.sh -Pmt, the output is
P: 'mt'
This is clearly difficult to work around. How do you know if the user intended the option argument to be literally "mt" and not the options -m and -t?
You might be able to work around this using getopt (see the canonical example) using an optional argument for a long option (those require an equal sign like --long=value) so it's maybe easier to check if the option argument is missing or not.
Translating getopts parsing to getopt -- it's more verbose, but you have finer-grained control
die() { echo "$*" >&2; exit 1; }
tmpArgs=$(getopt -o 'dBhmt' \
--long 'b::,P::' \
-n "$(basename "$0")" \
-- "$#"
)
(( $? == 0 )) || die 'Problem parsing options'
eval set -- "$tmpArgs"
while true; do
case "$1" in
-d) echo d; shift ;;
-B) echo B; shift ;;
-h) echo h; shift ;;
-m) echo m; shift ;;
-t) echo t; shift ;;
--P) case "$2" in
'') echo "P with no argument" ;;
*) echo "P: $2" ;;
esac
shift 2
;;
--b) case "$2" in
'') echo "b with no argument" ;;
*) echo "b: $2" ;;
esac
shift 2
;;
--) shift; break ;;
*) printf "> %q\n" "$#"
die 'getopt internal error: $*' ;;
esac
done
echo "Remaining arguments:"
for ((i=1; i<=$#; i++)); do
echo "$i: ${!i}"
done
Successfully invoking the program with --P:
$ ./myscript.sh --P -mt foo bar
P with no argument
m
t
Remaining arguments:
1: foo
2: bar
$ ./myscript.sh --P=arg -mt foo bar
P: arg
m
t
Remaining arguments:
1: foo
2: bar
This does impose higher overhead on your users, because -P (with one dash) is invalid, and the argument must be given with =
$ ./myscript.sh --P arg -mt foo bar
P with no argument
m
t
Remaining arguments:
1: arg
2: foo
3: bar
$ ./myscript.sh --Parg mt foo bar
myscript.sh: unrecognized option `--Parg'
Problem parsing options
$ ./myscript.sh -P -mt foo bar
myscript.sh: invalid option -- P
Problem parsing options
$ ./myscript.sh -P=arg -mt foo bar
myscript.sh: invalid option -- P
myscript.sh: invalid option -- =
myscript.sh: invalid option -- a
myscript.sh: invalid option -- r
myscript.sh: invalid option -- g
Problem parsing options
Do not mix logic with arguments parsing.
Prefer lower case variables.
My aim is to continue at my comment, when there is no $OPTARG set for -P
I advise not to. The less you do at one scope, the less you have to think about. Split parsing options and executing actions in separate stages. I advise to:
# set default values for options
do_something_related_to_P=false
recursive=false
tree_output=false
# parse arguments
while getopts ':dBhmtb:P:' option; do
case "$option" in
t) tree_output=true; ;;
r) recursive="$OPTARG"; ;;
P) do_something_related_to_P="$OPTARG"; ;;
\?) echo "?";;
:) echo "test -$OPTARG requieres an argument" >&2
esac
done
# application logic
if "$do_something_related_to_P"; then
do something related to P
if "$recursive"; then
do it in recursive style
fi
fi |
if "$tree_output"; then
output_as_tree
else
cat
fi
Example of "don't put programming application logic in the case branches" -- the touch command can take a -t timespec option or a -r referenceFile option but not both:
$ touch -t 202010100000 -r file1 file2
touch: cannot specify times from more than one source
Try 'touch --help' for more information.
I would implement that like (ignoring other options):
while getopts t:r: opt; do
case $opt in
t) timeSpec=$OPTARG ;;
r) refFile=$OPTARG ;;
esac
done
shift $((OPTIND-1))
if [[ -n $timeSpec && -n $refFile ]]; then
echo "touch: cannot specify times from more than one source" >&2
exit 1
fi
I would not do this:
while getopts t:r: opt; do
case $opt in
t) if [[ -n $refFile ]]; then
echo "touch: cannot specify times from more than one source" >&2
exit 1
fi
timeSpec=$OPTARG ;;
r) if [[ -n $timeSpec ]]; then
echo "touch: cannot specify times from more than one source" >&2
exit 1
fi
refFile=$OPTARG ;;
esac
done
You can see if the logic gets more complicated (as I mentioned, exactly one of -a or -b or -c), that the case statement size can easily balloon unmaintainably.

Using getopt to parse second argument into a variable

How do I parse the second argument into a variable using bash and getopt on the following script.
I can do sh test.sh -u and get "userENT" to display. But if I do sh test.sh -u testuser on this script I get an error.
#!/bin/sh
# Now check for arguments
OPTS=`getopt -o upbdhrstv: --long username,password,docker-build,help,version,\
release,remote-registry,stage,develop,target: -n 'parse-options' -- "$#"`
while true; do
case "$1" in
-u | --username)
case "$2" in
*) API_KEY_ART_USER="$2"; echo "userENT" ;shift ;;
esac ;;
-- ) shift; break ;;
* ) if [ _$1 != "_" ]; then ERROR=true; echo; echo "Invalid option $1"; fi; break ;;
esac
done
echo "user" $API_KEY_ART_USER
How can I pass the -u testuser and not have an Invalid option testuser error?
Output:
>sh test3.sh -u testuser
userENT
Invalid option testuser
user testuser
man getopt would tell you that a colon following the option indicates that it has an argument. You only have a colon after the v. You also weren't shifting within your loop, so you'd be unable to parse any options past the first one. And I'm not sure why you felt the need to have a second case statement that only had a single default option. In addition, there were a number of poor practices in your code including use of all caps variable names and backticks instead of $() for executing commands. And you've tagged your question bash but your shebang is /bin/sh. Give this a try, but you shouldn't be using code without understanding what it does.
#!/bin/sh
# Now check for arguments
opts=$(getopt -o u:pbdhrstv: --long username:,password,docker-build,help,version,\
release,remote-registry,stage,develop,target: -n 'parse-options' -- "$#")
while true; do
case "$1" in
-u|--username)
shift
api_key_art_user="$1"
echo "userENT"
;;
--)
shift;
break
;;
*)
if [ -n "$1" ]; then
err=true
echo "Invalid option $1"
fi
break
;;
esac
shift
done
echo "user $api_key_art_user"

Bash script command handler to replace long if chain

Let's say I have a bunch of if statements with the form:
if (some flag variable/argument is set) then
execute another command or bash script
This is a bit troublesome to maintain, so I was wondering if there was some other way of doing this. While this guide is for node.js, I was wondering if it is possible to achieve something similar in bash
I wonder if you're looking for something like this:
#!/bin/bash
# initialize global options
debug=false
verbose=0
main() {
local OPTIND
while getopts :hdv: opt; do
case $opt in
h) show_help; exit ;;
d) debug=true ;;
v) verbose=$OPTARG;;
:) echo "error: missing argument for -$OPTARG"; exit 1;;
?) echo "error: unknown option -$OPTARG"; exit 1 ;;
esac
done
shift $((OPTIND-1))
if [[ -z $1 ]]; then
echo "error: missing subcommand"
show_help
exit 1
fi
case $1 in
bar|baz)
# invoke the command with the arguments
"$#" ;;
*) echo "error: unknown subcommand $1"; exit 1;;
esac
}
show_help() {
echo "usage: $(basename "$0") [global opts] subcommand [local opts and args]"
echo "... more details..."
}
bar() {
echo "you called $FUNCNAME with args $*"
}
baz() {
echo "this is baz"
local OPTIND
while getopts :ab opt; do
case $opt in
a|b) echo "you selected option $opt";;
?) echo "unknown option -$OPTARG";;
esac
done
if $debug; then echo "some debug message"; fi
(( verbose > 0 )) && echo "some verbose message"
}
main "$#"
You could write a wrapper function that checks the variable and then executes the command passed in:
#!/bin/bash
run_if_set() {
local var=$1
shift
(($# == 0)) && return # nothing to run
[[ $var ]] && "$#" # execute only if var is set to a non-empty string
}
Then replace your if statements with:
run_if_set "$var" command ...
which is slightly more readable than
if [[ $var ]]; then
command ...
fi
or
[[ $var ]] && command ...

How to create a bash script with optional parameters for a flag

I'm trying to create a script which will have a flag with optional options. With getopts it's possible to specify a mandatory argument (using a colon) after the flag, but I want to keep it optional.
It will be something like this:
./install.sh -a 3
or
./install.sh -a3
where 'a' is the flag and '3' is the optional parameter that follows a.
Thanks in advance.
The getopt external program allows options to have a single optional argument by adding a double-colon to the option name.
# Based on a longer example in getopt-parse.bash, included with
# getopt
TEMP=$(getopt -o a:: -- "$#")
eval set -- "$TEMP"
while true ; do
case "$1" in
-a)
case "$2" in
"") echo "Option a, no argument"; shift 2 ;;
*) echo "Option a, argument $2"; shift 2;;
esac ;;
--) shift; break ;;
*) echo "Internal error!"; exit 1 ;;
esac
done
The following is without getopt and it takes an optional argument with the -a flag:
for WORD; do
case $WORD in
-a?) echo "single arg Option"
SEP=${WORD:2:1}
echo $SEP
shift ;;
-a) echo "split arg Option"
if [[ ${2:0:1} != "-" && ${2:0:1} != ""]] ; then
SEP=$2
shift 2
echo "arg present"
echo $SEP
else
echo "optional arg omitted"
fi ;;
-a*) echo "arg Option"
SEP=${WORD:2}
echo $SEP
shift ;;
-*) echo "Unrecognized Short Option"
echo "Unrecognized argument"
;;
esac
done
Other options/flags also can be added easily.
Use the getopt feature. On most systems, man getopt will yield documentation for it, and even examples of using it in a script. From the man page on my system:
The following code fragment shows how one might process the arguments
for a command that can take the options -a and -b, and the option -o,
which requires an argument.
args=`getopt abo: $*`
# you should not use `getopt abo: "$#"` since that would parse
# the arguments differently from what the set command below does.
if [ $? != 0 ]
then
echo 'Usage: ...'
exit 2
fi
set -- $args
# You cannot use the set command with a backquoted getopt directly,
# since the exit code from getopt would be shadowed by those of set,
# which is zero by definition.
for i
do
case "$i"
in
-a|-b)
echo flag $i set; sflags="${i#-}$sflags";
shift;;
-o)
echo oarg is "'"$2"'"; oarg="$2"; shift;
shift;;
--)
shift; break;;
esac
done
echo single-char flags: "'"$sflags"'"
echo oarg is "'"$oarg"'"
This code will accept any of the following as equivalent:
cmd -aoarg file file
cmd -a -o arg file file
cmd -oarg -a file file
cmd -a -oarg -- file file
In bash there is some implicit variable:
$#: contains number of arguments for a called script/function
$0: contains names of script/function
$1: contains first argument
$2: contains second argument
...
$n: contains n-th argument
For example:
#!/bin/ksh
if [ $# -ne 2 ]
then
echo "Wrong number of argument - expected 2 : $#"
else
echo "Argument list:"
echo "\t$0"
echo "\t$1"
echo "\t$2"
fi
My solution:
#!/bin/bash
count=0
skip=0
flag="no flag"
list=($#) #put args in array
for arg in $# ; do #iterate over array
count=$(($count+1)) #update counter
if [ $skip -eq 1 ]; then #check if we have to skip this args
skip=0
continue
fi
opt=${arg:0:2} #get only first 2 chars as option
if [ $opt == "-a" ]; then #check if option equals "-a"
if [ $opt == $arg ] ; then #check if this is only the option or has a flag
if [ ${list[$count]:0:1} != "-" ]; then #check if next arg is an option
skip=1 #skip next arg
flag=${list[$count]} #use next arg as flag
fi
else
flag=${arg:2} #use chars after "-a" as flag
fi
fi
done
echo $flag

Resources