Bash use getopts but enforce the options - bash

Is possible to use getopts to force the user that run the script, to add the options?
I am not asking how to make an option to require a parameter (done with the : after the option), but how to actually tell the user that he need to add the -something when running the script.
Something like myscript.sh -f FILENAME; and if the user run the script as myscript.sh FILENAME he will get an error because he didn't add the -f option.
As now I check if $1 is empty or not, to print the usage message; and another statement to check if the -f option is in what the user passed; although if you have 10 options, you add 10 conditional statements? That feels a bit off and not efficient.
Once again, I am not asking how to handle getopts parameters, but the options themselves. I think the question is pretty clear to show that this has nothing to do with the answer mentioned as possible duplicate of this question.

Sounds like you want something like tar, which (usually) requires the user to specify an operation mode followed (usually) by the file name arguments. If that is the case, then no getopts alone will not help you. However, you can still use getopts to manage the arguments:
#!/bin/bash
help() {
cat <<EOTXT
${1:-This program does something.}
USAGE:
${0##*/} <-abcde> <thing>
WHERE:
-a Sets mode a
...
EOTXT
}
mode=0
OPTIND=1
while getopts abc opt; do
case "${opt}" in
a|b|c) mode=${opt};;
?) help "Unrecognized option"; exit 1;
esac
done
shift "$((OPTIND-1))"
[[ 0 == ${mode} ]] && { help "Missing mode: use one of -a, -b, or -c"; exit 1; }
[[ 0 == $# ]] && { help "Missing thing argument"; exit 1; }
Here you use the typical while getopts construct to process your arguments. When it detects one of your required flags, you set the bookkeeping variable ($mode here) to the value. Then at the end, you check that you have both mode and an extra argument.
Thus all of these test cases fail:
my-program # no mode or argument
my-program -a # mode, but no argument
my-program foo # argument, but no mode
my-program -d # bad mode, no argument
my-program -d foo # bad mode with argument
Only this passes:
my-program -a foo

Related

Flag argument is another flag

Trying to read my flag and their arguments,
I came accros a case where passing a flag with an expected argument,
fallowed by another flag instead of the expected argument,
would result in the second flag being interpreted as argument for the first flag.
WWW_ALIAS=0
while getopts ':d:a:w' flag; do
case "${flag}" in
d)
DOMAIN_NAME=${OPTARG}
;;
a)
IFS=',' read -ra DOMAIN_ALIASES <<< "${OPTARG}"
;;
w)
WWW_ALIAS=1
;;
:)
echo "[Error] Argument for option -${OPTARG} was omitted." 1>&2
exit 1
;;
\?)
echo "[Warning] Option ${OPTARG} is not supported and will be ignored." 1>&2
;;
esac
done
if [ -z "${DOMAIN_NAME}" ]; then
echo "[Error] Domain parameter (-d) must be set" 1>&2
exit 1
fi
Thus, running ./script.sh -w -d will trigger the error message at the end of this code example.
But running ./script.sh -d -w will instead assign -w to the DOMAIN_NAME variable while it shouldn't.
Is there a way to make sure that any flag can't be used as argument for a flag ?
The supported syntax for getopts is:
a - option -a without value; error on unsupported options
a: - option -a with value; error on unsupported options
ab - check for options -a, -b; error on unsupported options
:ab - check for options -a, -b; silences errors on unsupported options
Since you specified d:, the argument parser will check for an additional value after the switch -d. Not specifying one, just like in this command: ./script.sh -w -d will trigger an error from the argument parser (not your code).
Since you specified w without a (: after it), the argument parser will NOT check for an additional value after the switch -w. Hence you don't see an error for that flag.
When you run with -d -w, the parser sees only the first switch, which is -d, and it consumes the next token as the argument, which is the expected outcome.
As there a way to make sure that any flag can't be used as argument for a flag?
Yes, there are a couple of options actually.
Option 1: inside the menu, add a sanity check to allow only reasonable values:
...
case "${flag}" in
d)
if [[ "${OPTARG}" == -* ]]; then
echo "Bad argument!"
exit 1
fi
DOMAIN_NAME=${OPTARG}
;;
...
Option 2: Use eval set -- "$OPTS"
Some don't like this option because eval is evil. But it's still an option if you are not afraid of this attack vector.
This command will sort the arguments and will prevent things like this from happening.
Find an example here

Bug in parsing args with getopts in bash

