Parsing a flag with a list of values - bash

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.

Related

Ways to provide list of parameters to options in shell scripts? [duplicate]

This question already has answers here:
How do I parse command line arguments in Bash?
(40 answers)
Closed 2 years ago.
Basically I am making a script where I would like to give a list of parameters to some option, say -l, eg:
usr#domain:$./somescript -l hi bye cry nigh
The list specified to -l would be then stored into a specific array and iterated over.
So far, I have been using these lines from Arch linux's mkinitcpio script to do so. This allows one to specify a comma-separated list to an option:
_optaddhooks()
while :; do
case $1 in
-A|--add|--addhooks)
shift
IFS=, read -r -a add <<< "$1"
_optaddhooks+=("${add[#]}")
unset add
;;
What are some other methods to do this, particularly when other options/parameters will be used? Can the list provided to an option be space separated? Or is specifying a list by "," (or some other character") easier to manage?
UPDATE: OP is looking for a non-getopts solution; adding a -l option to the current code:
NOTE: OP has shown the command line flag as -l so not sure what the -A|-add|--addhooks) relates to ... ???
unset _optaddhooks
while :; do
case "${1}" in
-A|--add|--addhooks)
shift
....
;;
-a) shift
vara="${1}"
;;
-l) shift
_optaddhooks=( ${1} )
;;
-z) shift
varz="${1}"
;;
....
esac
done
OP would still use the same command line as in the earlier answer (below):
$ ./somescript -a this_is_a -l 'hi bye cry nigh' -z 123456
If you're willing to wrap the -l args in quotes you could then pass the args as a single parameter and then have bash parse this parameter however you wish.
One idea to load the -l args into an array named _optaddhooks[]:
$ cat somescript
#!/usr/bin/bash
while getopts :a:l:z: opt
do
case ${opt} in
a) vara="${OPTARG}" ;;
l) _optaddhooks=( ${OPTARG} ) ;;
z) varz="${OPTARG}" ;;
*) echo "Sorry, don't understand the option '${opt}'" ;;
esac
done
typeset -p vara _optaddhooks varz
A sample run:
$ ./somescript -a this_is_a -l 'hi bye cry nigh' -z 123456
declare -- vara="this_is_a"
declare -a _optaddhooks=([0]="hi" [1]="bye" [2]="cry" [3]="nigh")
declare -- varz="123456"
While this was a simple example, it should be easy enough to modify the code (l section of the case statement) as needed.

how to pass other arguments besides flags

I am trying to execute my file by passing in an absolute path as the first argument ($1). I also want to add flags from that absolute path onward, but i do not know how to tell optargs to start counting from $2 forward since if i pass in the absolute path as the $1 it seems to break the getopts loop.
I'm gussing i have to implement a shift for the first argument in the following code:
while getopts :lq flag; do
case $flag in
l) echo "executing -l flag"
;;
q) echo "executing -q flag"
;;
esac
done
I'm not sure how to approach this. Any tips are welcome, thank you.
getopts does, indeed, stop processing the arguments when it sees the first non-option argument. For what you want, you can explicitly shift the first argument if it is not an option. Something like
if [[ $1 != -* ]]; then
path=$1
shift
fi
while getopts :lq flag; do
...
done
Keep the options before file argument (i.e. absolute path).
Many standard bash commands follow the same practice.
Example :
wc -wl ~/sample.txt
ls -lR ~/sample_dir
So if you follow the above practice, your code goes like this.
This code works even if options are not provided.
In general, that is the desired behavior with options.
# Consider last argument as file path
INPUT_FILEPATH=${*: -1}
echo $INPUT_FILEPATH
# Process options
while getopts :lq flag
do
case $flag in
l) echo "executing -l flag"
;;
q) echo "executing -q flag"
;;
esac
done
Sample execution :
bash sample.sh /home/username/try.txt
/home/username/try.txt
bash sample.sh -lq /home/username/try.txt
/home/username/try.txt
executing -l flag
executing -q flag

Run shell command several times, based on number of vars given in getopts

