Variable arguments to a shell script - bash

I would like to have my script accepting variable arguments. How do I check for them individually?
For example
./myscript arg1 arg2 arg3 arg4
or
./myscript arg4 arg2 arg3
The arguments can be any number and in any order. I would like to check if arg4 string is present or not irrespective of the argument numbers.
How do I do that?
Thanks,

The safest way — the way that handles all possibilities of whitespace in arguments, and so on — is to write an explicit loop:
arg4_is_an_argument=''
for arg in "$#" ; do
if [[ "$arg" = 'arg4' ]] ; then
arg4_is_an_argument=1
fi
done
if [[ "$arg4_is_an_argument" ]] ; then
: the argument was present
else
: the argument was not present
fi
If you're certain your arguments won't contain spaces — or at least, if you're not particularly worried about that case — then you can shorten that to:
if [[ " $* " == *' arg4 '* ]] ; fi
: the argument was almost certainly present
else
: the argument was not present
fi

This is playing fast and loose with the typical interpretation of command line "arguments", but I start most of my bash scripts with the following, as an easy way to add --help support:
if [[ "$#" =~ --help ]]; then
echo 'So, lemme tell you how to work this here script...'
exit
fi
The main drawback is that this will also be triggered by arguments like request--help.log, --no--help, etc. (not just --help, which might be a requirement for your solution).
To apply this method in your case, you would write something like:
[[ "$#" =~ arg4 ]] && echo "Ahoy, arg4 sighted!"
Bonus! If your script requires at least one command line argument, you can similarly trigger a help message when no arguments are supplied:
if [[ "${#---help}" =~ --help ]]; then
echo 'Ok first yer gonna need to find a file...'
exit 1
fi
which uses the empty-variable-substitution syntax ${VAR-default} to hallucinate a --help argument if absolutely no arguments were given.

maybe this can help.
#!/bin/bash
# this is myscript.sh
[ `echo $* | grep arg4` ] && echo true || echo false

Related

Bash while() loop :: Does this process cmd line arguments?

I'm a Bash newbie, and I'm puzzling through a Bash script where I see this:
while [ $# -ge 2 ]
if [ "x$1" = "x-a" ]; then
echo "STR_A = $2" >>tmpFile.txt
...do more stuff...
elif [ "x$1" = "x-b" ]; then
echo "STR_B = $2" >>tmpFile.txt
...do more stuff...
else
usage
done
The script takes in four (or five?) command line arguments, and that usage function is:
usage() {
echo "usage: myscript -a STR_A | -b STR_B" 1>&2
exit 1
}
So suppose I ran the script like this:
me#ubuntu1:~$./myscript -A apple -B banana
I'm guessing that this code processes the script's command line arguments. I think that the outer while() loop steps through the command line arguments after argument 1, which would be myscript. The inner if() statements check to see an -a or -b flag is used to supply arguments, and then records the text string that follows in tmpFile.txt. Anything outside of those parameters is rejected and the script exits.
A lot of these assumptions rest on the bet that the outer while() loop...
while [ $# -ge 2 ]
...means "parse the BASH argv[] after the first argument", (to put this in C terms.) If that's not a correct assumption, then I have no idea what's going on here. Any feedback is appreciated, thank you!
Some code explanation.
while [ $# -ge 2 ]
There is a missing do for the loop.
This should loop forever if there are two or more arguments, unless shift is used. If there are less than two arguments, the loop does not even start.
if [ "x$1" = "x-a" ]; then
In distant past, it was common to prevent empty strings by adding an extra letter. Nowadays you would if [ "$1" = "-a" ]; then.
else
usage
Note that the usage is called from within the loop. So, if I would call the script as myscript -a, I would not get a usage message. On the other hand, if I would myscript bla bla, I would get an endless stream of error messages, which is probably not what you want.
I would seriously edit the script; determine whether the while is indeed a loop-forever or whether it is used instead of an if, and try the getops for argument parsing.
This doesn't help understand the code you're reading, but I do option parsing like this:
# inititialize vars, not strictly required in this case
a=''
b=''
# process options
while getopts "ha:b:" opt; do
case $opt in
a) a=$OPTARG ;;
b) b=$OPTARG ;;
*) usage ;;
esac
done
# shift after the processing
shift $((OPTIND - 1))
# look for error conditions
if [[ -n $a && -n $b ]]; then
echo "only one of -a or -b should be given" >&2
exit 1
fi
getopts is a bash builtin

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.

Correct way to check for a command line flag in bash

