I'm using a variation of the BashFAQ manual loop and I want to convert it to a function and assign the arguments to local variable but I can't figure out the correct syntax. Here's what I have:
function parseArguments() {
local arguments=( "$#" )
while :; do
case ${1:-} in
-d|--debug)
set -o xtrace
# [...more stuff...]
;;
-p|--prompt)
IsPromptEnabled=true
;;
--)
shift
break
;;
-?*)
error ${LINENO} "\"${1:-}\" is an unknown option" 1
;;
*)
break
esac
shift
done
}
parseArguments "$#"
This works fine as is until I try to replace $1 in the loop with the value from arguments. ${arguments[0]} and every other variation I can think of fails, I'd like to understand why (and figure out the solution).
You should loop over the array
function parseArguments() {
local arguments=( "$#" )
for a in "${arguments[#]}"
do
case ${a:-} in
-d|--debug)
set -o xtrace
# [...more stuff...]
;;
-p|--prompt)
IsPromptEnabled=true
;;
--)
# shift
break
;;
-?*)
error ${LINENO} "\"${a:-}\" is an unknown option" 1
;;
*)
break
esac
shift
done
}
parseArguments "$#"
It's because you set the arguments array to the initial list of arguments.
For example, if you call parseArguments -d -p, then you start with $1 set to "-d" and $2 set to "-p". So arguments gets set to ("-d" "-p") as expected.
On the first time through the loop, it recognizes ${1:-} as "-p", sets IsPromptEnabled=true, and shifts that argument away.
So at the end of the first time through the loop, $1 is "-d" and $2 is unset. But arguments hasn't changed, it's still set to ("-d" "-p"). The shift doesn't update it, and it never gets reassigned, so it's always going to have the full list of arguments.
Even after "-d" gets recognized, acted on, and shifted away (making the actual argument list empty), arguments will still be set to ("-d" "-p").
If you want arguments to hold the non-option arguments, you should move the assignment to it after the option-parsing loop, so it gets set after all the options have been shifted away.
Related
After a lot of read, SO or others, I'm really wondering about the best / cleanest way to have a bash script with parameters, optionals with default values.
Here is my script for now:
#!/bin/bash
helpFunction()
{
echo ""
echo "Usage: $0 --reload --mode=[single|cluster]"
echo -e "\t--reload Reload the database : fixtures & schema"
echo -e "\t--mode Mode of build : single or cluster"
exit 1
}
while [[ "$#" -gt 0 ]]; do
case $1 in
-h|--help) helpFunction; shift ;;
-r|--reload) reload=true; shift ;;
-m|--mode) mode="single"; shift ;;
# ... (same format for other required arguments)
*) echo "Unknown parameter passed: $1" ;;
esac
shift
done
./bin/sh/tools/build.sh -e local -m $mode -p local
For now, the $mode variable seems to not be set if I don't set it, how can I have a default value for this variable ? (default is single)
What I want is the user to call the script like (reload is true, mode is cluster):
bin/script.sh -r --mode=cluster
Or by default (reload is false, mode is single):
bin/script.sh
Is this the good way to wait for parameters ? I read other ways, but no real explanations.
Thanks.
There is no universal best/cleanest method; it eventually comes down to what works best for you; some ideas:
set mode to a default value before the while/case loop
after the while/case loop test mode and if unset/undefined then set to a
default value (should probably play it safe and unset mode before the while/case loop though in this case you might as well see idea #1)
pass to build.sh wrapped in double quotes (ie, build.sh -e local -m "$mode" -p local) and have build.sh test for the -m argument being unset/undefined and set to a default value
a variation on #2 and #3 is to use parameter substitution when
passing mode to build.sh, eg: build.sh -e local -m "${mode:-default_value}" -p local
if your script sources a config/ini file you could assign mode a
default value in said config/ini file and then make sure said config/ini file is sourced before the while/case loop
If you set the mode beforehand, that will be its default. If you want parameter with assignment, you need to grab $2 and shift twice.
mode="single"
while [[ "$#" -gt 0 ]]; do
case $1 in
-h|--help) helpFunction; shift ;;
-r|--reload) reload=true; shift ;;
-m|--mode) mode="$2"; shift; shift ;;
# ... (same format for other required arguments)
*) echo "Unknown parameter passed: $1" ;;
esac
shift
done
I think it's also good to restore the positional arguments. Since you may want to use them, but that depends on your script.
mode="single"
reload="false"
POSITIONAL=()
while [[ "$#" -gt 0 ]]; do
case $1 in
-h | --help)
helpFunction
shift
;;
-r | --reload)
reload=true
shift
;;
-m | --mode)
mode="$2"
shift
shift
;;
*)
POSITIONAL+=("$1")
shift
;;
esac
done
set -- "${POSITIONAL[#]}"
COMMAND="$1"
Then you could invoke a function that is called like the $COMMAND and pass the rest of the params with $#, and shift again and set the subcommand to $1. You can do this infinitely to get a chain of (sub)commands.
You can still check if a valid command was provided before you try to call the $COMMAND.
COMMAND_LIST="add issue revoke"
case $COMMAND_LIST in
*"$COMMAND"*)
"$COMMAND" "$#"
;;
*)
helpFunction
echo "ERROR: Unknown command: $COMMAND"
exit 1
;;
esac
But that is of course only if your script supports commands/subcommands. Otherwise, it makes sense to error right away like you did, if there is an unknown parameter.
I have a bash script I wrote with say, 3 command line options, bib, bob and boo... and I want to read in the user options into a bash variable of the same name, which I do as follows:
PARSED_OPTIONS=$(getopt -n $0 --long "bib:,bob:,boo:" -- "$#")
eval set -- "$PARSED_OPTIONS";
while true; do
case "$1" in
--bib)
bib=$2
shift 2;;
--bob)
bob=$2
shift 2;;
--boo)
boo=$2
shift 2 ;;
--)
shift
break;;
esac
done
This all works fine, so far, so good...
But now I want to extend this to a list of many many options, and so rather than writing out a long case statement, it would be really nice to be able to somehow loop over a list of options and automatically pass the options to the variable, something along these lines
opts="bib:,bob:,boo:,"
PARSED_OPTIONS=$(getopt -n $0 --long $opts -- "$#")
for arg in `echo $opts | tr , " "` ; do
eval set -- "$PARSED_OPTIONS";
while true; do
case "$1" in
--${arg})
declare $arg=$2
shift 2
;;
--)
shift
break;;
esac
done
done
I'm using the declaration statement to get the argument into a dynamic variable of the same name (see Dynamic variable names in Bash second solution), and this solution to do the loop over comma separated lists Loop through a comma-separated shell variable but I'm getting an infinite loop here. I think because the 2 unused options are allows as they are in the PARSED_OPTIONS list, but then they are not sliced off in the loop as only "arg" is looked for... I can't see an obvious way around this, but I'm sure there is one.
I realized that I had the shift command still inside the case statement, so that is why it wasn't exiting. I also needed to strip the colon : from the argument, so here is my automated argument retrieval for a bash script that works:
# specify an arbitrary list of arguments:
opts=bib:,bob:,boo:
PARSED_OPTIONS=$(getopt -n $0 --long "${opts}" -- "$#")
for arg in ${opts//,/ } ; do
var=${arg//:} # remove the colon
eval set -- "$PARSED_OPTIONS";
while true ; do
case "$1" in
--${var})
declare ${var}=$2
;;
--)
break
;;
esac
shift 2
done
done
So if you try test_script --boo 3 --bib hello --bob lkkfrfrfr
echo $bib $bob $boo
should give
hello lkkfrfrfr 3
Wrapping another command's parameters
I have a command tool1 that parses arguments this way:
#!/usr/bin/env bash
# ...
while [[ $# -ge 1 ]]
do
key="$1"
case $key in
-o|--option)
OPT="$2"
shift
;;
-u|--user)
USR="$2"
shift
;;
-*)
echo -e "Unrecognized option: \"$key\"" && exit 1
;;
*)
OTHERS+=("$1")
;;
esac
shift
done
# ...
I have tool2 that calls tool1. Thus tool2 will have to pass parameters to tool1. It may also need to process the same parameters (--user)
tool2 looks like:
#!/usr/bin/env bash
# ...
while [[ $# -ge 1 ]]
do
key="$1"
case $key in
-O|--option2)
opt2="$2"
shift
;;
-u|--user)
USR="$2"
OTHERS+=("-u $2")
shift
;;
-*)
echo -e "Unrecognized option: \"$key\"" && exit 1
;;
*)
OTHERS+=("$1")
;;
esac
shift
done
## Call tool1 with other parameters to pass
bash tool1.sh ${OTHERS[#]}
# ...
To sum up
--option2 is an option used only by tool2.
--user is common to both tools, and may be used by tool2 too, before calling tool1.sh. Because of this, in this example --user has to be explicitly passed to tool1 thanks to the array OTHERS.
I'd like to know about possible and/or alternative ways of dealing with such parameter redundancies. A methodology that would help me wrapping another tool's expected parameters/options, without having to copy/paste the lines regarding the parsing of such redundant parameters/options.
tool2's approach is fine. However, you aren't setting OTHERS correctly.
-u|--user)
USR="$2"
OTHERS+=("-u" "$2")
shift
-u and its argument need to remain separate array elements, just as they were separate arguments to tool2. You also need to quote the expansion of OTHERS, to preserve arguments containing word-splitting characters or globs:
bash tool1.sh "${OTHERS[#]}"
Finally, all-uppercase variable names are reserved for use by the shell itself; don't define such names yourself. Just use others instead of OTHERS.
This is my entire script in its simplest form.
#!/bin/bash
src=""
targ=${PWD}
while getopts "s:t:" opt; do
case $opt in
s)
src=$OPTARG
;;
t)
targ=$OPTARG
;;
esac
shift $((OPTIND-1))
done
echo "Source: $src"
echo "Target: $targ"
I run this script as getopts_test -s a -t b
However, it always prints the pwd in front of the Target: and never b
What am I missing here?
The reason for why b is never printed is that the shift within the loop moves the processed options away after the first iteration, i.e. after a has been printed. Use of shift $((OPTIND-1)) is intended to access the possible given variadic parameters. Naturally, once you remove shift, targ gets reassigned to b, and ${PWD} is no longer included in it since you don't have concatenation of the strings (targ and the option of -t) anywhere.
An alternative to what #glenn-jackman suggested in his comment
would be this :
#!/bin/bash
src=""
targ=${PWD}
while getopts "s:t:" opt; do
case $opt in
s)
src=$OPTARG
echo "Source: $src"
;;
t)
targ=$OPTARG
echo "Target: $targ"
;;
esac
done
shift $((OPTIND-1)) # Turning to non-option arguments say a file name and so on.
Here you go with the natural flow of arguments without shifting.
I have a script with a long list of OPTIONAL arguments. some have associated values.
Such as:
.script --first 2012-12-25 --last 2012-12-26 --copy --remove
.script --first 2012-12-25
Thus the following case statement:
for arg in "$#"
do
case $arg in
"--first" )
START_DATE=$arg;;
"--last" )
END_DATE=$arg;;
"--copy" )
COPY=true;;
"--remove" )
REMOVE=true;;
# ... and so on
esac
done
My problem:
that needs a increment $arg+1 type statement to get the following arg (in some cases).
How is that possible?
I'm also happy to do a substring such .script --first2012-12-25 --last2012-12-26
and not sure how to proceed there.
You can allow both --a=arg or -a arg options with a little more work:
START_DATE="$(date '+%Y-%m-%d')";
LAST_DATE="$(date '+%Y-%m-%d')";
while [[ $# -gt 0 ]] && [[ "$1" == "--"* ]] ;
do
opt="$1";
shift; #expose next argument
case "$opt" in
"--" ) break 2;;
"--first" )
START_DATE="$1"; shift;;
"--first="* ) # alternate format: --first=date
START_DATE="${opt#*=}";;
"--last" )
LAST_DATE="$1"; shift;;
"--last="* )
LAST_DATE="${opt#*=}";;
"--copy" )
COPY=true;;
"--remove" )
REMOVE=true;;
"--optional" )
OPTIONAL="$optional_default";; #set to some default value
"--optional=*" )
OPTIONAL="${opt#*=}";; #take argument
*) echo >&2 "Invalid option: $#"; exit 1;;
esac
done
Note the --optional argument uses a default value if "=" is not used, else it sets the value in the normal way.
Use shift in the end of each case statement.
Quote from a bash manual:
shift [n]
The positional parameters from n+1 ... are renamed to $1
.... Parameters represented by the numbers $# down to $#-n+1 are
unset. n must be a non-negative number less than or equal to $#. If
n is 0, no parameters are changed. If n is not given, it is assumed
to be 1. If n is greater than $#, the positional parameters are not
changed. The return status is greater than zero if n is greater than
$# or less than zero; otherwise 0.
$# is an array, & not a simple variable.
You can capture it to a local variable as x=("$#") & then use array x with indices as 0 to ($# - 1).
To access individual elements, use ${x[$i]}. You can NOT directly use ${#[$i]}, however.
So instead of for arg in "$#" loop, you will have i=0; while [ $i -lt $# ]; do loop.
If you have more than one option, and especially options with values mixed with options without values, let getopts do the work for you.
getopts cannot have optional arguments it seems. otherwise great.
my solution
loop the $# and setting a variable equal to x=$arg
do the case switch on that variable (rather than arg)
that worked fine for arguments of the type --startdate 2012-12-25 --enddate 2012-12-29
but did not work for --remove that has no following argument.
therefore tack on stuff (unlikely argument) onto the arg string.
leaving the following
argc="$# jabberwhocky"
echo $argc
x=0
# x=0 for unset variable
for arg in $argc
do
case $x in
"--start" )
STARTDATE=$arg ;;
"--end" )
ENDDATE=$arg ;;
"--copy" )
COPY=true;;
"--remove" )
REMOVE=true;;
... and so on....
esac
x=$arg
done