I want to run a command n number of times, depending on how many arguments were given to the shell, using the getopts function.
Here's the contents of script.sh:
#!/bin/bash
while getopts "i" flag
do
case "$flag" in
i) name="$OPTARG";;
esac
done
echo $name
I would like the echo command to run as many times as names are given. For example if I run ./script.sh -i One, Two, Three, Four I would like the script to run echo 4 times and print the names to the shell.
Ideally, you give a single comma-delimited word as the argument to -i, then simply split it on the comma (note: requires bash due to the use of an array and the -a option to read).
while getopts "i:" flag
do
case "$flag" in
i) IFS=, read -a names <<< "$OPTARG";;
esac
done
printf '%s\n' "${names[#]}"
Then the following should work:
$ ./script.sh -i One,Two,Three,Four
One
Two
Three
Four
getopts isn't really designed to handle an arbitrary number of arguments to an option.
I would do something like this :
#!/bin/bash
if
[[ "$1" = -i ]]
then
shift
for arg in "$#"
do
echo "$arg"
done
fi
Maybe it does not work in your specific case depending on other options you need to handle, but I lack information to be sure.

Saving argument that comes before the flag

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.

Can getopts parse a subset of a bash script's arguments and leave the rest intact?

I am using getopts to parse arguments in a bash script. I want to do two things:
remove processed options from "$#"
leave unprocessed options in "$#"
consider the command-line
$ foo -a val_a -b val_b -c -d -e -f val_f positional_l positional_2 ...
Where foo uses getopts to parse options defined by a optstring of 'b:c' and afterwards needs to leave "$#" as
`-a val_a -d -e -f val_f positional_l positional_2 ...`
I need to do two things:
parse a subset of options that may be given
leave all other opptions intact
The reason for this is because foo must use the options it recognises to determine another script bar to which it must pass the remaining "#".
Normally getopts stops when it encounters an unrecognised option but I need it to continue (up to any --). I need it to proceess and remove the options it recognises and leave alone those that it doesn't.
I did try to work around my problem using -- between the foo options and the bar options but getopts seems to baulk if the text following -- begins with a - (I tried but could not escape the hyphen).
Anyway I would prefer not to have to use -- because I want the existence of bar to be effectively transparent to the caller of foo, and I'd like the caller of foo to be able to present the options in any order.
I also tried listing all baroptions in foo (i.e. using 'a:b:cdef:'for the optstring) without processing them but I need to delete the processed ones from "$#" as they occur. I could not work out how to do that (shift doesn't allow a position to be specified).
I can manually reconstruct a new options list (see my own answer) but I wondered if there was a better way to do it.
Try the following, which only requires the script's own options to be known in advance:
#!/usr/bin/env bash
passThru=() # init. pass-through array
while getopts ':cb:' opt; do # look only for *own* options
case "$opt" in
b)
file="$OPTARG";;
c) ;;
*) # pass-thru option, possibly followed by an argument
passThru+=( "-$OPTARG" ) # add to pass-through array
# see if the next arg is an option, and, if not,
# add it to the pass-through array and skip it
if [[ ${#: OPTIND:1} != -* ]]; then
passThru+=( "${#: OPTIND:1}" )
(( ++OPTIND ))
fi
;;
esac
done
shift $((OPTIND - 1))
passThru+=( "$#" ) # append remaining args. (operands), if any
./"$file" "${passThru[#]}"
Caveats: There are two types of ambiguities that cannot be resolved this way:
For pass-thru options with option-arguments, this approach only works if the argument isn't directly appended to the option.
E.g., -a val_a works, but -aval_a wouldn't (in the absence of a: in the getopts argument, this would be interpreted as an option group and turn it into multiple options -a, -v, -a, -l, -_, -a).
As chepner points out in a comment on the question, -a -b could be option -a with option-argument -b (that just happens to look like an option itself), or it could be distinct options -a and -b; the above approach will do the latter.
To resolve these ambiguities, you must stick with your own approach, which has the down-side of requiring knowledge of all possible pass-thru options in advance.
You can manually rebuild the options list like this example which processes the -b and -c options and passes anything left intact.
#!/bin/bash
while getopts ":a:b:cdef:" opt
do
case "${opt}" in
b) file="$OPTARG" ;;
c) ;;
*) opts+=("-${opt}"); [[ -n "$OPTARG" ]] && opts+=("$OPTARG") ;;
esac
done
shift "$((OPTIND-1))"
./$file "${opts[#]}" "$#"
So
./foo -a 'foo bar' -b bar -c -d -e -f baz one two 'three and four' five
would invoke bar, given as the argument to option b, as
./bar -a 'foo bar' -d -e -f baz one two 'three and four' five
This solution suffers the disadvantage that the optstring must include the pass-through options (i.e. ":a:b:cdef:" instead of the preferable ":b:c").
Replacing the argument list with the reconstructed one can be done like this:
set -- "${opts[#]}" "$#"
which would leave "$#" containing the unprocessed arguments as specified in the question.

Resources