Is it possible to implement a boolean cli option using getopts in bash? Basically I want to do one thing if -x is specified and another if it is not.
Of course it is possible. #JonathanLeffler already pretty much gave the answer in the comments to the question, so all I'm going to do here is add an example of the implementation and a few niceties to consider:
#!/usr/bin/env bash
# Initialise option flag with a false value
OPT_X='false'
# Process all options supplied on the command line
while getopts ':x' 'OPTKEY'; do
case ${OPTKEY} in
'x')
# Update the value of the option x flag we defined above
OPT_X='true'
;;
'?')
echo "INVALID OPTION -- ${OPTARG}" >&2
exit 1
;;
':')
echo "MISSING ARGUMENT for option -- ${OPTARG}" >&2
exit 1
;;
*)
echo "UNIMPLEMENTED OPTION -- ${OPTKEY}" >&2
exit 1
;;
esac
done
# [optional] Remove all options processed by getopts.
shift $(( OPTIND - 1 ))
[[ "${1}" == "--" ]] && shift
# "do one thing if -x is specified and another if it is not"
if ${OPT_X}; then
echo "Option x was supplied on the command line"
else
echo "Option x was not supplied on the command line"
fi
A few notes about the above example:
true and false are used as option x indicators because both are valid UNIX commands. This makes the test for the option presence more readable, in my opinion.
getopts is configured to run in silent error reporting mode because it suppressed default error messages and allows for a more precise error handling.
the example includes fragments of code for dealing with missing option arguments and post-getopts command line arguments. These are not part of the OP's question.
They are added for the sake of completeness as this code will be required in any reasonably complex script.
For more information about getopts see Bash Hackers Wiki: Small getopts tutorial
Related
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
In bash shell scripting, I'm trying to take the argument that comes before the flag.
When the argument comes after the flag, I know that I could use getopts and have the case smth like echo "there's an -g flag! Argument: $OPTARG
However I have no clue how to take an argument that comes before the flag. Let's say I would like to process this command: ./filename 2345 -g.
And the argument is a PID that the flag is trying to take argument as.
Thanks in advance!
Assuming Best Practices
Let's say your -g stands for global, and that you support passing -g either before or after the number whose meaning it changes. A mostly-conventional parser (not compliant with baseline POSIX conventions only inasmuch as the latter require all options to come before any positional arguments) might look a bit like the following:
#!/usr/bin/env bash
args=( )
global=0
while (( $# )); do
case $1 in
-g) global=1 ;;
--) shift; args+=( "$#" ); break ;;
-*) echo "Unrecognized argument $1" >&2; exit 1 ;;
*) args+=( "$1" ) ;;
esac
shift
done
if (( global )); then
echo "Doing something with global PID ${args[0]}"
fi
That is to say: Store your positional arguments in a separate location (in this case, the args array), and refer back to them as-needed.
Real, Literal (Awful) Answer
If you really want to store your last argument in a variable and refer back to that variable when you see -g, of course, you can do that:
#!/usr/bin/env bash
last_arg=
while (( $# )); do
case $1 in
-g) global=$last_arg ;;
esac
last_args=$1
shift
done
if [[ $global ]]; then
echo "Global value is $global"
fi
...however: Don't. This violates both POSIX and GNU command-line utility conventions, and thus will be surprising to any of your users who are long-time UNIX users.
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
I want to write a command line tool like git which will follow the POSIX standards. It will take the options like --help or -h , --version ..etc. But i am not getting how to do it. Can anybody tell me how to do this using bash scripting. Please help me. This is something very new to me.
Example : if the name of my tool is Check-code then i want to use the tool like ;
Check-code --help
or
Check-code --version
So far as I know, "long options", like --help and --version are not POSIX standard, but GNU standard. For command-line utilities the POSIX standard says:
The arguments that consist of hyphens and single letters or digits, such as 'a', are known as "options" (or, historically, "flags").
To support POSIX short options options it is worth getting to know getopts (there are tutorials on the web), but it does not support GNU long options.
For long options you have to roll your own:
filename=default
while (( $# > 0 ))
do
opt="$1"
shift
case $opt in
--help)
helpfunc
exit 0
;;
--version)
echo "$0 version $version"
exit 0
;;
--file) # Example with an operand
filename="$1"
shift
;;
--*)
echo "Invalid option: '$opt'" >&2
exit 1
;;
*)
# end of long options
break;
;;
esac
done
You can use the 'getopts' builtin, like so:
#!/bin/bash
# Parse arguments
usage() {
echo "Usage: $0 [-h] [-v] [-f FILE]"
echo " -h Help. Display this message and quit.
echo " -v Version. Print version number and quit.
echo " -f Specify configuration file FILE."
exit
}
optspec="hvf:"
while getopts "$optspec" optchar
do
case "${optchar}" in
h)
usage
;;
v)
version
;;
f)
file=${OPTARG}
;;
*)
usage
;;
esac
done
This only works for single character options, not for long options like -help or --help. In practice, I've never found that this is a significant restriction; any script which is complex enough to require long options is probably something that I would write in a different language.
There is probably a better way to do this, but here is what I find useful:
Each argument is represented by a variable in BASH. The first argument is $1. The second is $2, and so on. Match an option string with the first argument, and if it matches run some code accordingly.
Example:
#!/bin/bash
if [ $1 == "--hello" ]
then
echo "Hello"
else
echo "Goodbye"
fi
If you code in C or C++, then use the **argv variable. **argv is a double pointer that holds a list of all arguments passed to the program (with argv[0] being the program name).
I have a bash script which is getting options (using getopts). Let say they are '-n' , '-d' , '-u' . I only want to have one of the option being chosen, if not, it will prompt the user error.
The code is like this:
while getopts ":dun" name; do
case $name in
d )
DELETE='YES'
;;
u )
UPDATE='YES'
;;
n )
NEW='YES'
;;
esac
done
I can only have $DELETE or $UPDATE or $NEW being 'YES' in one time, meaning the user cannot specific '-n' and '-d' in the same time or '-u' and '-n' in the same time, how do I achieve that in a IF statement ?
I have been looking for this in stackoverflow, but can't find any. Thanks for the help, mate!
You can increment a counter every time getopts() senses one of the valid commandline options. Then, after the loop test the counter's value. If it is greater than one, then multiple options were specified.
This is a complete hack, and depends on the option variables being either unset or "YES" (and in the form I've written it, is bash-only):
if [[ "$DELETE$UPDATE$NEW" == YESYES* ]]; then
echo "Please only use one of -d, -u, and -n" >&2
exit 1
fi
(If you were using the brand-x shell instead of bash, it'd be something like if [ "$DELETE$UPDATE$NEW" = "YESYES" -o "$DELETE$UPDATE$NEW" = "YESYESYES" ]; then)
You're better off setting your variables to true or false, so you can simply write
if $DELETE; then
echo "You've already specified UPDATE!"
fi
and similar.
However, options are called that because they are supposed to be optional, and usually orthogonal. (There are a few instances where options of common utilities exclude each other, but the vast majority don't). What you want is really a mandatory mode of operation, so you shouldn't receive it as an option at all, but simply as the first command-line argument.
You can use some getopt-kludge to manage having only one of the option being chosen.
#!/bin/bash
while [ $# -gt 0 ]; do
case "$1" in
-u)
echo "got -u"
break
;;
-d)
echo "got -d"
break
;;
-n)
echo "got -n"
break
;;
*)
echo "some error"
break
;;
esac
done