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

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

Related

Bash: Extract remaining unflagged arguments (when using getopts)

I'm using the getopts command to process the flags in my bash script, using the structure found in this answer: https://stackoverflow.com/a/21128172/2230446
#!/bin/bash
t_flag='30'
print_usage() {
printf "Usage: ..."
}
while getopts 't:' flag; do
case "${flag}" in
t) t_flag="${OPTARG}" ;;
*) print_usage
exit 1 ;;
esac
done
echo "${t_flag}"
I run the script with the following command:
./test.sh -t dog cat fish
My goal is to extract cat fish to a variable so I can use it in the rest of the script, similar to how the t_flag was extracted to a variable.
Discard -t dog after parsing it, e.g:
shift $((OPTIND-1))
Then only cat and fish will be left as positional parameters. To extract them to a variable:
var=$*

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

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

Bash: handling mass arguments

I'd like to be able to handle multiple arguments to a given flag no matter what the order of flags is. Do you guys think this is acceptable? Any improvements?
So:
$ ./script -c opt1 opt2 opt3 -b foo
opt1 opt2 opt3
foo
Code:
echo_args () {
echo "$#"
}
while (( $# > 0 )); do
case "$1" in
-b)
echo $2
;;
-c|--create)
c_args=()
# start looping from this flag
for arg in ${#:2}; do
[ "${arg:0:1}" == "-" ] && break
c_args+=("$arg")
done
echo_args "${c_args[#]}"
;;
*)
echo "huh?"
;;
esac
shift 1
done
The getopts utility shall retrieve options and option-arguments from a list of parameters.
$ cat script.sh
cflag=
bflag=
while getopts c:b: name
do
case $name in
b) bflag=1
bval="$OPTARG";;
c) cflag=1
cval="$OPTARG";;
?) printf "Usage: %s: [-c value] [-b value] args\n" $0
exit 2;;
esac
done
if [ ! -z "$bflag" ]; then
printf 'Option -b "%s" specified\n' "$bval"
fi
if [ ! -z "$cflag" ]; then
printf 'Option -c "%s" specified\n' "$cval"
fi
shift $(($OPTIND - 1))
printf "Remaining arguments are: %s\n" "$*"
Note the Guideline 8:
When multiple option-arguments are specified to follow a single option, they should be presented as a single argument, using commas within that argument or <blank>s within that argument to separate them.
$ ./script.sh -c "opt1 opt2 opt3" -b foo
Option -b "foo" specified
Option -c "opt1 opt2 opt3" specified
Remaining arguments are:
The standard links are listed below:
getopts - parse utility options
Section 12.2 Utility Syntax Guidelines
I noticed in the comments that you don't want to use any of these. What you could do is set all of the arguments as a string, then sort them using a loop, pulling out the ones you want to set as switched and sorting them using if statements. It is a little brutish, but it can be done.
#!/bin/bash
#set all of the arguments as a variable
ARGUMENTS=$#
# Look at each argument and determine what to do with it.
for i in $ARGUMENTS; do
# If the previous loop was -b then grab the value of this argument
if [[ "$bgrab" == "1" ]]; then
#adds the value of -b to the b string
bval="$bval $i"
bgrab="0"
else
# If this argument is -b, prepare to grab the next argument and assign it
if [[ "$i" == "-b" ]]; then
bgrab="1"
else
#Collect the remaining arguments into one list per your example
RemainingArgs="$RemainingArgs $i"
fi
fi
done
echo "Arguments: $RemainingArgs"
echo "B Value: $bval"
I use something similar in a lot of my scripts because there are a significant amount of arguments that can be fed into some of them, and the script needs to look at each one to figure out what to do. They can be out of order or not exist at all and the code still has to work.

How do I access arguments to functions if there are more than 9 arguments?