I was trying to modify the bd script to use getopts. I am a newbie at bash scripting
my script is
while getopts ":hvis:d:" opt
do
...
done
...
echo $somedirpath
cd "$somedirpath"
this runs fine when doing
$ ./bd -v -i -s search
or
$ ./bd -is search -d dir
But when running it like this
$ . ./bd -s search
getopts doesn't read the arguments at all. And all the variables I set in the while loop according to the arguments are all not set, so the script no longer works. Please help!
Setting OPTIND=1 before invoking getopts works fine.
The problem is that getopts relies on OPTIND to loop through the arguments provided, and after sourcing the script, it will be set to some value greater than 1 by getopts according to how many arguments you pass. This value gets carried over even after the script ends(because its being sourced). So the next time its sourced, getopts will pick up from that OPTIND, rather than starting from 1!
This might cause strange behaviour with other scripts, and I don't know how safe this is. But it works!
For a better workaround, I think what #tripleee suggests looks safe and robust.
When you source a script, the arguments parsed by getopts are those of the current shell, not the parameters on the source command line.
The common workaround is to have your script merely print the path, and invoke it like cd "$(bd)" instead (perhaps indirectly through a function or alias).
Setting OPTIND=1 may not work reliably on zsh. Try to use something different than getopts:
while [ "$#" -gt 0 ]
do
case "$1" in
-h|--help)
help
return 0
;;
-o|--option)
option
return 0
;;
-*)
echo "Invalid option '$1'. Use -h|--help to see the valid options" >&2
return 1
;;
*)
echo "Invalid option '$1'. Use -h|--help to see the valid options" >&2
return 1
;;
esac
shift
done

Making a CLI command using an SH script [closed]

Closed. This question needs to be more focused. It is not currently accepting answers.
Want to improve this question? Update the question so it focuses on one problem only by editing this post.
Closed 9 years ago.
Improve this question
I want to make a Pathogen helper script using a .sh file. I know if you make it executable it can be run as a command, but I have no idea how to do -o --options or arguments or anything like that.
Basically that's what I want answered, really all I need to know is how to do something like:
pathogen install git://...
Or something along those lines. Any help is appreciated. :)
The bash builtin getopts does not handle long arg parsing mechanism as far as I know.
getopt(1) is the tool you are looking for.
Not a program entirely, but you'll get the idea
PARSED_OPTIONS=$(getopt -n "$0" -o h123: --long "help,one,two,three:" -- "$#")
while true;
do
case "$1" in
-h|--help)
echo "usage $0 -h -1 -2 -3 or $0 --help --one --two --three"
shift;;
-1|--one)
echo "One"
shift;;
--)
shift
break;;
esac
done
Take a look at code example and explanation given here.
Passing arguments is the easiest of the two (see "What are special dollar sign shell variables?" on SO):
#!/bin/sh
echo "$#"; # total number of arguments
echo "$0"; # name of the shell script
echo "$1"; # first argument
Assuming the file is named "stuff" (sans an extension) and the result of running ./stuff hello world:
3
stuff
hello
To pass in single letter switches (w/ optional associated params), e.g. ./stuff -v -s hello you'll want to use getopts. See "How do you use getopts" on SO and this great tutorial. Here is an example:
#!/bin/sh
verbose=1
string=
while getopts ":vs:" OPT; do
case "$OPT" in
v) verbose=0;;
s) string="$OPTARG";;
esac;
done;
if verbose; then
echo "verbose is on";
fi;
echo "$string";
The line having getopts coupled with while needs further explanation:
while - start the while loop, going through everything getopts returns back after it processes
getopts :vs: OPT; - the program getopts with 2 arguments :vs: and OPT
getopts - returns something while can iterate over
:vs: - the first argument, this describes what switches getopts will look for while it parses the shell line
: - the first colon takes getopts out of debug mode, omit this to make getopts verbose
v - find the switch -v, this will not have an argument after it, just a simple switch
s: - find the option -s with an argument after it
OPT - will store the character used (the name of the switch), e.g. "v" or "s"
OPTARG - the variable to load the value into during each of while's iterations. For v, $OPTARG will not have a value, but for s it will.
The colon : tells getopts to look for an argument after the switch. The only exception is if the sequence of characters starts with : then it toggles getopts in/out of debug/verbose mode. For example:
getopts :q:r:stu:v will take getopts out of debug mode, will tell it that switches q, r, and u will expects args, while s, t, and u won't. This would be applicable for something like: stuff -q hello -r world -s -t -u 123 -v
getopts tuv will only tell getopts to search for switches t, u and v with no arguments, e.g. stuff -t -u -v, and to be verbose

Parsing a flag with a list of values

