Bash interprets too long arguments for an option - bash

I'm writing a bash script, that can take three options: c, p, and m; c can't have arguments, but p and m must have arguments. In my script I have these lines of code:
while getopts ":cp:m:" opt
do
case $opt in
c ) capitals=1
;;
m ) length=$OPTARG
;;
p ) number=$OPTARG
;;
\? ) echo "ERROR" 1>&2
exit 1
;;
esac
done
shift `expr $OPTIND - 1`
Now, when I write in the command line
myScript.sh -c -p 15 -m 12
all goes perfect. But when I write
myScript.sh -cp15m12
it goes wrong. Bash interprets the argument of the option 'p' as "15m12", while it should just be "15".
How can I solve this?

Since command-line arguments are not typed, there is no way to tell bash that the argument to -p must be an integer, and therefore no way for bash to infer that the correct way to parse -cp15m12 is as -c -p15 -m12 rather than as -c -p15m12. Since -c does not take an argument and getopts cannot define multi-character options, bash can infer that -cp15 is split into two separate options, -c and -p.

The value of getopts is fairly limited -- it's not hard to write your own replacement which supports option clustering. Here's a quick ad-hoc attempt.
#!/bin/sh
while true; do
case $1 in
-*) opt=${1#-}; shift;;
*) break;;
esac
while [ "$opt" ]; do
case $opt in
'-') opt=""; break 2;;
c*) capitals=1; opt=${opt#c};;
m[0-9]*)
opt=${opt#m}; length=${opt%%[!0-9]*}; opt=${opt#$length};;
m) opt=""; length=$1; shift;;
p[0-9]*)
opt=${opt#p}; number=${opt%%[!0-9]*}; opt=${opt#$number};;
p) opt=""; number=$1; shift;;
h|\?) echo "Syntax: $0 [-c] [-m length] [-p number]" >&2; exit 1;;
*) echo "Invalid option '$opt'" >&2; exit 2;;
esac
done
done
cat <<HERE
capitals: '$capitals'
length: '$length'
number: '$number'
args: '$#'
HERE
(Not sure if break 2 is entirely compatible with stock sh. Works for me on dash.)

Related

Bash optarg fails to spot missing arguments

I am inexperienced with bash shell scripting, and have run into a problem with bash optarg
Here's a small script to reproduce the problem:
#!/bin/sh
while getopts ":a:b:" opt; do
case ${opt} in
a ) echo "a=$OPTARG"
;;
b ) echo "b=$OPTARG"
;;
\? ) echo "Invalid option: $OPTARG" 1>&2
;;
: ) echo "Invalid option: $OPTARG requires an argument" 1>&2
esac
done
When I try this:
./args.sh -a av -b bv
I get the expected result:
a=av
b=bv
But when I omit the argument for -a:
/args.sh -a -b bv
I get this unfortunate result:
a=-b
When I would expect an error to show that the value of -a is missing.
It seems to have taken the -b argument as the value for -a.
Have I done something wrong & how can I achieve the expected behaviour?
The only positive advice is how do you treat But when I omit the argument for '-a', you cannot just skip to the next subsequent option. By convention getopts a: means you are expecting to an provide an arg value for the flag defined.
So even for the omitting case, you need to define an empty string which means the value for the arg is not defined i.e.
-a '' -b bv
Or if you don't expect the -a to get any arg values, better change the option string to not receive any as :ab:.
Any other ways of working around by checking if the OPTARG for -a is does not contain - or other hacks are not advised as it does not comply with the getopts() work flow.
getopts doesn't support such detection. So there's no way to do that with getopts.
You can probably write a loop around the arguments instead. something like:
#!/bin/sh
check_option()
{
case $1 in
-*)
return 1
;;
esac
return 0
}
for opt in $#; do
case ${opt} in
-a) shift
if check_option $1; then
echo "arg for -a: $1"
shift
else
echo "Invalid option -a"
fi
;;
-b) shift
if check_option $1; then
echo "arg for -b: $1"
shift
else
echo "Invalid option -b"
fi
;;
esac
done

Extending getopts from sourced script

I am trying to extend the getopts from the sourced script as below.
if an option other than a,b,c,d is passed in, it needs to print "invalid option" and exit.
script_a.sh:
#!/bin/bash
script_a_getopts() {
while getopts 'a:b:' OPT; do
case "$OPT" in
a)
a=$OPTARG
;;
b)
b=$OPTARG
;;
*)
echo "invalid option"
exit 1
;;
esac
done
}
script_b.sh
#!/bin/bash
source script_a.sh
while getopts ':c:d:' OPT; do
case "$OPT" in
c)
c=$OPTARG
;;
d)
d=$OPTARG
;;
[?])
script_a_getopts $#
esac
done
echo "a=$a"
echo "b=$b"
echo "c=$c"
echo "d=$d"
When I run the script, it don't work as expected, obviously I am making a mistake.
$ ./script_b.sh -c cat -d dog -a apple -b boy
a=
b=
c=cat
d=dog
Didn't throw error when -x is passed.
$ ./script_b.sh -x
a=
b=
c=
d=
Short Answer: You have to rollback the OPTIND before calling script_a_getopts.
case "$OPT" in
c)
c=$OPTARG
;;
...
[?])
let OPTIND--
script_a_getopts $#
esac
done
Long Answer:
The getopts track which arguments have been processed using the OPTIND variable. When the top getopts recognized unknown item, it has already 'consumed' it by moving OPTIND to the next argument. To get script_a_getopts to process that argument, the OPTIND need to be rolled back to point to the unprocessed argument.
The let OPTIND-- will allow the unrecognized argument to be re-processed.
Side note, if you want to allow options to be placed in arbitrary order (-c cat -a apple -d dog -b boy) you will have to repeat the same in the script_a_getopts. This will require improved error processing in script_a_getopts function.