With first 9 arguments being referred from $1-$9, $10 gets interpreted as $1 followed by a 0. How do I account for this and access arguments to functions greater than 10?
Thanks.
Use :
#!/bin/bash
echo ${10}
To test the difference with $10, code in foo.sh :
#!/bin/bash
echo $10
echo ${10}
Then :
$ ./foo.sh first 2 3 4 5 6 7 8 9 10
first0
10
the same thing is true if you have :
foobar=42
foo=FOO
echo $foobar # echoes 42
echo ${foo}bar # echoes FOObar
Use {} when you want to remove ambiguities ...
my2c
If you are using bash, then you can use ${10}.
${...} syntax seems to be POSIX-compliant in this particular case, but it might be preferable to use the command shift like that :
while [ "$*" != "" ]; do
echo "Arg: $1"
shift
done
EDIT: I noticed I didn't explain what shift does. It just shift the arguments of the script (or function). Example:
> cat script.sh
echo "$1"
shift
echo "$1"
> ./script.sh "first arg" "second arg"
first arg
second arg
In case it can help, here is an example with getopt/shift :
while getopts a:bc OPT; do
case "$OPT" in
'a')
ADD=1
ADD_OPT="$OPTARG"
;;
'b')
BULK=1
;;
'c')
CHECK=1
;;
esac
done
shift $( expr $OPTIND - 1 )
FILE="$1"
In general, to be safe that the whole of a given string is used for the variable name when Bash is interpreting the code, you need to enclose it in braces: ${10}

Extract parameters before last parameter in "$#"

