Checking the existence of a command line argument in shell - shell

I want to check if a command line option exists when running a shell script, example
./test.sh arg1 arg2 arg3 arg4
Want to check if one of the argument is say arg3 (not necessarily the third argument)
The quick solution is to use a for loop and check if one of the argument matches a given string, but is there a better way to do it something of the form 'arg3' in $#.

(assuming bash)
I would do this:
have_arg3=false
for arg; do
if [[ $arg == "arg3" ]]; then
have_arg3=true
break
fi
done
if $have_arg3; then
echo "arg3 is present
fi
but you could do this (all quotes and spaces below are required!):
if [[ " $* " == *" arg3 "* ]]; then
echo "arg3 is present"
fi
can be encapsulated in a function:
$ arg_in_args () (
arg=$1
shift
IFS=:
[[ "$IFS$*$IFS" == *"$IFS$arg$IFS"* ]]
)
$ set -- foo bar baz
$ if arg_in_args "arg3" "$#"; then echo Y; else echo N; fi
N
$ if arg_in_args "baz" "$#"; then echo Y; else echo N; fi
Y

Related

Bash script using getopts to store strings as an array

I am working on a Bash script that needs to take zero to multiple strings as an input but I am unsure how to do this because of the lack of a flag before the list.
The script usage:
script [ list ] [ -t <secs> ] [ -n <count> ]
The list takes zero, one, or multiple strings as input. When a space is encountered, that acts as the break between the strings in a case of two or more. These strings will eventually be input for a grep command, so my idea is to save them in an array of some kind. I currently have the -t and -n working correctly. I have tried looking up examples but have been unable to find anything that is similar to what I want to do. My other concern is how to ignore string input after a flag is set so no other strings are accepted.
My current script:
while getopts :t:n: arg; do
case ${arg} in
t)
seconds=${OPTARG}
if ! [[ $seconds =~ ^[1-9][0-9]*$ ]] ; then
exit
fi
;;
n)
count=${OPTARG}
if ! [[ $count =~ ^[1-9][0-9]*$ ]] ; then
exit
fi
;;
:)
echo "$0: Must supply an argument to -$OPTARG" >&2
exit
;;
?)
echo "Invalid option: -${OPTARG}"
exit
;;
esac
done
Edit: This is for a homework assignment and am unsure if the order of arguments can change
Edit 2: Options can be in any order
Would you please try the following:
#!/bin/bash
# parse the arguments before getopts
for i in "$#"; do
if [[ $i = "-"* ]]; then
break
else # append the arguments to "list" as long as it does not start with "-"
list+=("$1")
shift
fi
done
while getopts :t:n: arg; do
: your "case" code here
done
# see if the variables are properly assigned
echo "seconds=$seconds" "count=$count"
echo "list=${list[#]}"
Try:
#! /bin/bash -p
# Set defaults
count=10
seconds=20
args=( "$#" )
end_idx=$(($#-1))
# Check for '-n' option at the end
if [[ end_idx -gt 0 && ${args[end_idx-1]} == -n ]]; then
count=${args[end_idx]}
end_idx=$((end_idx-2))
fi
# Check for '-t' option at the (possibly new) end
if [[ end_idx -gt 0 && ${args[end_idx-1]} == -t ]]; then
seconds=${args[end_idx]}
end_idx=$((end_idx-2))
fi
# Take remaining arguments up to the (possibly new) end as the list of strings
strings=( "${args[#]:0:end_idx+1}" )
declare -p strings seconds count
The basic idea is to process the arguments right-to-left instead of left-to-right.
The code assumes that the only acceptable order of arguments is the one given in the question. In particular, it assumes that the -t and -n options must be at the end if they are present, and they must be in that order if both are present.
It makes no attempt to handle option arguments combined with options (e.g. -t5 instead of -t 5). That could be done fairly easily if required.
It's OK for strings in the list to begin with -.
My shorter version
Some remarks:
Instead of loop over all argument**, then break if argument begin by -, I simply use a while loop.
From How do I test if a variable is a number in Bash?, added efficient is_int test function
As any output (echo) done in while getopts ... loop would be an error, redirection do STDERR (>&2) could be addressed to the whole loop instead of repeated on each echo line.
** Note doing a loop over all argument could be written for varname ;do. as $# stand for default arguments, in "$#" are implicit in for loop.
#!/bin/bash
is_int() { case ${1#[-+]} in
'' | *[!0-9]* ) echo "Argument '$1' is not a number"; exit 3;;
esac ;}
while [[ ${1%%-*} ]];do
args+=("$1")
shift
done
while getopts :t:n: arg; do
case ${arg} in
t ) is_int "${OPTARG}" ; seconds=${OPTARG} ;;
n ) is_int "${OPTARG}" ; count=${OPTARG} ;;
: ) echo "$0: Must supply an argument to -$OPTARG" ; exit 2;;
? ) echo "Invalid option: -${OPTARG}" ; exit 1;;
esac
done >&2
declare -p seconds count args
Standard practice is to place option arguments before any non-option arguments or variable arguments.
getopts natively recognizes -- as the end of option switches delimiter.
If you need to pass arguments that starts with a dash -, you use the -- delimiter, so getopts stops trying to intercept option arguments.
Here is an implementation:
#!/usr/bin/env bash
# SYNOPSIS
# script [-t<secs>] [-n<count>] [string]...
# Counter of option arguments
declare -i opt_arg_count=0
while getopts :t:n: arg; do
case ${arg} in
t)
seconds=${OPTARG}
if ! [[ $seconds =~ ^[1-9][0-9]*$ ]] ; then
exit
fi
opt_arg_count+=1
;;
n)
count=${OPTARG}
if ! [[ $count =~ ^[1-9][0-9]*$ ]] ; then
exit 1
fi
opt_arg_count+=1
;;
?)
printf 'Invalid option: -%s\n' "${OPTARG}" >&2
exit 1
;;
esac
done
shift "$opt_arg_count" # Skip all option arguments
[[ "$1" == -- ]] && shift # Skip option argument delimiter if any
# Variable arguments strings are all remaining arguments
strings=("$#")
declare -p count seconds strings
Example usages
With strings not starting with a dash:
$ ./script -t45 -n10 foo bar baz qux
declare -- count="10"
declare -- seconds="45"
declare -a strings=([0]="foo" [1]="bar" [2]="baz" [3]="qux")
With string starting with a dash, need -- delimiter:
$ ./script -t45 -n10 -- '-dashed string' foo bar baz qux
declare -- count="10"
declare -- seconds="45"
declare -a strings=([0]="-dashed string" [1]="foo" [2]="bar" [3]="baz" [4]="qux")