How to take 2 argument from getopts

I am creating a bash script that will two argument from the user from command line . But i am not sure how I can take 2 argument from the user and both arguments are required if not passed will show error and return from the script . Below is the code i am using to take argument from the user , but currently my getopts is taking only one argument.
optspec="h-:"
while getopts "$optspec" optchar; do
case "${optchar}" in
-)
case "$OPTARG" in
file)
display_usage ;;
file=*)
INPUTFILE=${OPTARG#*=};;
esac;;
h|*) display_usage;;
esac
done
How could i add an an option to take one more args from command line. Like below
script.sh --file="abc" --date="dd/mm/yyyy"
getopts does not support long arguments. It only supports single letter arguments.
You can use getopt. It is not as widely available as getopts, which is from posix and available everywhere. getopt will be for sure available on any linux and not only. On linux it's part of linux-utils, a group of most basic utilities like mount or swapon.
Typical getopt usage looks like:
if ! args=$(getopt -n "your_script_name" -oh -l file:,date: -- "$#"); then
echo "Error parsing arguments" >&2
exit 1
fi
# getopt parses `"$#"` arguments and generates a nice looking string
# getopt .... -- arg1 --file=file arg2 --date=date arg3
# would output:
# --file file --date date -- arg1 arg2 arg3
# the idea is to re-read bash arguments using `eval set`
eval set -- "$args"
while (($#)); do
case "$1" in
-h) echo "help"; exit; ;;
--file) file="$2"; shift; ;;
--date) date="$2"; shift; ;;
--) shift; break; ;;
*) echo "Internal error - programmer made an error with this while or case" >&2; exit 1; ;;
esac
shift
done
echo file="$file" date="$date"
echo Rest of arguments: "$#"

Getopts considers my option chain as the argument for the first one

The following code is executed in subscript.sh, and subscript.sh is run inside primaryscript.sh
#### primaryscript.sh
#! /bin/bash
#...
"$bss_path"/subscript.sh "$option"
#...
I am using this code to parse my arguments and their values:
#### subscript.sh
#! /bin/bash
while getopts "hw:l:s:b:" opt; do
case $opt in
w)
x="$OPTARG";;
l)
xx="$OPTARG";;
s)
xxx="$OPTARG";;
b)
xxxx="$OPTARG";;
h)
print_usage
exit 0;;
\?)
echo "Invalid option: -$OPTARG"
exit 1;;
esac
done
The problem appears when I call my script with multiple arguments:
./myscript -l 5.0.3-0 -s 4.0-0 -b 010
getopts thinks that option l have 5.0.3-0 -s 4.0-0 -b 010 as argument.
What am I doing wrong?
I tried to play around with the : and the option, but as I understood I have to put them behind the options taking an argument right?
And getopts naturally knows that - is the separator for arguments.
$> echo $BASH_VERSION
$> 3.2.25(1)-release
As Cyrus pointed out in the comment, the problem was how I passed arguments.
./myscript "$options"
The correct way is :
./myscript $options
Since "$options" won't care of the spaces and take the whole string as a single argument.
It works for me. You can read more about GETOPTS, OPTARG as well as the while loop and case statement by typing "man bash" in your shell and searching.
#!/bin/bash
while getopts "hw:l:s:b:" opt; do
case $opt in
w)
x="$OPTARG";;
l)
xx="$OPTARG";;
s)
xxx="$OPTARG";;
b)
xxxx="$OPTARG";;
h)
print_usage
exit 0;;
\?)
echo "Invalid option: -$OPTARG"
exit 1;;
esac
done
echo $xx
echo $xxxx
echo $xxx
Output:
5.0.3-0
010
4.0-0

Pass parameters as option in custom getopts script in bash

I'd like to pass options as a parameter. E.g.:
mycommand -a 1 -t '-q -w 111'
The script cannot recognize a string in quotes. I.e it gets only part of the string.
getopts works the same - it see only -q.
For custom getopts I use similar script (example):
while :
do
case $1 in
-h | --help | -\?)
# Show some help
;;
-p | --project)
PROJECT="$2"
shift 2
;;
-*)
printf >&2 'WARN: Unknown option (ignored): %s\n' "$1"
shift
;;
*) # no more options. Stop while loop
break
;;
--) # End of all options
echo "End of all options"
shift
break
;;
esac
done
Maybe I misunderstand the question, but getopts seems to work for me:
while getopts a:t: arg
do
case $arg in
a) echo "option a, argument <$OPTARG>"
;;
t) echo "option t, argument <$OPTARG>"
;;
esac
done
Run:
bash gash.sh -a 1 -t '-q -w 111'
option a, argument <1>
option t, argument <-q -w 111>
Isn't that what you want? Maybe you missed the : after the options with arguments?

Resources