Escape accumulated arguments in a shell script? - shell

With a view to a bug in git, at the moment git-submodule.sh reads (reordered):
[iterating over command line arguments]
--reference)
case "$2" in '') usage ;; esac
reference="--reference=$2"
shift
;;
--reference=*)
reference="$1"
shift
;;
[...]
if test -n "$reference"
then
git-clone $quiet "$reference" -n "$url" "$path" --separate-git-dir "$gitdir"
else
git-clone $quiet -n "$url" "$path" --separate-git-dir "$gitdir"
fi ||
die "$(eval_gettext "Clone of '\$url' into submodule path '\$path' failed")"
This uses only the last argument given by --reference. I now want to enhance this so that all --reference options are passed on to git-clone. This is trivial for trivial arguments (reference="$reference --reference=$2"), but my mind boggles when thinking about arguments containing white space, quote or shell meta characters.
What is the best practice to escape such accumulated arguments?

Best practice would be to use a bash array:
declare -a references
#...
--reference)
case ... esac
references+=("--reference=$2")
shift
;;
--reference=*)
references+=("$1")
shift
;;
#...
# no need to test the array for emptiness
git-clone $quiet "${references[#]}" -n "$url" "$path" --separate-git-dir "$gitdir"
However, the referenced script uses /bin/sh instead.

Related

Bash - Wrapping another command's parameters

Wrapping another command's parameters
I have a command tool1 that parses arguments this way:
#!/usr/bin/env bash
# ...
while [[ $# -ge 1 ]]
do
key="$1"
case $key in
-o|--option)
OPT="$2"
shift
;;
-u|--user)
USR="$2"
shift
;;
-*)
echo -e "Unrecognized option: \"$key\"" && exit 1
;;
*)
OTHERS+=("$1")
;;
esac
shift
done
# ...
I have tool2 that calls tool1. Thus tool2 will have to pass parameters to tool1. It may also need to process the same parameters (--user)
tool2 looks like:
#!/usr/bin/env bash
# ...
while [[ $# -ge 1 ]]
do
key="$1"
case $key in
-O|--option2)
opt2="$2"
shift
;;
-u|--user)
USR="$2"
OTHERS+=("-u $2")
shift
;;
-*)
echo -e "Unrecognized option: \"$key\"" && exit 1
;;
*)
OTHERS+=("$1")
;;
esac
shift
done
## Call tool1 with other parameters to pass
bash tool1.sh ${OTHERS[#]}
# ...
To sum up
--option2 is an option used only by tool2.
--user is common to both tools, and may be used by tool2 too, before calling tool1.sh. Because of this, in this example --user has to be explicitly passed to tool1 thanks to the array OTHERS.
I'd like to know about possible and/or alternative ways of dealing with such parameter redundancies. A methodology that would help me wrapping another tool's expected parameters/options, without having to copy/paste the lines regarding the parsing of such redundant parameters/options.
tool2's approach is fine. However, you aren't setting OTHERS correctly.
-u|--user)
USR="$2"
OTHERS+=("-u" "$2")
shift
-u and its argument need to remain separate array elements, just as they were separate arguments to tool2. You also need to quote the expansion of OTHERS, to preserve arguments containing word-splitting characters or globs:
bash tool1.sh "${OTHERS[#]}"
Finally, all-uppercase variable names are reserved for use by the shell itself; don't define such names yourself. Just use others instead of OTHERS.

Bash: handling mass arguments

I'd like to be able to handle multiple arguments to a given flag no matter what the order of flags is. Do you guys think this is acceptable? Any improvements?
So:
$ ./script -c opt1 opt2 opt3 -b foo
opt1 opt2 opt3
foo
Code:
echo_args () {
echo "$#"
}
while (( $# > 0 )); do
case "$1" in
-b)
echo $2
;;
-c|--create)
c_args=()
# start looping from this flag
for arg in ${#:2}; do
[ "${arg:0:1}" == "-" ] && break
c_args+=("$arg")
done
echo_args "${c_args[#]}"
;;
*)
echo "huh?"
;;
esac
shift 1
done
The getopts utility shall retrieve options and option-arguments from a list of parameters.
$ cat script.sh
cflag=
bflag=
while getopts c:b: name
do
case $name in
b) bflag=1
bval="$OPTARG";;
c) cflag=1
cval="$OPTARG";;
?) printf "Usage: %s: [-c value] [-b value] args\n" $0
exit 2;;
esac
done
if [ ! -z "$bflag" ]; then
printf 'Option -b "%s" specified\n' "$bval"
fi
if [ ! -z "$cflag" ]; then
printf 'Option -c "%s" specified\n' "$cval"
fi
shift $(($OPTIND - 1))
printf "Remaining arguments are: %s\n" "$*"
Note the Guideline 8:
When multiple option-arguments are specified to follow a single option, they should be presented as a single argument, using commas within that argument or <blank>s within that argument to separate them.
$ ./script.sh -c "opt1 opt2 opt3" -b foo
Option -b "foo" specified
Option -c "opt1 opt2 opt3" specified
Remaining arguments are:
The standard links are listed below:
getopts - parse utility options
Section 12.2 Utility Syntax Guidelines
I noticed in the comments that you don't want to use any of these. What you could do is set all of the arguments as a string, then sort them using a loop, pulling out the ones you want to set as switched and sorting them using if statements. It is a little brutish, but it can be done.
#!/bin/bash
#set all of the arguments as a variable
ARGUMENTS=$#
# Look at each argument and determine what to do with it.
for i in $ARGUMENTS; do
# If the previous loop was -b then grab the value of this argument
if [[ "$bgrab" == "1" ]]; then
#adds the value of -b to the b string
bval="$bval $i"
bgrab="0"
else
# If this argument is -b, prepare to grab the next argument and assign it
if [[ "$i" == "-b" ]]; then
bgrab="1"
else
#Collect the remaining arguments into one list per your example
RemainingArgs="$RemainingArgs $i"
fi
fi
done
echo "Arguments: $RemainingArgs"
echo "B Value: $bval"
I use something similar in a lot of my scripts because there are a significant amount of arguments that can be fed into some of them, and the script needs to look at each one to figure out what to do. They can be out of order or not exist at all and the code still has to work.