How to search within a range of bash command-line parameters

Let's say I have the program test.sh, which has parameters, some optional and some not.
For example:
./test.sh --foo /path/to/file --baz --bar1 --bar2 --bar3
where foo and baz, as well as the path, are necessary and the bars are optional parameters.
Now, I want to be able to make the everything after the path order-insensitive.
I could use
if [[ "$3" == "--baz" ]] || [[ "$4" == "--baz" ]] || ... || [[ "${(n-1)}" == "--baz" ]] || [[ "${n}" == "--baz" ]]
but that's slow and messy, even for me.
Ideally I would have something along the lines of this:
if [[ ${n > 2} == "--baz" ]]; then
Violating standard utility syntax guidelines like this makes your program unpredictable and difficult to use, but you can still do so e.g. with a simple utility function:
hasArgAfter() {
n="$1"
word="$2"
shift "$((n+2))"
for arg
do
[ "$arg" = "$word" ] && return 0
done
return 1
}
if hasArgAfter 2 "--baz" "$#"
then
echo "Some argument strictly after #2 is --baz"
fi
Based on the answers in:
Check if a Bash array contains a value
How to slice an array in Bash
You could come up with the following answer:
[[ " ${#:3} " =~ " --baz " ]] && echo yes || echo no
This might fail if you have something like
command --foo /path/to/file --flag1 option1 --flag2 "foo --baz bar" --flag3
where foo --baz bar is an option to --flag2
Another way, a bit safer, would be:
for arg in "${#:3}"; do
[[ "${arg}" != "--baz" ]] && continue
# perform action here if --baz is found
set-of-commands
# need this line for early return
break
done
Create a function for handling inputs, then ordering won't matter.
What I would do is define global variables for your script using the typset command at the top of your script. Then I would handle the user input options using a function or just code it without a function. This way when input is not in order or it is missing it is properly handled.
The example below is using a case statement and it is using the build in shift option to go through all of the inputs. $1 is --option $2 is the value such as "/path/to/something", I have checks in there check if $2 is empty "-z" or || if it isn't, set it. When done items are either set or empty. In your code you will check if set or empty to determine if you are going to use that variable (not shown here.
# -- create globals --
typeset fooPath
typeset baz
typeset bar1
# -- get required commandline input --
get_user_input_options() {
while [[ $# -gt 0 ]] ;do
key="$1"
case ${key,,} in
--foo|--foo-path)
fooPath="${2}"
shift
;;
-b|--baz)
[[ -z "${2}" ]] || baz="${2}"
shift
;;
--bar1)
[[ -z "${2}" ]] || bar1="${2}"
shift
;;
*) echo "ERROR: Unknown option $key given."
exit 9
;;
esac
shift
done
}
# -- get inputs first in script logic --
get_user_input_options "$#"
echo $fooPath
echo $baz
echo $bar1
Example outputs:
[centos#ip-172-31-22-252 ~]$ ./t.sh --foo "/some/thing" --baz askdh
/some/thing
askdh
[centos#ip-172-31-22-252 ~]$ ./t.sh --foo "/some/thing" --baz askdh --bar1
/some/thing
askdh
[centos#ip-172-31-22-252 ~]$ ./t.sh --foo "/some/thing" --baz askdh --bar1 test
/some/thing
askdh
test
[centos#ip-172-31-22-252 ~]$ ./t.sh --foo "/some/thing" --baz askdh --bar1 test --notvalid askdha
ERROR: Unknown option --notvalid given.

Unexpected if statement behaviour

I was running a small bash script, but I couldn't figure out why it was entering a if block even when condition should be false.
$ cat script.sh
#!/bin/bash
if [[ "$#"="-h" || "$#"="--help" ]]
then
echo 'Show help'
exit
fi
echo 'Do stuff'
$ ./script.sh
Show help
$ bash -x script.sh
+ [[ -n =-h ]]
+ echo 'Show help'
Show help
+ exit
$ bash -x script.sh -option
+ [[ -n -option=-h ]]
+ echo 'Show help'
Show help
+ exit
So why is $# equal to -n when I didn't pass any arguments? Also even if it is, how does -n =-h evaluate to true? When I do pass an argument -option, why is it evaluated to true, too?
Whitespace is significant. Spaces between the arguments to [[ are mandatory.
if [[ "$#" = "-h" || "$#" = "--help" ]]
Also, "$#" means "all of the command-line arguments". It would be better to just check a single argument.
if [[ "$1" = "-h" || "$1" = "--help" ]]
And for what it's worth, variable expansions in [[ don't have to be quoted. It doesn't hurt, and quoting your variables actually a good habit to develop, but if you want you can remove the quotes.
if [[ $1 = -h || $1 = --help ]]
[[ string ]] return true if string is not empty, i.e. it's a shorcut for
[[ -n string ]]
In your case, the string was =-h, that's why you see
[[ -n =-h ]]
To test for string equiality, you have to use the = (or ==) operator, that must be preceded and followed by whitespace.
[[ "$#" = "-h" ]]
Note that "$#" means all the arguments:
set -- a b c
set -x
[[ "$#" == 'a b c' ]] && echo true
gives
+ [[ a b c == \a\ \b\ \c ]]
+ echo true
true
The other answers have already explained the problems with your code. This one shows that
bashisms such as [[ ... ]] are not needed,
you can gain flexibility by using a for loop to check whether at least one of the command-line argument matches -h or --help.
Script
#!/bin/sh
show_help=0
for arg in "$#"; do
shift
case "$arg" in
"--help")
show_help=1
;;
"-h")
show_help=1
;;
*)
;;
esac
done
if [ $show_help -eq 1 ]; then
printf "Show help\n"
exit
fi
Tests
After making the script (called "foo") executable, by running
chmod u+x foo
I get the following results
$ ./foo
$ ./foo -h
Show help
$ ./foo --help
Show help
$ ./foo bar
$ ./foo bar --help
Show help
$ ./foo bar --help baz -h
Show help

How to pass a long option to a bash script?

./script.sh -abc hello
How can I write my script to use '-abc' as the option and 'hello' as the value to that option?
I should be able to pass this value to all the functions in this script. Lets say I have 2 functions: X and Y.
Use this in your script:
[[ $1 == -abc ]] && value="$2" || echo invalid option
If you don't want to print any messages on wrong option or no option, then omit the || echo ... part, value will be empty.
If you want to make the second argument a must, then:
[[ $1 == -abc ]] && [[ $2 != "" ]] && value="$2" || echo invalid option
Using if else loop will give you complete control over this:
if [[ $1 == -abc ]]; then
#if first option is valid then do something here
if [[ $2 != "" ]]; then
value="$2"
else
#if second option is not given then do something here
echo invalid option
fi
else
echo invalid option
#if first option is invalid then do something here
fi
If you want to make the first argument a must too, then change the first if statement line to
if [[ $1 == -abc && $1 != "" ]]; then
If you want to pass as many arguments as you wish and process them,
then use something like this:
#!/bin/bash
opts=( "$#" )
#if no argument is passed this for loop will be skipped
for ((i=0;i<$#;i++));do
case "${opts[$i]}" in
-abc)
# "${opts[$((i+1))]}" is the immediately follwing option
[[ "${opts[$((i+1))]}" != "" ]] &&
value="${opts[$((i+1))]}"
echo "$value"
((i++))
#skips the nex adjacent argument as it is already taken
;;
-h)
#dummy help option
echo "Options are [-abc value], -h"
;;
*)
#other unknown options
echo invalid option
break
;;
esac
done
This is an example of handling multiple arguments with only two options available -abc value and -h
bash doesn't have a built in command for processing long arguments. In order to parse long options in a shell script, you'll need to iterate over the arguments list yourself.
Here's one approach:
#!/bin/sh
is_option_arg () {
case $1 in
-*)
return 1
;;
*)
return 0
;;
esac
}
usage () {
echo "$(basename "$0") -abc ARG -def ARG -verbose"
}
OPT_ABC=
OPT_DEF=
OPT_VERBOSE=false
while [ "$#" -gt 0 ]; do
case $1 in
-abc)
shift
{ [ "$#" -ne 0 ] && is_option_arg "$1"; } || { usage >&2; exit 1; }
OPT_ABC=$1
;;
-def)
shift
{ [ "$#" -ne 0 ] && is_option_arg "$1"; } || { usage >&2; exit 1; }
OPT_DEF=$1
;;
-verbose)
OPT_VERBOSE=true
;;
*)
break
;;
esac
shift
done
echo "OPT_ABC=$OPT_ABC"
echo "OPT_DEF=$OPT_DEF"
echo "OPT_VERBOSE=$OPT_VERBOSE"
if [ "$#" -gt 0 ]; then
echo "Remaining args:"
for arg in "$#"; do
echo "$arg"
done
fi
You pretty much have to implement it yourself manually. Here's one way:
abc=
while [[ "$1" == -* ]]; do
opt=$1
shift
case "$opt" in
-abc)
if (( ! $# )); then
echo >&2 "$0: option $opt requires an argument."
exit 1
fi
abc="$1"
shift
;;
*)
echo >&2 "$0: unrecognized option $opt."
exit 2
;;
esac
done
echo "abc is '$abc', remaining args: $*"
Some sample runs of the above:
(0)$ ./script.sh
abc is '', remaining args:
(0)$ ./script.sh hello
abc is '', remaining args: hello
(0)$ ./script.sh -abc hello
abc is 'hello', remaining args:
(0)$ ./script.sh -abc hello there
abc is 'hello', remaining args: there
(0)$ ./script.sh -abc
./script.sh: option -abc requires an argument.
(1)$ ./script.sh -bcd
./script.sh: unrecognized option -bcd.
(2)$

Shell quoting with quotation marks in bash

I want to implement a bash function that runs its arguments as a command, while (maybe optionally) printing the command before. Think of an installation script or test runner script.
Just using
function run () {
echo "Running $#"
"$#"
}
would not allow me to distinguish a call from run foo arg1 arg2 and run foo "arg1 arg2", so I need to properly escape arguments.
My best shot so far is
function run () {
echo -n "Running"
printf " %q" "$#"
echo
"$#"
}
Which works:
$ run echo "one_argument" "second argument" argument\"with\'quotes
Running echo one_argument second\ argument argument\"with\'quotes
one_argument second argument argument"with'quotes
but is not very elegant. How can I achieve an output of
$ run echo "one_argument" "second argument" argument\"with\'quotes
Running echo one_argument "second argument" "argument\"with'quotes"
one_argument second argument argument"with'quotes
i.e. how can I make printf to put quotation marks around arguments that need quoting, and properly escape quotes therein, so that the output can be copy’n’pasted correctly?
This will quote everything:
run() {
printf "Running:"
for arg; do
printf ' "%s"' "${arg//\"/\\\"}"
done
echo
"$#"
}
run echo "one_argument" "second argument" argument\"with\'quotes
Running: "echo" "one_argument" "second argument" "argument\"with'quotes"
one_argument second argument argument"with'quotes
This version only quotes arguments containing double quotes or whitespace:
run() {
local fmt arg
printf "Running:"
for arg; do
[[ $arg == *[\"[:space:]]* ]] && fmt=' "%s"' || fmt=" %s"
printf "$fmt" "${arg//\"/\\\"}"
done
echo
"$#"
}
run echo "one_argument" "second argument" argument\"with\'quotes
Running: echo one_argument "second argument" "argument\"with'quotes"
one_argument second argument argument"with'quotes
I don't think there's an elegant solution to what you want, because "$#" is handled by bash before any command ever gets to see it. You'll have to manually re-construct the command-line:
#!/bin/bash
function run() {
echo -n "Running:"
for arg in "$#"; do
arg="$(sed 's/"/\\&/g' <<<$arg)"
[[ $arg =~ [[:space:]\\\'] ]] && arg=\"arg\"
echo -n " $arg"
done
echo ""
"$#"
}
run "$#"
Output:
$ ./test.sh echo arg1 "arg 2" "arg3\"with'other\'\nstuff"
Running: echo arg1 "arg 2" "arg3\"with'other\'\nstuff"
arg1 arg 2 arg3"with'other\'\nstuff
Note that there are some corner cases where you won't get the exact input command line. This happens when you pass arguments that bash expands before passing them on, e.g.:
$ ./test.sh echo foo'bar'baz
Running: echo foobarbaz
foobarbaz
$ ./test.sh echo "foo\\bar"
Running: echo "foo\bar"
foobar

Resources