bash - getopts only parses the first argument if operands are required - bash

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".

Related

Parse combination of command-line arguments and flags in bash

I am trying to write bash script which will read multiple filenames and a target directory, which is optional.
./myfile -t /home/users/ file1 file2
I have tried the following code, but I am not able to handle different scenarios mentioned below:
while getopts "t:" opt; do
case $opt in
t)
echo "-t was triggered, Parameter: $OPTARG"
;;
\?)
echo "Invalid option: -$OPTARG"
exit 1
;;
:)
echo "Option -$OPTARG requires an argument."
exit 1
;;
esac
done
But the code should handle different scenarios like:
./myfile file1 file2 -t /home/users/,
./myfile file1 -t /home/users/ file2 file3,
./myfile file1 file2 file3 file4
and should be able to read the files.
Using a while loop to read and shift the arguments might be easier in this case.
In the example below the arguments are looped through to look for the string -t in which case the arguments array is shifted one step and the now nr 1 index is supposed to be the optional homedir. In all the other cases the item is moved to another array called files.
#! /bin/bash
files=()
homedir=
while (( $# > 0 )); do
case "$1" in
-t )
shift
homedir="$1"
;;
* )
files+=("$1")
;;
esac
shift
done
echo "${files[#]}"
echo "$homedir"
After your while loop, you need to shift out any options and their arguments. This works even if there aren't any flags/flag arguements.
shift $(($OPTIND - 1))
Then the rest of the arguments are available in "$#" and can be dealt with in any of the usual ways. For example:
for arg in "$#"
do
something_with "$arg"
done
For some more information, see my answer here.

How to process more than 2 arguments in Bash? [duplicate]

This question already has answers here:
An example of how to use getopts in bash
(8 answers)
Closed 3 years ago.
So, I want to pass 2 arguments and want those arguments to be processed in combination and separately too. I have coded the switch case to process them separately but I am not sure how to do process them together.
$ sh match_the_pattern.sh -a 6 -b 5 words.txt
Here,
-a: at least number of letters specified.
-b: at most number of letters specified.
What I am looking for?
when I get -a and -b together, first the script should list all the words which have at least given number of words, save, then out of those, it should process with the -b, that is the most number of letters in the result we got from the -a, then print the count.
This is a double filter, it is not like you posted both of them individually.
while [[ $# -gt 0 ]]; do
case $1 in
-a)
argument=$
egrep "^[[:alnum:]]{$argument,}$" $dictionyname | wc -l
shift ;;
-b)
arg=$2
egrep "^[[:alnum:]]{0,$argument}$" $dictionyname | wc -l
shift ;;
esac
shift
done
$ sh match_the_pattern.sh -a 6 -b 5 words.txt
7000
$ sh match_the_pattern.sh -a 6 words.txt
115690
$ sh match_the_pattern.sh -b 5 words.txt
12083
Note, I do know how to process -a and -b, I do not know how to combine the results of -a and -b when passed together in the argument...
If right now, I pass the command, I am getting this output :
$ sh match_the_pattern.sh -a 6 -b 5 words.txt
115690
12083
So it is processing a and b but giving the results one after another.
Set variables based on the parameters, then use them after the loop.
min=
max=
while [[ $# -gt 0 ]]; do
case $1 in
-a)
min=$2
shift ;;
-b)
max=$2
shift ;;
-*)
echo "Unknown option $1"
exit 1 ;;
*)
break ;;
esac
shift
done
if [[ -z $min && -z $max ]]
then
echo "At least one of -a and -b must be used"
exit 1
fi
egrep "^[[:alnum:]]{${min:-1},$max}$" "$#" | wc -l
${min:-1} means to use the value of $min, but if it's empty use 1 instead. So if they don't give -a, it will be {1,$max}.

The advantage of shift in shell script over reassign value straightforward

