This question already has answers here:
Passing parameters to a Bash function
(7 answers)
Closed 3 months ago.
I'm writing the following script and I found the argument is not passed to a function called. However, when in an individual shell I can archive this.
usage() { if [ -n "$error_mess" ]; then echo -e "$error_mess"; fi; echo -e "usage stamens too long to present" 1>&2; exit 1; }
while getopts ":f:" flag
do
case "${flag}" in
f) if [ -n "${OPTARG}" ] ; then file=${OPTARG} ; else error_mess='\033[1m\033[5mError: file cannot be empty\033[0m' | usage; fi;;
*) usage;;
esac
done
I tried to put error_mess section after usage when calling inside the switch statement, but this error message is still not printing out.
When tried directly in bash it is working (only when the first line is run). The code entered directly to shell was:
# To check syntax error
error_mess='\033[1m\033[5mError: file cannot be empty\033[0m' ; if [ -n "$error_mess" ]; then echo -e "$error_mess"; fi
# To check if it can be passed inside a function
usage() { if [ -n "$error_mess" ]; then echo -e "$error_mess"; fi; echo -e "usage statement" 1>&2; exit 1; } ; error_mess='\033[1m\033[5mError: file cannot be empty\033[0m' | usage
# To check if it can be nested in if statement
a=0; if [ $a = 1 ] ; then echo "$a" ; else error_mess='\033[1m\033[5mError: file cannot be empty\033[0m' | usage ; fi
If I'm not doing it right, how should I correct it? I have other use case that do not have an error_mess or is having a different error_mess.
When you do cmd1 | cmd2 in bash, each command is executed in a separate subshell.
When one of the commands is var=value, then that variables only exists in that subshell.
You'll want to remove the pipe
if [ -n "$OPTARG" ]; then
file=$OPTARG
else
error_mess='\033[1m\033[5mError: file cannot be empty\033[0m' usage
fi
Think about passing the error message as an argument to the usage function. Then if you accidentally set the error_mess in one place but call usage in another place, you won't get a spurious error message.
If you're willing to give up the fancy error output:
while getopts "f:" flag # remove leading colon
do
case "${flag}" in
f) file=${OPTARG:?Error: file cannot be empty} ;;
# ...........^^
*) usage;;
esac
done
Then:
$ bash prog.sh -f
prog.sh: option requires an argument -- f
usage ...
$ bash prog.sh -f ""
prog.sh: line 5: OPTARG: Error: file cannot be empty
From the manual
${parameter:?word}
If parameter is null or unset, the expansion of word (or a message to that effect if word is not present) is written to the standard error and the shell, if it is not interactive, exits. Otherwise, the value of parameter is substituted.
To conclude, what you probably want is this:
if [ -n "$OPTARG" ]; then
file=$OPTARG
else
usage 'Error: file cannot be empty'
fi
And then usage looks like:
usage() {
{
[[ "$1" ]] && printf '\033[1m\033[5m%s\033[0m\n' "$1"
echo -e "usage stamens too long to present"
} 1>&2
exit 1
}
Related
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")
I have a bash script that I need to take in a user name with a flag, and then I want to be able to look for -r or -w to indicate whether this should be a read or write.
Currently I am using get opts, but this requires that an actual argument be passed to -r and -w.
How do I test if just -r or -w is there without passing something to those flags.
Currently my script looks like this:
#!/bin/bash
while getopts :u:r:w: opt; do
case $opt in
u ) user="$OPTARG" ;;
r ) my_read=1 ;;
w ) my_write=1 ;;
\? ) echo "${0##*/}" [ -erw ]; exit 1 ;;
esac
done
if [[ ${my_write} -eq 1 ]] ; then
echo "write"
fi
if [[ ${my_read} -eq 1 ]] ; then
echo "read"
fi
As noted in the comments, a colon (:) indicates the preceding option character requires an argument. Just remove the colons:
#!/bin/bash
while getopts u:rw opt; do
case $opt in
u ) user="$OPTARG" ;;
r ) my_read=1 ;;
w ) my_write=1 ;;
\? ) echo "${0##*/} [ -erw ]" >&2; exit 1 ;;
esac
done
shift $((OPTIND-1))
if [[ "${my_write}" -eq 1 ]] ; then
echo "write"
fi
if [[ "${my_read}" -eq 1 ]] ; then
echo "read"
fi
Other changes made: quotes on final case moved to include square brackets, output to standard error (>&2) to avoid getting piped inappropriately, the shift line was added so your argument list ($# and $1, etc) have the getopts-parsed options removed, and quotes were placed around tests because otherwise the shell can complain about being passed empty tests (it'll see [[ -eq 1]] if either variable is undefined, which will happen if either -r or -w is not passed, and that is invalid while [[ "" -eq 1 ]] will simply evaluate as false).
Just get them as parameters not options:
while [ -n "$1" ]; do
case "$1" in
-r) echo "read";;
-w) echo "write";;
esac
shift
done
I have a bash script which accepts three command line arguments, e.g script is executed like this: script -c <value> -h <value> -w <value>. I would like to ensure that:
order of arguments is not important
if argument does not have a value, then error message is printed
if any of the arguments are missing, then error message is printed
if there are unknown arguments, then error message is printed
I accomplished this with following case statements:
#!/bin/bash
while :; do
case "$1" in
-h)
[[ x${2%%-*} != x ]] || { echo "Value for "$1" missing!"; exit 1; }
host="$2"
shift 2
;;
-w)
[[ x${2%%-*} != x ]] || { echo "Value for "$1" missing!"; exit 1; }
warning="$2"
shift 2
;;
-c)
[[ x${2%%-*} != x ]] || { echo "Value for "$1" missing!"; exit 1; }
critical="$2"
shift 2
;;
"")
[[ $host && $warning && $critical ]] || { echo "One of the arguments is missing!"; exit 1; }
break
;;
*)
echo "Unknow option"
exit 1
;;
esac
done
However, maybe case itself has some advanced options which could avoid all those [[ ]] tests? Or maybe I should use another method altogether for processing command line arguments if I want to make sure that corner cases described above are also covered?
You should ideally use the getopts builtin for this, though there are other ways as well. Getopts is the most portable and legible option, handling all of your "corner cases" pretty much automatically.
while getopts c:h:w: arg; do
case $arg in
( c ) critical="$OPTARG" ;;
( h ) host="$OPTARG" ;;
( w ) warning="$OPTARG" ;;
( \? ) exit 2 ;;
esac
done
shift $((OPTIND-1))
if [ -z "$critical" ] || [ -z "$host" ] || [ -z "$warning" ]; then
echo "One of the arguments is missing!"
exit 1
fi
Each option is followed by a colon, which indicates it has a mandatory argument. If you have a flag that does not use an argument, do not follow the option with a colon. POSIX getopts does not support options with optional arguments.
See also my answer to this question about supporting long options, which merely keys on the - option whose argument is parsed by a nested case switch. Long options implemented in this manner can actually support optional arguments.
I'm a big fan of overloading -h for help. Assuming you have a help function, put this before the getopts loop:
# when the sole argument is -h, provide help
if [ "$*" = "-h" ]; then
help
exit 0
fi
I am trying to write a bash script that takes in an option.
Lets call these options A and B.
In the script A and B may or may not be defined as variables.
I want to be able to check if the variable is defined or not.
I have tried the following but it doesn't work.
if [ ! -n $1 ]; then
echo "Error"
fi
Thanks
The "correct" way to test whether a variable is set is to use the + expansion option. You'll see this a lot in configure scripts:
if test -s "${foo+set}"
where ${foo+set} expands to "set" if it is set or "" if it's not. This allows for the variable to be set but empty, if you need it. ${foo:+set} additionally requires $foo to not be empty.
(That $(eval echo $a) thing has problems: it's slow, and it's vulnerable to code injection (!).)
Oh, and if you just want to throw an error if something required isn't set, you can just refer to the variable as ${foo:?} (leave off the : if set but empty is permissible), or for a custom error message ${foo:?Please specify a foo.}.
You did not define how these options should be passed in, but I think:
if [ -z "$1" ]; then
echo "Error"
exit 1
fi
is what you are looking for.
However, if some of these options are, err, optional, then you might want something like:
#!/bin/bash
USAGE="$0: [-a] [--alpha] [-b type] [--beta file] [-g|--gamma] args..."
ARGS=`POSIXLY_CORRECT=1 getopt -n "$0" -s bash -o ab:g -l alpha,beta:,gamma -- "$#"`
if [ $? -ne 0 ]
then
echo "$USAGE" >&2
exit 1
fi
eval set -- "$ARGS"
unset ARGS
while true
do
case "$1" in
-a) echo "Option a"; shift;;
--alpha) echo "Option alpha"; shift;;
-b) echo "Option b, arg '$2'"; shift 2;;
--beta) echo "Option beta, arg '$2'"; shift 2;;
-g|--gamma) echo "Option g or gamma"; shift;;
--) shift ; break ;;
*) echo "Internal error!" ; exit 1 ;;
esac
done
echo Remaining args
for arg in "$#"
do
echo '--> '"\`$arg'"
done
exit 0
Don't do it that way, try this:
if [[ -z $1 ]]; then
echo "Error"
fi
The error in your version is actually the lack of quoting.
Should be:
if [ ! -n "$1" ]; then
echo "Error"
fi
But you don't need the negation, use -z instead.
If you work on Bash, then use double brackets [[ ]] too.
from the man bash page:
-z string
True if the length of string is zero.
-n string
True if the length of string is non-zero.
Also, if you use bash v4 or greater (bash --version) there's -v
-v varname
True if the shell variable varname is set (has been assigned a value).
The trick is "$1", i.e.
root#root:~# cat auto.sh
Usage () {
echo "error"
}
if [ ! -n $1 ];then
Usage
exit 1
fi
root#root:~# bash auto.sh
root#root:~# cat auto2.sh
Usage () {
echo "error"
}
if [ ! -n "$1" ];then
Usage
exit 1
fi
root#root:~# bash auto2.sh
error
I want the user to input something at the command line either -l or -e.
so e.g. $./report.sh -e
I want an if statement to split up whatever decision they make so i have tried...
if [$1=="-e"]; echo "-e"; else; echo "-l"; fi
obviously doesn't work though
Thanks
I use:
if [[ "$1" == "-e" ]]; then
echo "-e"
else
echo "-l";
fi
However, for parsing arguments, getopts might make your life easier:
while getopts "el" OPTION
do
case $OPTION in
e)
echo "-e"
;;
l)
echo "-l"
;;
esac
done
If you want it all on one line (usually it makes it hard to read):
if [ "$1" = "-e" ]; then echo "-e"; else echo "-l"; fi
You need spaces between the square brackets and what goes inside them. Also, just use a single =. You also need a then.
if [ $1 = "-e" ]
then
echo "-e"
else
echo "-l"
fi
The problem specific to -e however is that it has a special meaning in echo, so you are unlikely to get anything back. If you try echo -e you'll see nothing print out, while echo -d and echo -f do what you would expect. Put a space next to it, or enclose it in brackets, or have some other way of making it not exactly -e when sending to echo.
If you just want to print which parameter the user has submitted, you can simply use echo "$1". If you want to fall back to a default value if the user hasn't submitted anything, you can use echo "${1:--l} (:- is the Bash syntax for default values). However, if you want really powerful and flexible argument handling, you could look into getopt:
params=$(getopt --options f:v --longoptions foo:,verbose --name "my_script.sh" -- "$#")
if [ $? -ne 0 ]
then
echo "getopt failed"
exit 1
fi
eval set -- "$params"
while true
do
case $1 in
-f|--foo)
foobar="$2"
shift 2
;;
-v|--verbose)
verbose='--verbose'
shift
;;
--)
while [ -n "$3" ]
do
targets[${#targets[*]}]="$2"
shift
done
source_dir=$(readlink -fn -- "$2")
shift 2
break
;;
*)
echo "Unhandled parameter $1"
exit 1
;;
esac
done
if [ $# -ne 0 ]
then
error "Extraneous parameters." "$help_info" $EX_USAGE
fi