I'm trying to create a Bash script that will extract the last parameter given from the command line into a variable to be used elsewhere. Here's the script I'm working on:
#!/bin/bash
# compact - archive and compact file/folder(s)
eval LAST=\$$#
FILES="$#"
NAME=$LAST
# Usage - display usage if no parameters are given
if [[ -z $NAME ]]; then
echo "compact <file> <folder>... <compressed-name>.tar.gz"
exit
fi
# Check if an archive name has been given
if [[ -f $NAME ]]; then
echo "File exists or you forgot to enter a filename. Exiting."
exit
fi
tar -czvpf "$NAME".tar.gz $FILES
Since the first parameters could be of any number, I have to find a way to extract the last parameter, (e.g. compact file.a file.b file.d files-a-b-d.tar.gz). As it is now the archive name will be included in the files to compact. Is there a way to do this?
To remove the last item from the array you could use something like this:
#!/bin/bash
length=$(($#-1))
array=${#:1:$length}
echo $array
Even shorter way:
array=${#:1:$#-1}
But arays are a Bashism, try avoid using them :(.
Portable and compact solutions
This is how I do in my scripts
last=${#:$#} # last parameter
other=${*%${!#}} # all parameters except the last
EDIT
According to some comments (see below), this solution is more portable than others.
Please read Michael Dimmitt's commentary for an explanation of how it works.
last_arg="${!#}"
Several solutions have already been posted; however I would advise restructuring your script so that the archive name is the first parameter rather than the last. Then it's really simple, since you can use the shift builtin to remove the first parameter:
ARCHIVENAME="$1"
shift
# Now "$#" contains all of the arguments except for the first
Thanks guys, got it done, heres the final bash script:
#!/bin/bash
# compact - archive and compress file/folder(s)
# Extract archive filename for variable
ARCHIVENAME="${!#}"
# Remove archive filename for file/folder list to backup
length=$(($#-1))
FILES=${#:1:$length}
# Usage - display usage if no parameters are given
if [[ -z $# ]]; then
echo "compact <file> <folder>... <compressed-name>.tar.gz"
exit
fi
# Tar the files, name archive after last file/folder if no name given
if [[ ! -f $ARCHIVENAME ]]; then
tar -czvpf "$ARCHIVENAME".tar.gz $FILES; else
tar -czvpf "$ARCHIVENAME".tar.gz "$#"
fi
Just dropping the length variable used in Krzysztof Klimonda's solution:
(
set -- 1 2 3 4 5
echo "${#:1:($#-1)}" # 1 2 3 4
echo "${#:(-$#):($#-1)}" # 1 2 3 4
)
I would add this as a comment, but don't have enough reputation and the answer got a bit longer anyway. Hope it doesn't mind.
As #func stated:
last_arg="${!#}"
How it works:
${!PARAM} indicates level of indirection. You are not referencing PARAM itself, but the value stored in PARAM ( think of PARAM as pointer to value ).
${#} expands to the number of parameters (Note: $0 - the script name - is not counted here).
Consider following execution:
$./myscript.sh p1 p2 p3
And in the myscript.sh
#!/bin/bash
echo "Number of params: ${#}" # 3
echo "Last parameter using '\${!#}': ${!#}" # p3
echo "Last parameter by evaluating positional value: $(eval LASTP='$'${#} ; echo $LASTP)" # p3
Hence you can think of ${!#} as a shortcut for the above eval usage, which does exactly the approach described above - evaluates the value stored in the given parameter, here the parameter is 3 and holds the positional argument $3
Now if you want all the params except the last one, you can use substring removal ${PARAM%PATTERN} where % sign means 'remove the shortest matching pattern from the end of the string'.
Hence in our script:
echo "Every parameter except the last one: ${*%${!#}}"
You can read something in here: Parameter expansion
Are you sure this fancy script is any better than a simple alias to tar?
alias compact="tar -czvpf"
Usage is:
compact ARCHIVENAME FILES...
Where FILES can be file1 file2 or globs like *.html
Try:
if [ "$#" -gt '0' ]; then
/bin/echo "${!#}" "${#:1:$(($# - 1))}
fi
Array without last parameter:
array=${#:1:$#-1}
But it's a bashism :(. Proper solutions would involve shift and adding into variable as others use.
#!/bin/bash
lastidx=$#
lastidx=`expr $lastidx - 1`
eval last='$'{$lastidx}
echo $last
Alternative way to pull the last parameter out of the argument list:
eval last="\$$#"
eval set -- `awk 'BEGIN{for(i=1;i<'$#';i++) printf " \"$%d\"",i;}'`
#!/bin/sh
eval last='$'$#
while test $# -gt 1; do
list="$list $1"
shift
done
echo $list $last
I can't find a way to use array-subscript notation on $#, so this is the best I can do:
#!/bin/bash
args=("$#")
echo "${args[$(($#-1))]}"
This script may work for you - it returns a subrange of the arguments, and can be called from another script.
Examples of it running:
$ args_get_range 2 -2 y a b "c 1" d e f g
'b' 'c 1' 'd' 'e'
$ args_get_range 1 2 n arg1 arg2
arg1 arg2
$ args_get_range 2 -2 y arg1 arg2 arg3 "arg 4" arg5
'arg2' 'arg3'
$ args_get_range 2 -1 y arg1 arg2 arg3 "arg 4" arg5
'arg2' 'arg3' 'arg 4'
# You could use this in another script of course
# by calling it like so, which puts all
# args except the last one into a new variable
# called NEW_ARGS
NEW_ARGS=$(args_get_range 1 -1 y "$#")
args_get_range.sh
#!/usr/bin/env bash
function show_help()
{
IT="
Extracts a range of arguments from passed in args
and returns them quoted or not quoted.
usage: START END QUOTED ARG1 {ARG2} ...
e.g.
# extract args 2-3
$ args_get_range.sh 2 3 n arg1 arg2 arg3
arg2 arg3
# extract all args from 2 to one before the last argument
$ args_get_range.sh 2 -1 n arg1 arg2 arg3 arg4 arg5
arg2 arg3 arg4
# extract all args from 2 to 3, quoting them in the response
$ args_get_range.sh 2 3 y arg1 arg2 arg3 arg4 arg5
'arg2' 'arg3'
# You could use this in another script of course
# by calling it like so, which puts all
# args except the last one into a new variable
# called NEW_ARGS
NEW_ARGS=\$(args_get_range.sh 1 -1 \"\$#\")
"
echo "$IT"
exit
}
if [ "$1" == "help" ]
then
show_help
fi
if [ $# -lt 3 ]
then
show_help
fi
START=$1
END=$2
QUOTED=$3
shift;
shift;
shift;
if [ $# -eq 0 ]
then
echo "Please supply a folder name"
exit;
fi
# If end is a negative, it means relative
# to the last argument.
if [ $END -lt 0 ]
then
END=$(($#+$END))
fi
ARGS=""
COUNT=$(($START-1))
for i in "${#:$START}"
do
COUNT=$((COUNT+1))
if [ "$QUOTED" == "y" ]
then
ARGS="$ARGS '$i'"
else
ARGS="$ARGS $i"
fi
if [ $COUNT -eq $END ]
then
echo $ARGS
exit;
fi
done
echo $ARGS
This works for me, with sh and bash:
last=${*##* }
others=${*%${*##* }}

Resources