Once a bash program is executed while processing options in getops
, the loop exits.
As a short example, I have the following bash script:
#!/usr/bin/env bash
while getopts ":a:l:" opt; do
case ${opt} in
a)
ls -a $2
;;
l)
ls -l $2
;;
\?)
echo "Invalid option: -$OPTARG" >&2
exit 1
;;
:)
echo "Option -$OPTARG requires an argument" >&2
exit 1
;;
esac
done
echo -e "\nTerminated"
If the script is called test.sh
, when I execute the script with this command, I get the following output, where only the -a
flag is processed, and -l
is ignored:
$ ./test.sh -al .
. .. file1.txt file2.txt test.sh
Terminated
However, if I remove the colons after each argument, indicating that operands are not required for each argument, then the script does as intended. If the while
loop is changed to:
while getopts ":al" opt; do
Then, running my script gives the following output (with both -a
and -l
processed):
$ ./test.sh -al .
. .. file1.txt file2.txt test.sh
total 161
-rwxrwxrwx 1 root root 0 Nov 24 22:31 file1.txt
-rwxrwxrwx 1 root root 0 Nov 24 22:32 file2.txt
-rwxrwxrwx 1 root root 318 Nov 24 22:36 test.sh
Terminated
Additionally, adding something like OPTIND=1
to the end of my loop only causes an infinite loop of the script executing the first argument.
How can I get getopts
to parse multiple arguments with option arguments (:
after each argument)?
Speaking about short options only, there is no need for a space between an option and its argument, so -o something
equals to -osomething
. Although it's very common to separate them, there are some exceptions like: cut -d: -f1
.
Just like @AlexP said, if you use while getopts ":a:l:" opt
, then options -a
and -l
are expected to have an argument. When you pass -al
to your script and you make the option -a
to require an argument, getopts
looks for it and basically sees this: -a l
which is why it ignores the -l
option, because -a
"ate it".
Your code is a bit messy and as @cdarke suggested, it doesn't use the means provided by getopts
, such as $OPTARG
. You might want to check this getopts tutorial.
If I understand correctly, your main goal is to check that a file/folder has been passed to the script for ls
. You will achieve this not by making the options require an argument, but by checking whether there is a file/folder after all the options. You can do that using this:
#!/usr/bin/env bash
while getopts ":al" opt; do
case ${opt} in
a) a=1 ;;
l) l=1 ;;
\?) echo "Invalid option: -$OPTARG" >&2; exit 1 ;;
:) echo "Option -$OPTARG requires an argument" >&2; exit 1 ;;
esac
done
shift $(( OPTIND - 1 ));
[[ "$#" == 0 ]] && { echo "No input" >&2; exit 2; }
input=("$@")
[[ "$a" == 1 ]] && ls -a "${input[@]}"
[[ "$l" == 1 ]] && ls -l "${input[@]}"
echo Done
This solution saves your choices triggered by options to variables (you can use an array instead) and later on decide based on those variables. Saving to variables/array gives you more flexibility as you can use them anywhere within the script.
After all the options are processed, shift $(( OPTIND - 1 ));
discards all options and associated arguments and leaves only arguments that do not belong to any options = your files/folders. If there aren't any files/folders, you detect that with [[ "$#" == 0 ]]
and exit. If there are, you save them to an array input=("$@")
and use this array later when deciding upon your variables:
[[ "$a" == 1 ]] && ls -a "${input[@]}"
[[ "$l" == 1 ]] && ls -l "${input[@]}"
Also, unlike ls -a $2
, using an array ls -a "${input[@]}"
gives you the possibility to pass more than just one file/folder: ./test.sh -la . "$HOME"
.