I'm creating a bash script which involves parsing arguments. The usage would be:
$ ./my_script.sh -a ARG_1 -b ARG_2 [-c LIST_OF_ARGS...]
Using getopts I'm able to parse -a and -b and get their respective values ARG_1 and ARG_2. If and only if user places -c as last argument, then I'm also able to get -c and create a list with all values in LIST_OF_ARGS....
But I would not like to force user to insert -c as the last flag. For instance, it would be great if the script can be invoked this way:
$ ./my_script.sh -b ARG_2 -c V1 V2 V3 -a ARG_1
Here is my current code:
while getopts a:b:c opt
do
case $opt in
a)
A_FLAG=$OPTARG
;;
b)
B_FLAG=$OPTARG
;;
c)
# Handle values as regular expressions
args=("$#")
C_LIST=()
for (( i=$OPTIND-1 ; i <= $#-1 ; i++ ))
do
C_LIST=("${C_LIST[#]}" ${args[$i]})
done
;;
?)
usage
;;
esac
done
You need to separate your detection of the -c flag with the processing associated with it. For example, something like:
while getopts a:b:c opt
do
case $opt in
a)
A_FLAG=$OPTARG
;;
b)
B_FLAG=$OPTARG
;;
c)
C_FLAG=1
;;
?)
usage
;;
esac
done
# discard all of our options.
shift `expr $OPTIND - 1`
if [ "$C_FLAG" = 1 ]; then
# Handle values as regular expressions
args=("$#")
C_LIST=()
for (( i=0 ; i <= $#-1 ; i++ ))
do
C_LIST=("${C_LIST[#]}" ${args[$i]})
done
fi
This script doesn't collect all the non-option arguments until after processing all the command line options.
Here's a question: why have a -c option at all?
If the full usage involves a list of values, why not just have no -c option and allow the -a and -b options only while the rest are regular args as in ./myscript.sh -a ARG_1 -b ARG_2 [argument ...], where any arguments are optional (like the -c option and its arguments are in your usage example?
Then your question becomes "how do I intersperse program options and arguments", to which I would respond: "You shouldn't do this, but to achieve this anyway, parse the command line yourself; getopts won't work the way you want it to otherwise."
Of course, parsing is the hard way. Another possibility involves adding the values after -c to a list, so long as you don't encounter another option or the end of the options:
C_LIST=()
while getopts a:b:c: opt; do
#Skipping code...
c)
C_LIST+="$OPTARG"
shift $(expr $OPTIND - 1)
while [ -n "$1" ] && [ $(printf "%s" "$1" | grep -- '^[^-]') ]; do
C_LIST+="$1"
shift
done
OPTIND=1
;;
The behaviour of getopts is mimicked: even if OPTARG begins with a '-' character, it is still kept, but after OPTARG, any string starting with the '-' character may simply be an invalid option such as -n. I used printf instead of echo because some versions of echo, such as the one that bash has built-in, have a -e option that may or may not allow the loop to continue, which isn't desired. The grep expression should prevent this, but who knows if that version of echo allows for -e'hello', which would cause grep to succeed because it sees "hello"? While possibly unnecessary, why take chances?
Personally, I'd avoid this behaviour if you can, but I also don't understand why you're asking for this behaviour in the first place. If I were to recommend anything, I'd suggest the more common /path/to/script -a ARG_1 -b ARG_2 [argument ...] style above any other possible choice of implementation.
On my system, I haven a /usr/share/doc/util-linux/examples/getopt-parse.bash file. It puts the result of getopt into a variable, and set the positional parameters to that variable. Then uses a switch similar to yours, but uses shift to remove arguments when found.
You could do something similar, but for your -c option use shift until you get an option or run out of arguments.
Or it might be enough for you to use your current solution, but remember to set the OPTIND variable after the loop.

How can I prevent an option that requires an argument to use the next option when no argument is given?

I have the following code in test.sh:
while getopts "f:i:" opt; do
case $opt in
f)
echo $OPTARG
i) echo $OPTARG
Now if I run ./test.sh -f I will get the error:
option requires an argument -- i
However, when I run ./test.sh -f -i test it will echo -i.
I know that this is because it just gets the next argument separated by a space, but is there an easy way to handle this?
I could do if [ $OPTARG == "-i" ]; then exit 1 but I'm hoping there is an easier way for when I have multiple options.
If you are using getopts, it has it's own ways. Just go with it.
After all, who says the option's argument cannot begin with a dash? If it's a filename, maybe the user wants the filename to begin with a dash. If it's a number, maybe it is a negative number.

Resources