I have this weird behaviour that I can't understand; at some point I need to pass some flags to my script, one flag in particular is supposed to carry out a series of options used inside my script, for example I'm invoking my script as
sh script.sh --flag1="-options1=value1 -options2=value2" --flag2
the result is
-options1=value1
-options2=value2
1
,and so flag1 magically appears as a multiline declaration and something happens and I don't really get the logic behind this behaviour.
This is the complete script
parse()
{
while [ $# -gt 0 ]
do
case "$1" in
--flag1=* ) FLAG_1="${1#*=}"; shift;;
--flag2 ) FLAG_2="1"; shift;;
(*) printf $0' : error - unrecognized option '$1'\n' 1>&2; exit 1;;
esac
done
}
printvar()
{
printf %s'\n' $FLAG_1
printf %s'\n' $FLAG_2
}
parse "$#"
printvar
What I'm doing wrong here ?
To get FLAG_1 as a single line, just quote the variable:
printf "%s\n" "$FLAG_1"
The below snippet should clarify:
$ printf "%s\n" ab cd
ab
cd
$ printf "%s\n" "ab cd"
ab cd
Related
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.
I'm trying to pass an argument as "verbatim" to my script, the problem is that the argument in inside double quotes "" and it starts with a double dash/double hyphen --.
This is an example
script.sh -f "--conf=bla"
In my script both $* and $# transform this into
-f --conf=bla
and when this thing reaches getopts there is no way to decode this information the way it should be.
And by the way this is my getopts
foo()
{
while getopts ":f:" vars
do
case ${vars} in
f ) MYVAR=${OPTARG};;
* ) Err; exit 1;;
esac
done
shift $((OPTIND-1))
}
I would like to store --conf=bla inside MYVAR, I can't find a way to do this, apparently I can't control the way the double quotes are stripped away and, in general, I can't really pass text as verbatim to my script.
How I can control this ?
$ cat t.sh
#!/bin/sh
foo()
{
# unset OPTIND
while getopts ":f:" vars
do
case ${vars} in
f) MYVAR=${OPTARG} ;;
*) echo "error"; exit 1 ;;
esac
done
shift $((OPTIND-1))
echo "\$MYVAR=[${MYVAR}]"
}
foo "$#"
.
$ ./t.sh -f "--conf=blah"
$MYVAR=[--conf=blah]
Please elaborate?
I wrote a bash script that takes a command as the first positional parameter and uses a case construct as a dispatch similar to the following:
do_command() {
# responds to invocation `$0 command ...`
}
do_copy() {
# respond to invocation: `$0 copy...`
}
do_imperative() {
# respond to invocation: `$0 imperative ...`
}
cmd=$1
shift
case $cmd in
command)
do_command $*
;;
copy)
do_copy $*
;;
imperative)
do_imperative $*
;;
*)
echo "Usage: $0 [ command | copy | imperative ]" >&2
;;
esac
This script decides what function to call based on $1 and then passes the remaining arguments to that function. I would like to add the ability dispatch on distinct partial matches, but I want to do it in an elegant way (elegant defined as a way that is both easy to read and is not so verbose as to be an eyesore or a distraction).
The obvious functioning (but not elegant) solution might be something like this:
case $cmd in
command|comman|comma|comm|com)
do_command $*
;;
copy|cop)
do_copy $*
;;
imperative|imperativ|imperati|imperat|impera|imper|impe|imp|im|i)
do_imperative $*
;;
*)
echo "Usage: $0 [ command | copy | imperative ]" >&2
;;
esac
As you can see, explicitly enumerating all distinct permutations of each command name can get really messy.
For a moment, I thought it might be alright to use a wildcard match like this:
case $cmd in
com*)
do_command $*
;;
cop*)
do_copy $*
;;
i*)
do_imperative $*
;;
*)
echo "Usage: $0 [ command | copy | imperative ]" >&2
;;
esac
This is less of an eyesore. However, this could result in undesirable behavior such as where do_command is called when $1 is given as "comblah" or something else that shouldn't be recognized as a valid argument.
My question is: What is the most elegant (as defined above) way to correctly dispatch such a command where the user can provide any distinct truncated form of the expected commands?
I came up with the following solution, which should work with any
bourne-compatible shell:
disambiguate() {
option="$1"
shift
found=""
all=""
comma=""
for candidate in "$#"; do
case "$candidate" in
"$option"*)
found="$candidate"
all="$all$comma$candidate"
comma=", "
esac
done
if [ -z "$found" ] ; then
echo "Unknown option $option: should be one of $#" >&2
return 1;
fi
if [ "$all" = "$found" ] ; then
echo "$found"
else
echo "Ambigious option $option: may be $all" >&2
return 1
fi
}
foo=$(disambiguate "$1" lorem ipsum dolor dollar)
if [ -z "$foo" ] ; then exit 1; fi
echo "$foo"
Yes, the source code of disambiguate is not pretty, but I hope you
won't have to look at this code most of the time.
Pattern Matching for Command Dispatch in Bash
It seems that a few of you like the idea of using a resolver to find the full command match prior to the dispatching logic. That's might just be the best way to go for large command sets or sets that have long words. I put together the following hacked up mess--it makes 2 passes using built-in parameter expansion substring removal. I seems to work well enough and it keeps the dispatch logic clean of the distraction of resolving partial commands. My bash version is 4.1.5.
#!/bin/bash
resolve_cmd() {
local given=$1
shift
local list=($*)
local inv=(${list[*]##${given}*})
local OIFS=$IFS; IFS='|'; local pat="${inv[*]}"; IFS=$OIFS
shopt -s extglob
echo "${list[*]##+($pat)}"
shopt -u extglob
}
valid_cmds="start stop status command copy imperative empathy emperor"
m=($(resolve_cmd $1 $valid_cmds))
if [ ${#m[*]} -gt 1 ]; then
echo "$1 is ambiguous, possible matches: ${m[*]}" >&2
exit 1
elif [ ${#m[*]} -lt 1 ]; then
echo "$1 is not a recognized command." >&2
exit 1
fi
echo "Matched command: $m"
Update 1:
match is called using (partial) command as first positional parameter, followed by strings to test against. On multiple matches, each partial match will be hinted with uppercase.
# #pos 1 string
# #pos 2+ strings to compare against
# #ret true on one match, false on none|disambiguate match
match() {
local w input="${1,,}" disa=();
local len=${#input}; # needed for uppercase hints
shift;
for w in $*; do
[[ "$input" == "$w" ]] && return 0;
[[ "$w" == "$input"* ]] && disa+=($w);
done
if ! (( ${#disa[*]} == 1 )); then
printf "Matches: "
for w in ${disa[*]}; do
printf "$( echo "${w:0:$len}" | tr '[:lower:]' '[:upper:]')${w:${len}} ";
done
echo "";
return 1;
fi
return 0;
}
Example of usage. match can be tweaked to print/return the whole non disambiguate command, otherwise do_something_with would require some logic to resolve partial commands. (something like my first answer)
cmds="start stop status command copy imperative empathy emperor"
while true; do
read -p"> " cmd
test -z "$cmd" && exit 1;
match $cmd $cmds && do_something_with "$cmd";
done
First answer: Case approach; would require some logic before use, to solve disambiguate partial matches.
#!/bin/bash
# script.sh
# set extended globbing, in most shells it's not set by default
shopt -s extglob;
do_imperative() {
echo $*;
}
case $1 in
i?(m?(p?(e?(r?(a?(t?(i?(v?(e))))))))))
shift;
do_imperative $*;
;;
*)
echo "Error: no match on $1";
exit 1;
;;
esac
exit 0;
i, im, imp up till imperative will match. The shift will set second positional parameter as first, meaning; if the script is called as:
./script.sh imp say hello
will resolve into
do_imperative say hello
If you wish to further resolve short hand commands, use the same approach within the functions as well.
Here's a simple (possibly even elegant) solution which will only work with bash, because it relies on the bash-specific compgen command.
This version assumes that the action functions are always called do_X where X is the command name. Before calling this function, you need to set $commands to a space-separated list of legal commands; the assumption is that legal commands will be simple words, since function names cannot include special characters.
doit () {
# Do nothing if there is no command
if (( ! $# )); then return 0; fi;
local cmd=$1
shift
local -a options=($(compgen -W "$commands" "$cmd"));
case ${#options[#]} in
0)
printf "Unrecognized command '%b'\n" "$cmd" >> /dev/stderr;
return 1
;;
1)
# Assume that the action functions all have a consistent name
"do_$options" "$#"
;;
*)
printf "Ambigous command '%b'. Possible completions:" "$cmd";
printf " %s" "${options[#]}";
printf "\n";
return 1
;;
esac
}
do_command () { printf "command %s\n" "$#"; }
do_copy () { printf "copy %s\n" "$#"; }
do_imperative () { printf "imperative %s\n" "$#"; }
commands="command copy imperative"
Trial run:
$ doit cop a "b c"
copy a
copy b c
$ doit comfoo a "b c"
Unrecognized command 'comfoo'
$ doit co a "b c"
Ambigous command 'co'. Possible completions: command copy
$ doit i a "b c"
If you were confident that there were no stray do_X variables available, you could use compgen to make the list of commands as well:
command=$(compgen -Afunction do_ | cut -c4-)
Alternatively, you could use the same system to make a resolver, and then handle the returned option with a normal case statement:
# resolve cmd possible-commands
resolve () {
# Fail silently if there is no command
if [[ -z $1 ]]; then return 1; fi;
local cmd=$1
shift
local commands="$*"
local -a options=($(compgen -W "$commands" "$cmd"));
case ${#options[#]} in
0)
printf "Unrecognized command '%b'\n" "$cmd" >> /dev/stderr;
return 1
;;
1)
echo $options
return 0
;;
*)
printf "Ambigous command '%b'. Possible completions:" "$cmd";
printf " %s" "${options[#]}";
printf "\n";
return 1
;;
esac
}
$ resolve com command copy imperative && echo OK
command
OK
$ resolve co command copy imperative && echo OK
Ambigous command 'co'. Possible completions: command copy
$ resolve copx command copy imperative && echo OK
Unrecognized command 'copx'
The intent would be to write something like:
cmd=$(resolve "$1" "$commands") || exit 1
case "$cmd" in
command)
# ...
Is there a way to feed non positional arguments to a shell script?
Meaning explicitly specify some kind of flag?
. myscript.sh value1 value2
. myscript.sh -val1=value1 -val2=value2
You can use getopts, but I don't like it because it's complicated to use and it doesn't support long option names (not the POSIX version anyway).
I recommend against using environment variables. There's just too much risk of name collision. For example, if your script reacts differently depending on the value of the ARCH environment variable, and it executes another script that (unbeknownst to you) also reacts to the ARCH environment variable, then you probably have a hard-to-find bug that only shows up occasionally.
This is the pattern I use:
#!/bin/sh
usage() {
cat <<EOF
Usage: $0 [options] [--] [file...]
Arguments:
-h, --help
Display this usage message and exit.
-f <val>, --foo <val>, --foo=<val>
Documentation goes here.
-b <val>, --bar <val>, --bar=<val>
Documentation goes here.
--
Treat the remaining arguments as file names. Useful if the first
file name might begin with '-'.
file...
Optional list of file names. If the first file name in the list
begins with '-', it will be treated as an option unless it comes
after the '--' option.
EOF
}
# handy logging and error handling functions
log() { printf '%s\n' "$*"; }
error() { log "ERROR: $*" >&2; }
fatal() { error "$*"; exit 1; }
usage_fatal() { error "$*"; usage >&2; exit 1; }
# parse options
foo="foo default value goes here"
bar="bar default value goes here"
while [ "$#" -gt 0 ]; do
arg=$1
case $1 in
# convert "--opt=the value" to --opt "the value".
# the quotes around the equals sign is to work around a
# bug in emacs' syntax parsing
--*'='*) shift; set -- "${arg%%=*}" "${arg#*=}" "$#"; continue;;
-f|--foo) shift; foo=$1;;
-b|--bar) shift; bar=$1;;
-h|--help) usage; exit 0;;
--) shift; break;;
-*) usage_fatal "unknown option: '$1'";;
*) break;; # reached the list of file names
esac
shift || usage_fatal "option '${arg}' requires a value"
done
# arguments are now the file names
The easiest thing to do is pass them as environment variables:
$ val1=value1 val2=value2 ./myscript.sh
This doesn't work with csh variants, but you can use env if you are using such a shell.
Yes there is. The name is getopts http://www.mkssoftware.com/docs/man1/getopts.1.asp
Example:
#!/bin/bash
while getopts d:x arg
do
case "$arg" in
d) darg="$OPTARG";;
x) xflag=1;;
?) echo >&2 "Usage: $0 [-x] [-d darg] files ..."; exit 1;;
esac
done
shift $(( $OPTIND-1 ))
for file
do
echo =$file=
done
Script has arguments as follows:
- $0 - script name
- $1, $2, $3.... - received arguments
$* = all arguments,
$# = number of arguments
Reference:
http://famulatus.com/ks/os/solaris/item/203-arguments-in-sh-scripts.html
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.