I don't understand the shift in following code:
#! /usr/local/bin/bash
# process command line options
interactive=
filename=
while [[ -n $1 ]]; do
case $1 in
-f | --file) shift #don't understand the shift #No.1
filename=$1 ;;
-i | --interactive) interactive=1
;;
-h | --help) usage
exit;;
*) echo "usage >&2 exit 1";;
esac
shift # don't understand the shift #2
done
#interactive mode
if [[ -n $interactive ]]; then
echo "do something"
fi
#output html page
if [[ -n $filename ]]; then
if touch $filename && [[ -f $filename ]]; then
echo "write_html_page > $filename" #debug
else
echo "$program: Cannot write file $filename " >&2
exit 1
fi
else
echo "write_html_page to terminal" # debug
fi
Test it
$ bash question.sh -f test
write_html_page > test
$ bash question.sh -f
write_html_page to terminal
When I delete shift and change filename=$1 to filename=$2
$ bash question.sh -f
write_html_page to terminal
# it works properly
$ bash question.sh -f test
usage >&2 exit 1
write_html_page > test
# it almost function nicely except that `usage >&2 exit 1` is executed.
So the shift cannot be replaced by filename=$2 entirely.
The second shift at the botton if deleted, the loop run endlessly.
Could I interpret shift intuitively?
I did't find such a magic command in other languages.
shift will remove the first positional argument, and shift every other argument left one.
For example, let's consider the following:
#!/bin/bash
echo "$#"
shift
echo "$#"
shift
echo "$#"
Given that echo "$#" will print all of the arguments, if you were to run this, then the following would happen:
./test.bash 1 2 3 4 5
echo "$#" # prints 1 2 3 4 5
shift # Removes 1 and shifts everything else along
echo "$#" # prints 2 3 4 5
shift # shifting again
echo "$#" # prints 3 4 5
In your example, the script is parsing all of the flags. -i and -h are just switches and handle no following arguments. However, -f requires a filename.
The second shift will process the flag, shift the arguments, and then process them again. Therefore you can have ./program.bash -i -f filename. The -i will be shifted by the second shift, and then the filename will be processed on the next loop.
If you were to run ./program.bash -f filename -i, then the filename would need to be shifted along with the -f. Therefore, on the case block for -f there is an extra shift. In this example, -f would be shifted inside the case block, and then filename would be shifted by the second shift. Then the loop would run again to process any further flags.
As the while loop is [[ -n $1 ]], the loop will run until there are no more arguments.
The example on this page:
http://tldp.org/LDP/Bash-Beginners-Guide/html/sect_09_07.html
explains what it's doing.
EDIT:
The example:
A shift statement is typically used when the number of arguments to a command is not known in advance, for instance when users can give as many arguments as they like. In such cases, the arguments are usually processed in a while loop with a test condition of (( $# )). This condition is true as long as the number of arguments is greater than zero. The $1 variable and the shift statement process each argument. The number of arguments is reduced each time shift is executed and eventually becomes zero, upon which the while loop exits.

How to combine getopts and positional parameters in bash? [duplicate]

This question already has answers here:
Is mixing getopts with positional parameters possible?
(9 answers)
Closed 5 years ago.
I want to use both getopts and positional parameters, but if I pass in a positional parameter to the program the getopts get lost.
directory=$1
while getopts l: flag; do
case "$flag" in
l) level=$OPTARG;;
esac
done
if [ -n "$level" ]; then
echo "Level exist!"
else
echo "Level doesn't exist!"
fi
So when I run the program like this:
sh myprogram.sh ~/documents -l 2
I expect:
Level exist!
And instead it returns:
Level doesn't exist!
The thing is, if I run the program without the positional parameter (~/documents) like this:
sh myprogram.sh -l 2
I get the correct output:
Level exist!
Why is that? How can I use both positional parameters and getopts in bash?
Thanks!
Most tools are written in the form: tool [options] arg ...
So you would do this:
# first, parse the options:
while getopts l: flag; do
case "$flag" in
l) level=$OPTARG;;
\?) exit 42;;
esac
done
# and shift them away
shift $((OPTIND - 1))
# validation
if [ -n "$level" ]; then
echo "Level exist!"
else
echo "Level doesn't exist!"
fi
# THEN, access the positional params
echo "there are $# positional params remaining"
for ((i=1; i<=$#; i++)); do
printf "%d\t%s\n" $i "${!i}"
done
Use the \? to abort the script if the user provides an unknown option or fails to provide a required argument
And invoke it like:
$ bash test.sh
Level doesn't exist!
there are 0 positional params remaining
$ bash test.sh -l 2
Level exist!
there are 0 positional params remaining
$ bash test.sh -l 2 foo bar
Level exist!
there are 2 positional params remaining
1 foo
2 bar
$ bash test.sh -x
test.sh: illegal option -- x
$ bash test.sh -l
test.sh: option requires an argument -- l
But you cannot put the options after the arguments: getopts stops when the first non-option argument is found
$ bash test.sh foo bar -l 2
Level doesn't exist!
there are 4 positional params remaining
1 foo
2 bar
3 -l
4 2

How to create a bash script that takes arguments?

I already know about getopts, and this is fine, but it is annoying that you have to have a flag even for mandatory arguments.
Ideally, I'd like to be able to have a script which receives arguments in this form:
script.sh [optional arguments] [anything required]
for example
script.sh -rvx output_file.txt
where the script says you HAVE to have an output file. Is there any easy way to do this?
As far as I know, with getopts it would have to look like: script.sh -rvx -f output_file.txt, and that is just not very clean.
I can also use python if necessary, but only have 2.4 available, which is a bit dated.
Don't use the getopts builtin, use getopt(1) instead. They are (subtly) different and do different things well. For you scenario you could do this:
#!/bin/bash
eval set -- $(getopt -n $0 -o "-rvxl:" -- "$#")
declare r v x l
declare -a files
while [ $# -gt 0 ] ; do
case "$1" in
-r) r=1 ; shift ;;
-v) v=1 ; shift ;;
-x) x=1 ; shift ;;
-l) shift ; l="$1" ; shift ;;
--) shift ;;
-*) echo "bad option '$1'" ; exit 1 ;;
*) files=("${files[#]}" "$1") ; shift ;;
esac
done
if [ ${#files} -eq 0 ] ; then
echo output file required
exit 1
fi
[ ! -z "$r" ] && echo "r on"
[ ! -z "$v" ] && echo "v on"
[ ! -z "$x" ] && echo "x on"
[ ! -z "$l" ] && echo "l == $l"
echo "output file(s): ${files[#]}"
EDIT: for completeness I have provided an example of handling an option requiring an argument.
If you are using getops, just shift by $OPTIND-1 after your case statement. Then what is left in $* will be everything else, which is probably what you want.
shift $(( ${OPTIND} - 1 )); echo "${*}"
You're are suffering from illusions; using getopts does not require mandatory arguments prefixed by a flag letter. I tried to find a suitable example from my corpus of scripts; this is a semi-decent approximation. It is called rcsunco and is used to cancel a checkout from RCS. I haven't modified it in a while, I see; I use it quite often (because I haven't migrated from RCS completely, yet).
#!/bin/sh
#
# "#(#)$Id: rcsunco.sh,v 2.1 2002/08/03 07:41:00 jleffler Exp $"
#
# Cancel RCS checkout
# -V print version number
# -n do not remove or rename checked out file (like SCCS unget) (default)
# -r remove checked out file (default)
# -k keep checked out file as $file.keep
# -g checkout (unlocked) file after clean-up
# -q quiet checkout
: ${RCS:=rcs}
: ${CO:=co}
remove=yes
keep=no
get=no
quiet=
while getopts gknqrV opt
do
case $opt in
V) echo "`basename $0 .sh`: RCSUNCO Version $Revision: 2.1 $ ($Date: 2002/08/03 07:41:00 $)" |
rcsmunger
exit 0;;
g) get=yes;;
k) keep=yes;;
n) remove=no;;
q) quiet=-q;;
r) remove=yes;;
*) echo "Usage: `basename $0 .sh` [-{n|g}][-{r|k}] file [...]" 1>&2
exit 1;;
esac
done
shift $(($OPTIND-1))
for file in $*
do
rfile=$(rfile $file)
xfile=$(xfile $rfile)
if $RCS -u $rfile
then
if [ $keep = yes ]
then
if [ -f $xfile ]
then
mv $xfile $xfile.keep
echo "$xfile saved in $xfile.keep"
fi
elif [ $remove = yes ]
then rm -f $xfile
fi
if [ $get = yes ] && [ $remove = yes -o $keep = yes ]
then $CO $quiet $rfile
fi
fi
done
It's only a semi-decent approximation; the script quietly does nothing if you don't supply any file names after the optional arguments. However, if you need to, you can check that the mandatory arguments are present after the 'shift'. Another script of mine does have mandatory arguments. It contains:
...
shift $(($OPTIND - 1))
case $# in
2) case $1 in
install) MODE=Installation;;
uninstall) MODE=Uninstallation;;
*) usage;;
esac;;
*) usage;;
esac
So, that command (jlss) can take optional arguments such as -d $HOME, but requires either install or uninstall followed by the name of something to install. The basic mode of use is:
jlss install program
But the optional mode is:
jlss -d $HOME -u me -g mine -x -p install program
I didn't show all of jlss because it has about 12 options - it isn't as compact as rcsunco.
If you were dealing with mandatory arguments before option arguments, then you'd have to do a bit more work:
You'd pick up the mandatory arguments, shifting them out of the way.
Then you process the optional arguments with the flags.
Finally, if appropriate, you handle the extra 'file name' arguments.
If you are dealing with mandatory arguments interspersed with option arguments (both before and after the mandatory ones), then you have still more work to do. This is used by many VCS systems; CVS and GIT both have the facility:
git -global option command [-sub option] [...]
Here, you run one getopts loop to get the global options; pick up the mandatory arguments; and run a second getopts loop to get the sub-options (and maybe run a final loop over the 'file name' arguments).
Isn't life fun?
And I heard a completely opposite thing, that you shouldn't use getopt, but the getopts builtin.
Cross-platform getopt for a shell script
Never use getopt(1). getopt cannot handle empty arguments strings, or
arguments with embedded whitespace. Please forget that it ever
existed.
The POSIX shell (and others) offer getopts which is safe to use
instead.
Here's yet another way to "Option-ize your shell scripts" (whithout using getopt or getopts):
http://bsdpants.blogspot.com/2007/02/option-ize-your-shell-scripts.html

Resources