I am trying to parse some arguments in a bash script with the following code:
for arg in "$#"
do
case $arg in
-i|--initialize)
SHOULD_INITIALIZE=1
shift # Remove --initialize from processing
;;
-r|--root)
ROOT_DIRECTORY="$2"
shift # Remove argument name from processing
shift # Remove argument value from processing
;;
*)
echo "Error, Invalid argument $arg"
exit 1
;;
esac
done
However, when calling the function as expected
bash script.sh -i -r /some/directory
The result is always
Error /some/directory
It seems like the "/some/directory" string is reentering the switch statement. Isn't that the whole purpose of the shifts? I am not that well versed in Bash, so any help would be appreciated.
Thanks!
shift doesn't affect what value will be assigned to arg next, because the for loop effectively "freezes" the set of values to iterate over.
Use a while loop instead:
while [ $# -gt 0 ]; do
case $1 in
-i|--initialize)
SHOULD_INITIALIZE=1
shift
;;
-r|--root)
ROOT_DIRECTORY="$2"
shift # Remove argument name from processing
shift || { echo "Error, no root directory given"; exit 1; }
;;
*)
echo "Error, Invalid argument $1"
exit 1
;;
esac
done
Unlike the for loop, the while loop re-evaluates its condition at the start of each iteration, which includes re-evaluating the value of $# after any previous shifts.
Note that shift will fail if there is no argument to shift, so you can use its exit status to check if $2 actually existed.
Related
I wrote a bash script that reads script arguments and pass them to parsearg function:
#!/usr/bin/env bash
function main() {
parseargs2 "$#"
}
function parseargs2() {
MAINCOMMAND=$1
shift
while [ $# -gt 0 ]; do
case $1 in
-s|--service) SERVICE_NAME="$2" ;;
-r|--registry) REGISTRY="$2" ;;
-h|--help) HELP=true ;;
*) echo "help" && exit 1;;
esac
shift
done
echo "SERVICE_NAME: $SERVICE_NAME"
echo "REGISTRY: $REGISTRY"
echo "HELP: $HELP"
echo "cmd: $MAINCOMMAND"
}
main "$#"
now when I run my script it always executes help command and then exits, I don't know why it will be ok when I remove *) case
./example.sh build --service api --registry dockerhub
EDIT:
thanks to #chepner comment I found the problem I solved this by adding shift 2 at end of while loop
Doing your own option parsing is a little fraught, but the idea should be to do an extra shift whenever you have an argument:
while [ $# -gt 0 ]; do
case $1 in
-s|--service) SERVICE_NAME="$2" ; shift;;
-r|--registry) REGISTRY="$2" ; shift;;
-h|--help) HELP=true ;;
*) echo "help" && exit 1;;
esac
shift
done
You may or may not want to support a style where the single-letter version of an option has its argument jammed up against it, which works in many stock programs:
while [ $# -gt 0 ]; do
case $1 in
-s|--service) SERVICE_NAME="$2" ; shift;;
-s*) SERVICE_NAME=${1#-s};;
-r|--registry) REGISTRY="$2" ; shift;;
-r*) REGISTRY=${1#-r};;
-h|--help) HELP=true ;;
*) echo "help" && exit 1;;
esac
shift
done
Also, there's no reason to name your variables in all-caps. All caps is usually a sign that a variable is being exported into the environment so some other program you're going to run can see it; otherwise, just use lowercase. Additionally, you don't need quotation marks on the right-hand side of an assignment, and if you're really using bash (rather than some other POSIX type shell), you likely want to use ((...)) for numeric comparisons.
help= # don't assume variables aren't set on entry
maincommand=$1
shift
while (( $# )); do
case "$1" in
-s|--service) service_name=$2 ; shift;;
-s*) service_name=${1#-s};;
-r|--registry) registry=$2 ; shift;;
-r*) registry=${1#-r};;
-h|--help) help=true ;;
*) echo "help" && exit 1;;
esac
shift
done
for var in service_name registry help maincommand; do
printf '%s: "%s"\n' "$var" "${!var}"
done
FWIW, true is just a string to the shell with no special significance; the shell doesn't have Boolean values beyond "command succeeded/failed" (which is just zero/nonzero on the exit code) or Boolean operators other than the ones that operate on commands (e.g. command1 && run this if command1 succeeded). So when flags are stored in shell variables the Booleanness is usually represented either by empty vs. nonempty string (and true is a fine value to use for the nonempty case) or 0 vs. 1 (or 0 vs nonzero) number.
I have a bashscript.sh that I $ chmod +x bashscript.sh and move it to $ mv bashscript.sh ~/.local/bin/ in order for it to be executable like a system command.
I'd like to be able to invoke it with
bashscript [<-w|-working|--working>[=|:]] <y|yes|n|no>
And return usage/help/error (call it whatever we want) if the call isn't respected.
To do so, I wrote this parsing part:
usage(){
echo "you're wrong."
exit 1
}
[[ $# -lt 1 ]] && usage
options=$(getopt -o y,n,h,w: -l yes,no,help,working: -- "$#")
set -- $options
while true; do
case "$1" in
-h|--help|*) usage
shift;;
y|yes)
#do something
shift;;
n|no)
#..
shift;;
-w|-working|--working)
shift;;
--) #this is just syntax
shift
break;;
esac
done
But when I test it doesn't work as intended*, would you know why/have a sample that handles my option possibilites?
*edit : I always trigger the usage display
edit 2 : removed the spaces around the "=" of options as #costaparas3 pointed out, thank you, still stuck to usage() though
Here are the issues I found:
Exit if there are no arguments
Set the options with options=$(... (no spaces)
-h|--help|*) matches everything so you have an infinite loop. You don't need to match on * as getopt will reuturn non-zero if it finds an invalid argument, and the match on -- is what usually terminates the loop.
getopt returns non-zero for invalid arguments so exit 1 then
Use -n|--no to specify short and long options
--working requires an argument but you only shift 1.
-working is not valid (with getopt). Use either -w or --working.
Here is corrected version:
#!/bin/bash
usage() {
echo "you're wrong."
exit $1
}
[ $# -lt 1 ] && usage
options=$(getopt -o y,n,h,w: -l yes,no,help,working: -- "$#")
[ $? -ne 0 ] && usage 1
# default values
yes=
working=
set -- $options
while :
do
case "$1" in
-h|--help)
usage
;;
-y|--yes)
yes=1
shift
;;
-n|--no)
yes=0 # or no=1
shift
;;
-w|--working)
working=$2
shift 2
;;
--)
break
;;
esac
done
When I run the command like this :
$: ./script -r f1 f2 :
it detects the "-r" flag and sets the recursive flag to 1.
$: ./script directory/ -r :
getopts doesn't detect the -r flag at all. So inside the case statement it never detects -r flag and so the while loop doens't even run at all. how to fix this ?
RECURSIVE_FLAG=0
while getopts ":rR" opt ; do
echo " opt = $opt"
set -x
case "$opt" in
r) RECURSIVE_FLAG=1 ;;
R) RECURSIVE_FLAG=1 ;;
:)echo "not working" ;;
*)echo "Testing *" ;;
esac
done
It has nothing to do with slash. getopts stops processing options when it gets to the first argument that doesn't begin with -. This is the documented behavior:
When the end of options is encountered, getopts exits with a return value greater than zero. OPTIND is set to the index of the first non-option argument, and name is set to ?.
Your claim that it works when you use
./script f1 f2 -r
is simply wrong. I added echo $RECURSIVE_FLAG to the end of your script, and when I ran it that way it echoed 0.
If you want to allow a more liberal syntax, with options after filenames (like GNU rm) you'll need to do some argument parsing of your own. Put your getopts loop inside another loop. When the getopts loop finishes, you can do:
# Find next option argument
while [[ $OPTIND <= $# && ${!OPTIND} != -* ]]; do
((OPTIND++))
done
# Stop when we've run out of arguments
if [[ $OPTIND > $# ]]; then
break
fi
This question already has answers here:
Using getopts to process long and short command line options
(32 answers)
Closed 8 years ago.
I want to use my shell script like this:
myscript.sh -key keyValue
How can I get the keyValue ?
I tried getopts, but it requires the key to be a single letter!
use a manual loop such as:
while :; do
case $1 in
-key)
shift
echo $1
break
;;
*)
break
esac
done
No need to use getopts:
while [ "$#" -gt 0 ]; do
case "$1" in
-key)
case "$2" in
[A-Za-z])
;;
*)
echo "Argument to $1 must be a single letter."
exit 1
;;
esac
keyValue=$2
shift
;;
*)
echo "Invalid argument: $1"
exit 1
;;
esac
shift
done
If your shell is bash it could be simplified like this:
while [[ $# -gt 0 ]]; do
case "$1" in
-key)
if [[ $2 != [A-Za-z] ]]; then
echo "Argument to $1 must be a single letter."
exit 1
fi
keyValue=$2
shift
;;
*)
echo "Invalid argument: $1"
exit 1
;;
esac
shift
done
I really think it's well worth learning getopts: you'll save lots of time in the long run.
If you use getopts then it requires short versions of switches to be a single letter, and prefixed by a single "-"; long versions can be any length, and are prefixed by "--". So you can get exactly what you want using getopts, as long as you're happy with
myscript.sh --key keyValue
This is useful behaviour for getopts to insist on, because it means you can join lots of switches together. If "-" indicates a short single letter switch, then "-key" means the same as "-k -e -y", which is a useful shorthand.
I want to parse the arguments given to a shell script by using a for-loop. Now, assuming I have 3 arguments, something like
for i in $1 $2 $3
should do the job, but I cannot predict the number of arguments, so I wanted use an RegEx for the range and $# as the number of the last argument. I don't know how to use these RegEx' in a for-loop, I tried something like
for i in $[1-$#]
which doesn't work. The loop only runs 1 time and 1-$# is being calculated, not used as a RegEx.
Basic
A for loop by default will loop over the command-line arguments if you don't specify the in clause:
for arg; do
echo "$arg"
done
If you want to be explicit you can get all of the arguments as "$#". The above loop is equivalent to:
for arg in "$#"; do
echo "$arg"
done
From the bash man page:
Special Parameters
$# — Expands to the positional parameters, starting from one. When the expansion occurs within
double quotes, each parameter expands to a separate word. That is, "$#" is equivalent to "$1" "$2" .... If the double-quoted expansion occurs within a word, the expansion of the first
parameter is joined with the beginning part of the original word, and the expansion of the
last parameter is joined with the last part of the original word. When there are no positional parameters, "$#" and $# expand to nothing (i.e., they are removed).
Advanced
For heavy-duty argument processing, getopt + shift is the way to go. getopt will pre-process the command-line to give the user some flexibility in how arguments are specified. For example, it will expand -xzf into -x -z -f. It adds a -- argument after all the flags which separates flags from file names; this lets you do run cat -- -my-file to display the contents of -my-file without barfing on the leading dash.
Try this boilerplate code on for size:
#!/bin/bash
eval set -- "$(getopt -o a:bch -l alpha:,bravo,charlie,help -n "$0" -- "$#")"
while [[ $1 != -- ]]; do
case "$1" in
-a|--alpha)
echo "--alpha $2"
shift 2
;;
-b|--bravo)
echo "--bravo"
shift
;;
-c|--charlie)
echo "--charlie"
shift
;;
-h|--help)
echo "Usage: $0 [-a ARG] [-b] [-c]" >&2
exit 1
;;
esac
done
shift
Notice that each option has a short a long equivalent, e.g. -a and --alpha. The -a flag takes an argument so it's specified as a: and alpha: in the getopt call, and has a shift 2 at the end of its case.
Another way to iterate over the arguments which is closer to what you were working toward would be something like:
for ((i=1; i<=$#; i++))
do
echo "${#:i:1}"
done
but the for arg syntax that John Kugelman showed is by far preferable. There are, however, times when array slicing is useful. Also, in this version, as in John's, the argument array is left intact. Using shift discards its elements.
You should note that what you were trying to do with square brackets is not a regular expression at all.
I suggest doing something else instead:
while [ -n "$1" ] ; do
# Do something with $1
shift
# Now whatever was in $2 is now in $1
done
The shift keyword moves the content of $2 into $1, $3 into $2, etc. pp.
Let's say the arguments where:
a b c d
After a shift, the arguments are now:
b c d
With the while loop, you can thus parse an arbitrary number of arguments and can even do things like:
while [ -n "$1" ] ; do
if [ "$1" = "-f" ] ; then
shift
if [ -n "$1" ] ; then
myfile="$1"
else
echo "-f needs an additional argument"
end
fi
shift
done
Imagine the arguments as being an array and $n being indexes into that array. shift removes the first element, so the index 1 now references the element that was at index 2 prior to shift. I hope you understand what I want to say.