Shell getopt first parameter error - shell

Running the following code, I find host_ip is empty, I don't know what the reason is?
TEMP=`getopt --long hostip:,hostport: -n 'javawrap' -- "$#"`
if [ $? != 0 ] ; then echo "Terminating..." >&2 ; exit 1 ; fi
eval set -- "$TEMP"
host_ip=
host_port=
while true; do
case "$1" in
--hostip ) host_ip="$2"; shift 2;;
--hostport ) host_port="$2"; shift 2 ;;
* ) break ;;
esac
done
echo $host_ip
echo $host_port

It seems you need to specify the short options to getopt otherwise it (IMO) messes up the parsing. From man getopt:
If this option is not found, the first parameter of getopt that does not start with a '-' (and is not an option argument) is used as the short options string.
This works:
$ getopt --options '' --longoptions hostip:,hostport: -n 'javawrap' -- --hostip foo --hostport bar
--hostip 'foo' --hostport 'bar' --

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

Is there a way to continue in a Flag when there is no $OPTARG set in Bash, GETOPTS?

I would like to build a script with getopts, that continues in the flag, when an $OPTARG isn't set.
My script looks like this:
OPTIONS=':dBhmtb:P:'
while getopts $OPTIONS OPTION
do
case "$OPTION" in
m ) echo "m"
t ) echo "t"
d ) echo "d";;
h ) echo "h";;
B ) echo "b";;
r ) echo "r";;
b ) echo "b"
P ) echo hi;;
#continue here
\? ) echo "?";;
:) echo "test -$OPTARG requieres an argument" >&2
esac
done
My aim is to continue at my comment, when there is no $OPTARG set for -P.
All I get after running ./test -P is :
test -P requieres an argument
and then it continues after the loop but I want to continue in the -P flag.
All clear?
Any Ideas?
First, fix the missing ;; in some of the case branches.
I don't think you can: you told getopts that -P requires an argument: two error cases
-P without an argument is the last option. In this case getops sees that nothing follows -P and sets the OPTION variable to :, which you handle in the case statement.
-P is followed by another option: getopts will simply take the next word, even if the next word is another option, as OPTARG.
Change the case branch to
P ) echo "P: '$OPTARG'";;
Then:
invoking the script like bash script.sh -P -m -t, the output is
P: '-m'
t
invoking the script like bash script.sh -Pmt, the output is
P: 'mt'
This is clearly difficult to work around. How do you know if the user intended the option argument to be literally "mt" and not the options -m and -t?
You might be able to work around this using getopt (see the canonical example) using an optional argument for a long option (those require an equal sign like --long=value) so it's maybe easier to check if the option argument is missing or not.
Translating getopts parsing to getopt -- it's more verbose, but you have finer-grained control
die() { echo "$*" >&2; exit 1; }
tmpArgs=$(getopt -o 'dBhmt' \
--long 'b::,P::' \
-n "$(basename "$0")" \
-- "$#"
)
(( $? == 0 )) || die 'Problem parsing options'
eval set -- "$tmpArgs"
while true; do
case "$1" in
-d) echo d; shift ;;
-B) echo B; shift ;;
-h) echo h; shift ;;
-m) echo m; shift ;;
-t) echo t; shift ;;
--P) case "$2" in
'') echo "P with no argument" ;;
*) echo "P: $2" ;;
esac
shift 2
;;
--b) case "$2" in
'') echo "b with no argument" ;;
*) echo "b: $2" ;;
esac
shift 2
;;
--) shift; break ;;
*) printf "> %q\n" "$#"
die 'getopt internal error: $*' ;;
esac
done
echo "Remaining arguments:"
for ((i=1; i<=$#; i++)); do
echo "$i: ${!i}"
done
Successfully invoking the program with --P:
$ ./myscript.sh --P -mt foo bar
P with no argument
m
t
Remaining arguments:
1: foo
2: bar
$ ./myscript.sh --P=arg -mt foo bar
P: arg
m
t
Remaining arguments:
1: foo
2: bar
This does impose higher overhead on your users, because -P (with one dash) is invalid, and the argument must be given with =
$ ./myscript.sh --P arg -mt foo bar
P with no argument
m
t
Remaining arguments:
1: arg
2: foo
3: bar
$ ./myscript.sh --Parg mt foo bar
myscript.sh: unrecognized option `--Parg'
Problem parsing options
$ ./myscript.sh -P -mt foo bar
myscript.sh: invalid option -- P
Problem parsing options
$ ./myscript.sh -P=arg -mt foo bar
myscript.sh: invalid option -- P
myscript.sh: invalid option -- =
myscript.sh: invalid option -- a
myscript.sh: invalid option -- r
myscript.sh: invalid option -- g
Problem parsing options
Do not mix logic with arguments parsing.
Prefer lower case variables.
My aim is to continue at my comment, when there is no $OPTARG set for -P
I advise not to. The less you do at one scope, the less you have to think about. Split parsing options and executing actions in separate stages. I advise to:
# set default values for options
do_something_related_to_P=false
recursive=false
tree_output=false
# parse arguments
while getopts ':dBhmtb:P:' option; do
case "$option" in
t) tree_output=true; ;;
r) recursive="$OPTARG"; ;;
P) do_something_related_to_P="$OPTARG"; ;;
\?) echo "?";;
:) echo "test -$OPTARG requieres an argument" >&2
esac
done
# application logic
if "$do_something_related_to_P"; then
do something related to P
if "$recursive"; then
do it in recursive style
fi
fi |
if "$tree_output"; then
output_as_tree
else
cat
fi
Example of "don't put programming application logic in the case branches" -- the touch command can take a -t timespec option or a -r referenceFile option but not both:
$ touch -t 202010100000 -r file1 file2
touch: cannot specify times from more than one source
Try 'touch --help' for more information.
I would implement that like (ignoring other options):
while getopts t:r: opt; do
case $opt in
t) timeSpec=$OPTARG ;;
r) refFile=$OPTARG ;;
esac
done
shift $((OPTIND-1))
if [[ -n $timeSpec && -n $refFile ]]; then
echo "touch: cannot specify times from more than one source" >&2
exit 1
fi
I would not do this:
while getopts t:r: opt; do
case $opt in
t) if [[ -n $refFile ]]; then
echo "touch: cannot specify times from more than one source" >&2
exit 1
fi
timeSpec=$OPTARG ;;
r) if [[ -n $timeSpec ]]; then
echo "touch: cannot specify times from more than one source" >&2
exit 1
fi
refFile=$OPTARG ;;
esac
done
You can see if the logic gets more complicated (as I mentioned, exactly one of -a or -b or -c), that the case statement size can easily balloon unmaintainably.

Using getopt to parse second argument into a variable

How do I parse the second argument into a variable using bash and getopt on the following script.
I can do sh test.sh -u and get "userENT" to display. But if I do sh test.sh -u testuser on this script I get an error.
#!/bin/sh
# Now check for arguments
OPTS=`getopt -o upbdhrstv: --long username,password,docker-build,help,version,\
release,remote-registry,stage,develop,target: -n 'parse-options' -- "$#"`
while true; do
case "$1" in
-u | --username)
case "$2" in
*) API_KEY_ART_USER="$2"; echo "userENT" ;shift ;;
esac ;;
-- ) shift; break ;;
* ) if [ _$1 != "_" ]; then ERROR=true; echo; echo "Invalid option $1"; fi; break ;;
esac
done
echo "user" $API_KEY_ART_USER
How can I pass the -u testuser and not have an Invalid option testuser error?
Output:
>sh test3.sh -u testuser
userENT
Invalid option testuser
user testuser
man getopt would tell you that a colon following the option indicates that it has an argument. You only have a colon after the v. You also weren't shifting within your loop, so you'd be unable to parse any options past the first one. And I'm not sure why you felt the need to have a second case statement that only had a single default option. In addition, there were a number of poor practices in your code including use of all caps variable names and backticks instead of $() for executing commands. And you've tagged your question bash but your shebang is /bin/sh. Give this a try, but you shouldn't be using code without understanding what it does.
#!/bin/sh
# Now check for arguments
opts=$(getopt -o u:pbdhrstv: --long username:,password,docker-build,help,version,\
release,remote-registry,stage,develop,target: -n 'parse-options' -- "$#")
while true; do
case "$1" in
-u|--username)
shift
api_key_art_user="$1"
echo "userENT"
;;
--)
shift;
break
;;
*)
if [ -n "$1" ]; then
err=true
echo "Invalid option $1"
fi
break
;;
esac
shift
done
echo "user $api_key_art_user"

Why do I have error with multi long options using getopt in bash?

I just wrote a script in bash, which work expect for multi long option:
#!/bin/bash
OPTS=`getopt -q -o fdhl: -l free,df,help,log: -- "$*"`
#Check if error with getopt
if [ $? != 0 ]
then
echo -e "error: parameter could not be found\n\nUsage:\n supervision [options]\n\n Try 'supervision --help'\n or 'supervision -h'\n for additional help text." ;
exit 1
fi
eval set -- "$OPTS"
while true ; do
case "$1" in
-f|--free)
free -h ;
shift;;
-d|--df)
df -h ; #Run df system command
shift;;
-l|--log)
case "$2" in
"") echo "miss file" ;
shift 2;; #No file passed as parameter
*)
df -h >> "$2" ;
shift 2;;
esac ;;
-h|--help) #Display help
shift;;
--) #End of parsed parameters list
shift ; break ;;
*)
break ;;
esac
done
I don't get why i'm supposed to, when I use more than 1 long option, for example:
sh myscript --free --df
And when I use --log:
sh myscript --log logfile
Both case exit on the if [ $? != 0 ], seems like the element which follow the 1st long option doesn't get parsed.
Ok, I figured out and it's all due to the using of "$*" instead of "$#" in the getopt call. I don't exactly why, i guessed both do the same thing, but it turns out to be the one which causes the problem.

How to detect if an argument was passed to the script?

I am a novice at shell scripting. I have written a script that takes zero or more options and an optional path parameter. I want to use the current directory if a path parameter is not set.
This is the argument parsing section of the script:
OPTIONS=$(getopt -o dhlv -l drop-databases,help,learner-portal,verifier-portal -- "$#")
if [ $? -ne 0 ]; then
echo "getopt error"
exit 1
fi
eval set -- $OPTIONS
while true; do
case "$1" in
-d|--drop-databases) RESETDB=1
;;
-h|--help) echo "$usage"
exit
;;
-l|--learner-portal) LERPOR=1
;;
-v|--verifier-portal) VERPOR=1
;;
--) shift
break;;
*) echo -e "\e[31munknown option: $1\e[0m"
echo "$usage"
exit 1
;;
esac
shift
done
# Set directory of module
if [[ -n $BASH_ARGV ]]
then
MOD_DIR=$(readlink -f $BASH_ARGV)
fi
if [[ -n $MOD_DIR ]]
then
cd $MOD_DIR
fi
The script works as intended when called without and arguments, or when called with both options and a path.
However, when I run the script and only specify options, I get an error from readlink like so
$ rebuild_module -dv
readlink: invalid option -- 'd'
Try 'readlink --help' for more information.
Obviously, it's parsing the options wrong, but I'm not sure how to detect that I haven't passed a path, and therefore avoid calling readlink. How can I go about correcting this behaviour?
You can do [ $# -ne 0 ] instead of [[ -n $BASH_ARGV ]]. The former is affected by shift/set, but the latter isn't:
$ cat test.sh
echo "$#"
echo "${BASH_ARGV[#]}"
echo "$#"
eval set -- foo bar
shift
echo "$#"
echo "${BASH_ARGV[#]}"
echo "$#"
$ bash test.sh x y z
3
z y x
x y z
1
z y x
bar

Resources