In the middle of a script, I want to check if a given flag was passed on the command line. The following does what I want but seems ugly:
if echo $* | grep -e "--flag" -q
then
echo ">>>> Running with flag"
else
echo ">>>> Running without flag"
fi
Is there a better way?
Note: I explicitly don't want to list all the flags in a switch/getopt. (In this case any such things would become half or more of the full script. Also the bodies of the if just set a set of vars)
An alternative to what you're doing:
if [[ $* == *--flag* ]]
See also BashFAQ/035.
Note: This will also match --flags-off since it's a simple substring check.
I typically see this done with a case statement. Here's an excerpt from the git-repack script:
while test $# != 0
do
case "$1" in
-n) no_update_info=t ;;
-a) all_into_one=t ;;
-A) all_into_one=t
unpack_unreachable=--unpack-unreachable ;;
-d) remove_redundant=t ;;
-q) GIT_QUIET=t ;;
-f) no_reuse=--no-reuse-object ;;
-l) local=--local ;;
--max-pack-size|--window|--window-memory|--depth)
extra="$extra $1=$2"; shift ;;
--) shift; break;;
*) usage ;;
esac
shift
done
Note that this allows you to check for both short and long flags. Other options are built up using the extra variable in this case.
you can take the straight-forward approach, and iterate over the arguments to test each of them for equality with a given parameter (e.g. -t, --therizinosaurus).
put it into a function:
has_param() {
local term="$1"
shift
for arg; do
if [[ $arg == "$term" ]]; then
return 0
fi
done
return 1
}
… and use it as a predicate in test expressions:
if has_param '-t' "$#"; then
echo "yay!"
fi
if ! has_param '-t' "$1" "$2" "$wat"; then
echo "nay..."
fi
if you want to reject empty arguments, add an exit point at the top of the loop body:
for arg; do
if [[ -z "$arg" ]]; then
return 2
fi
# ...
this is very readable, and will not give you false positives, like pattern matching or regex matching will.
it will also allow placing flags at arbitrary positions, for example, you can put -h at the end of the command line (not going into whether it's good or bad).
but, the more i thought about it, the more something bothered me.
with a function, you can take any implementation (e.g. getopts), and reuse it. encapsulation rulez!
but even with commands, this strength can become a flaw. if you'll be using it again and again, you'll be parsing all the arguments each time.
my tendency is to favor reuse, but i have to be aware of the implications. the opposed approach would be to parse these arguments once at the script top, as you dreaded, and avoid the repeated parsing.
you can still encapsulate that switch case, which can be as big as you decide (you don't have to list all the options).
You can use the getopt keyword in bash.
From http://aplawrence.com/Unix/getopts.html:
getopt
This is a standalone executable that has been around a long time.
Older versions lack the ability to handle quoted arguments (foo a "this
won't work" c) and the versions that can, do so clumsily. If you are
running a recent Linux version, your "getopt" can do that; SCO OSR5,
Mac OS X 10.2.6 and FreeBSD 4.4 has an older version that does not.
The simple use of "getopt" is shown in this mini-script:
#!/bin/bash
echo "Before getopt"
for i
do
echo $i
done
args=`getopt abc:d $*`
set -- $args
echo "After getopt"
for i
do
echo "-->$i"
done
I've made small changes to the answer of Eliran Malka:
This function can evaluate different parameter synonyms, like "-q" and "--quick". Also, it does not use return 0/1 but an echo to return a non-null value when the parameter is found:
function has_param() {
local terms="$1"
shift
for term in $terms; do
for arg; do
if [[ $arg == "$term" ]]; then
echo "yes"
fi
done
done
}
# Same usage:
# Assign result to a variable.
FLAG_QUICK=$(has_param "-q --quick" "$#") # "yes" or ""
# Test in a condition using the nonzero-length-test to detect "yes" response.
if [[ -n $(has_param "-h --help" "$#") ]]; then;
echo "Need help?"
fi
# Check, is a flag is NOT set by using the zero-length test.
if [[ -z $(has_param "-f --flag" "$#") ]]; then
echo "FLAG NOT SET"
fi
The modification of Dennis Williamson's answer with additional example for a argument in the short form.
if [[ \ $*\ == *\ --flag\ * ]] || [[ \ $*\ == *\ -f\ * ]]
It solves the problem of false positive matching --flags-off and even --another--flag (more popular such case for an one-dashed arguments: --one-more-flag for *-f*).
\ (backslash + space) means space for expressions inside [[ ]]. Putting spaces around $* allows to be sure that the arguments contacts neither line's start nor line's end, they contacts only spaces. And now the target flag surrounded by spaces can be searched in the line with arguments.
if [ "$1" == "-n" ]; then
echo "Flag set";
fi
Here is a variation on the most voted answer that won't pick up false positives
if [[ " $* " == *" -r "* ]]; then
Not an alternative, but an improvement, though.
if echo $* | grep -e "\b--flag\b" -q
Looking for word boundaries will make sure to really get the option --flag and neither --flagstaff nor --not-really--flag

Bash, no-arguments warning, and case decisions

I am learning bash.
I would like to do a simple script that, when not arguments given, shows some message. And when I give numers as argument,s depending on the value, it does one thing or another.
I would also like to know suggestions for the best online manuals for beginners in bash
Thanks
if [[ $# -eq 0 ]] ; then
echo 'some message'
exit 0
fi
case "$1" in
1) echo 'you gave 1' ;;
*) echo 'you gave something else' ;;
esac
The Advanced Bash-Scripting Guide is pretty good. In spite of its name, it does treat the basics.
If only interested in bailing if a particular argument is missing, Parameter Substitution is great:
#!/bin/bash
# usage-message.sh
: ${1?"Usage: $0 ARGUMENT"}
# Script exits here if command-line parameter absent,
#+ with following error message.
# usage-message.sh: 1: Usage: usage-message.sh ARGUMENT
Example
if [ -z "$*" ]; then echo "No args"; fi
Result
No args
Details
-z is the unary operator for length of string is zero.
$* is all arguments.
The quotes are for safety and encapsulating multiple arguments if present.
Use man bash and search (/ key) for "unary" for more operators like this.
Old but I have reason to rework the answer now thanks to some previous confusion:
if [[ $1 == "" ]] #Where "$1" is the positional argument you want to validate
then
echo "something"
exit 0
fi
This will echo "Something" if there is no positional argument $1. It does not validate that $1 contains specific information however.
If there is not only 1 argument, then print usage and exit
if [[ $# != 1 ]] ; then
echo 'USAGE: bin/siege COOKIE'
exit 0
fi

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