Best way to parse arguments in bash script

So I've been reading around about getopts, getopt, etc. but I haven't found an exact solution to my problem.
The basic idea of the usage of my script is:
./program [-u] [-s] [-d] <TEXT>
Except TEXT is not required if -d is passed. Note that TEXT is usually a paragraph of text.
My main problem is that once getopts finishing parsing the flags, I have no way of knowing the position of the TEXT parameter. I could just assume that TEXT is the last argument, however, if a user messes up and does something like:
./program -u "sentence 1" "sentence 2"
then the program will not realize that the usage is incorrect.
The closest I've come is using getopt and IFS by doing
ARGS=$(getopt usd: $*)
IFS=' ' read -a array <<< "$ARGS"
The only problem is that TEXT might be a long paragraph of text and this method splits every word of text because of the spaces.
I'm thinking my best bet is to use a regular expression to ensure the usage is correctly formed and then extract the arguments with getopts, but it would be nice if there was a simpler solution
It's quite simple with getopts:
#!/bin/bash
u_set=0
s_set=0
d_set=0
while getopts usd OPT; do
case "$OPT" in
u) u_set=1;;
s) s_set=1;;
d) d_set=1;;
*) # getopts produces error
exit 1;;
esac
done
if ((!d_set && OPTIND>$#)); then
echo You must provide text or use -d >>/dev/stderr
exit 1
fi
# The easiest way to get rid of the processed options:
shift $((OPTIND-1))
# This will run all of the remaining arguments together with spaces between them:
TEXT="$*"
This is what I typically do:
local badflag=""
local aflag=""
local bflag=""
local cflag=""
local dflag=""
while [[ "$1" == -* ]]; do
case $1 in
-a)
aflag="-a"
;;
-b)
bflag="-b"
;;
-c)
cflag="-c"
;;
-d)
dflag="-d"
;;
*)
badflag=$1
;;
esac
shift
done
if [ "$badflag" != "" ]; do
echo "ERROR CONDITION"
fi
if [ "$1" == "" ] && [ "$dflag" == "" ]; do
echo "ERROR CONDITION"
fi
local remaining_text=$#

Command line argument validation library for Bash

I am looking for a reusable code snippet that does command line argument validation for bash.
Ideally something akin to the functionality offered by Apache Commons CLI:
Commons CLI supports different types of options:
POSIX like options (ie. tar -zxvf foo.tar.gz)
GNU like long options (ie. du --human-readable --max-depth=1)
Short options with value attached (ie. gcc -O2 foo.c)
long options with single hyphen (ie. ant -projecthelp)
...
and it generates a "usage" message for the program automatically, like this:
usage: ls
-A,--almost-all do not list implied . and ..
-a,--all do not hide entries starting with .
-B,--ignore-backups do not list implied entried ending with ~
-b,--escape print octal escapes for nongraphic characters
--block-size <SIZE> use SIZE-byte blocks
-c with -lt: sort by, and show, ctime (time of last
modification of file status information) with
-l:show ctime and sort by name otherwise: sort
by ctime
-C list entries by columns
I would include this code snippet at the beginning of my Bash scripts and reuse it across scripts.
There must be something like this. I don't believe we are all writing code to this effect or similar:
#!/bin/bash
NUMBER_OF_REQUIRED_COMMAND_LINE_ARGUMENTS=3
number_of_supplied_command_line_arguments=$#
function show_command_usage() {
echo usage:
(...)
}
if (( number_of_supplied_command_line_arguments < NUMBER_OF_REQUIRED_COMMAND_LINE_ARGUMENTS )); then
show_command_usage
exit
fi
...
This is the solution I use (found it on the net somewhere, probably here itself, don't remember for sure). Please note that the GNU getopt (/usr/bin/getopt) does support single dash long options (ant -projecthelp style) using the option -a, however I haven't used it so it is not shown in the example.
This code parses for 3 options: --host value or -h value, --port value or -p value and --table value or -t value. In case the required parameter isn't set, a test for it is
# Get and parse options using /usr/bin/getopt
OPTIONS=$(getopt -o h:p:t: --long host:,port:,table: -n "$0" -- "$#")
# Note the quotes around `$OPTIONS': they are essential for handling spaces in
# option values!
eval set -- "$OPTIONS"
while true ; do
case "$1" in
-h|--host) HOST=$2 ; shift 2 ;;
-t|--table)TABLE=$2 ; shift 2 ;;
-p|--port)
case "$2" in
"") PORT=1313; shift 2 ;;
*) PORT=$2; shift 2 ;;
esac;;
--) shift ; break ;;
*) echo "Internal error!" ; exit 1 ;;
esac
done
if [[ -z "$HOST" ]] || [[-z "$TABLE" ]] || [[ -z "$PORT" ]] ; then
usage()
exit
if
An alternative implementation using the getopts shell builtin(this only supports small options):
while getopts ":h:p:t:" option; do
case "$option" in
h) HOST=$OPTARG ;;
p) PORT=$OPTARG ;;
t) TABLE=$OPTARG ;;
*) usage(); exit 1 ;;
esac
done
if [[ -z "$HOST" ]] || [[-z "$TABLE" ]] || [[ -z "$PORT" ]] ; then
usage()
exit
if
shift $((OPTIND - 1))
Further reading for GNU getopt and getopts bash builtin

bash: store all command-line args after all parameters

how I can create this small script?
For example:
~$ script.sh -b my small string... other things -a other string -c any other string ant etc
I want only string, every have a mode.
-b
my small string... other things
-a
other string
-c
any other string ant etc
Anyone know how implements it?
Thanks
Here's a very simple command-line argument loop. The command-line arguments are $1, $2, etc., and the number of command-line arguments is $#. The shift command discards the arguments after we're done with them.
#!/bin/bash
while [[ $# -gt 0 ]]; do
case "$1" in
-a) echo "option $1, argument: $2"; shift 2;;
-b) echo "option $1, argument: $2"; shift 2;;
-c) echo "option $1, argument: $2"; shift 2;;
-*) echo "unknown option: $1"; shift;;
*) echo "$1"; shift;;
esac
done
UNIX commands normally expect you to quote multi-word arguments yourself so they show up as single arguments. Usage would look like:
~$ script.sh -b 'my small string... other things' -a 'other string' -c 'any other string ant etc'
option -b, argument: my small string... other things
option -a, argument: other string
option -c, argument: any other string ant etc
Notice how I've quoted the long arguments.
I don't recommend it, but if you really want to pass in multiple words on the command-line but treat them as single arguments, you'll need something a little more complicated:
#!/bin/bash
while [[ $# -gt 0 ]]; do
case "$1" in
-a) echo "option: $1"; shift;;
-b) echo "option: $1"; shift;;
-c) echo "option: $1"; shift;;
-*) echo "unknown option: $1"; shift;;
*) # Concatenate arguments until we find the next `-x' option.
OTHER=()
while [[ $# -gt 0 && ! ( $1 =~ ^- ) ]]; do
OTHER+=("$1")
shift
done
echo "${OTHER[#]}"
esac
done
Example usage:
~$ script.sh -b my small string... other things -a other string -c any other string ant etc
option: -b
my small string... other things
option: -a
other string
option: -c
any other string ant etc
Again, though, this usage is not recommended. It goes against UNIX norms and conventions to concatenate arguments like this.
I looked into doing this with getopt, but I don't think it's capable; it's very unusual to treat an unquoted spaced string as one argument. I think you're going to have to do it manually; for example:
long_str=""
for i; do
if [ ${i:0:1} = '-' ]; then
[ -z "$long_str" ] || echo ${long_str:1}
long_str=""
echo $i
else
long_str="$long_str $i"
fi
done
[ -z "$long_str" ] || echo ${long_str:1}
You should look into quoting the parameters you pass to the script:
For example:
Exhibit A:
script.sh -a one string here -b another string here
Exhibit B:
script.sh -a "one string here" -b "another string here"
and script.sh:
echo "$1:$2:$3:$4"
With exhibit A, the script will display: -a:one:string:here
With exhibit B, the script will display: -a:one string here:-b:another string here
I used the colon to separate things, to make it more obvious.
In Bash, if you quote the parameters you inhibit tokenization of the string, forcing your space separated string to be just one token, instead of many.
As a side note, you should quote each and every variable you use in Bash, just for the case where its value contains token separators (spaces, tabs, etc.), because "$var" and $var are two different things, especially if var="a string with spaces".
Why? Because at one point you'll probably want something like this:
script.sh -a "a string with -b in it" -b "another string, with -a in it"
And if you don't use quoted parameters, but rather attemp heuristics to find where the next parameter is, your code will brake when it hits the fake -a and -b tokens.

Resources