Unexpected if statement behaviour - bash

I was running a small bash script, but I couldn't figure out why it was entering a if block even when condition should be false.
$ cat script.sh
#!/bin/bash
if [[ "$#"="-h" || "$#"="--help" ]]
then
echo 'Show help'
exit
fi
echo 'Do stuff'
$ ./script.sh
Show help
$ bash -x script.sh
+ [[ -n =-h ]]
+ echo 'Show help'
Show help
+ exit
$ bash -x script.sh -option
+ [[ -n -option=-h ]]
+ echo 'Show help'
Show help
+ exit
So why is $# equal to -n when I didn't pass any arguments? Also even if it is, how does -n =-h evaluate to true? When I do pass an argument -option, why is it evaluated to true, too?

Whitespace is significant. Spaces between the arguments to [[ are mandatory.
if [[ "$#" = "-h" || "$#" = "--help" ]]
Also, "$#" means "all of the command-line arguments". It would be better to just check a single argument.
if [[ "$1" = "-h" || "$1" = "--help" ]]
And for what it's worth, variable expansions in [[ don't have to be quoted. It doesn't hurt, and quoting your variables actually a good habit to develop, but if you want you can remove the quotes.
if [[ $1 = -h || $1 = --help ]]

[[ string ]] return true if string is not empty, i.e. it's a shorcut for
[[ -n string ]]
In your case, the string was =-h, that's why you see
[[ -n =-h ]]
To test for string equiality, you have to use the = (or ==) operator, that must be preceded and followed by whitespace.
[[ "$#" = "-h" ]]
Note that "$#" means all the arguments:
set -- a b c
set -x
[[ "$#" == 'a b c' ]] && echo true
gives
+ [[ a b c == \a\ \b\ \c ]]
+ echo true
true

The other answers have already explained the problems with your code. This one shows that
bashisms such as [[ ... ]] are not needed,
you can gain flexibility by using a for loop to check whether at least one of the command-line argument matches -h or --help.
Script
#!/bin/sh
show_help=0
for arg in "$#"; do
shift
case "$arg" in
"--help")
show_help=1
;;
"-h")
show_help=1
;;
*)
;;
esac
done
if [ $show_help -eq 1 ]; then
printf "Show help\n"
exit
fi
Tests
After making the script (called "foo") executable, by running
chmod u+x foo
I get the following results
$ ./foo
$ ./foo -h
Show help
$ ./foo --help
Show help
$ ./foo bar
$ ./foo bar --help
Show help
$ ./foo bar --help baz -h
Show help

Related

How to search within a range of bash command-line parameters

Let's say I have the program test.sh, which has parameters, some optional and some not.
For example:
./test.sh --foo /path/to/file --baz --bar1 --bar2 --bar3
where foo and baz, as well as the path, are necessary and the bars are optional parameters.
Now, I want to be able to make the everything after the path order-insensitive.
I could use
if [[ "$3" == "--baz" ]] || [[ "$4" == "--baz" ]] || ... || [[ "${(n-1)}" == "--baz" ]] || [[ "${n}" == "--baz" ]]
but that's slow and messy, even for me.
Ideally I would have something along the lines of this:
if [[ ${n > 2} == "--baz" ]]; then
Violating standard utility syntax guidelines like this makes your program unpredictable and difficult to use, but you can still do so e.g. with a simple utility function:
hasArgAfter() {
n="$1"
word="$2"
shift "$((n+2))"
for arg
do
[ "$arg" = "$word" ] && return 0
done
return 1
}
if hasArgAfter 2 "--baz" "$#"
then
echo "Some argument strictly after #2 is --baz"
fi
Based on the answers in:
Check if a Bash array contains a value
How to slice an array in Bash
You could come up with the following answer:
[[ " ${#:3} " =~ " --baz " ]] && echo yes || echo no
This might fail if you have something like
command --foo /path/to/file --flag1 option1 --flag2 "foo --baz bar" --flag3
where foo --baz bar is an option to --flag2
Another way, a bit safer, would be:
for arg in "${#:3}"; do
[[ "${arg}" != "--baz" ]] && continue
# perform action here if --baz is found
set-of-commands
# need this line for early return
break
done
Create a function for handling inputs, then ordering won't matter.
What I would do is define global variables for your script using the typset command at the top of your script. Then I would handle the user input options using a function or just code it without a function. This way when input is not in order or it is missing it is properly handled.
The example below is using a case statement and it is using the build in shift option to go through all of the inputs. $1 is --option $2 is the value such as "/path/to/something", I have checks in there check if $2 is empty "-z" or || if it isn't, set it. When done items are either set or empty. In your code you will check if set or empty to determine if you are going to use that variable (not shown here.
# -- create globals --
typeset fooPath
typeset baz
typeset bar1
# -- get required commandline input --
get_user_input_options() {
while [[ $# -gt 0 ]] ;do
key="$1"
case ${key,,} in
--foo|--foo-path)
fooPath="${2}"
shift
;;
-b|--baz)
[[ -z "${2}" ]] || baz="${2}"
shift
;;
--bar1)
[[ -z "${2}" ]] || bar1="${2}"
shift
;;
*) echo "ERROR: Unknown option $key given."
exit 9
;;
esac
shift
done
}
# -- get inputs first in script logic --
get_user_input_options "$#"
echo $fooPath
echo $baz
echo $bar1
Example outputs:
[centos#ip-172-31-22-252 ~]$ ./t.sh --foo "/some/thing" --baz askdh
/some/thing
askdh
[centos#ip-172-31-22-252 ~]$ ./t.sh --foo "/some/thing" --baz askdh --bar1
/some/thing
askdh
[centos#ip-172-31-22-252 ~]$ ./t.sh --foo "/some/thing" --baz askdh --bar1 test
/some/thing
askdh
test
[centos#ip-172-31-22-252 ~]$ ./t.sh --foo "/some/thing" --baz askdh --bar1 test --notvalid askdha
ERROR: Unknown option --notvalid given.

Bash - build URL query string by parsing script command arguments

I don't use Bash very frequently but I need to work on a bit of Bash that has to make a curl request to a web service with a query string tail built from command arguments contained in the script command arguments variable $#. So if the command argument string is something like -e something -v -s somethingelse, then the query string that must be produced is e=something&v=blank&s=somethingelse.
At the moment my test script looks like this:
#!/bin/bash
set -e
echo "${#}"
query_string=""
for arg in "${#}" ; do
if [[ "${arg}" =~ "-" ]] ; then
query_string+="${arg}="
else
if [ -z "${arg}" ] ; then
query_string+="blank&"
else
query_string+="${arg}&"
fi
fi
done
echo "${query_string}" | tr -d '-'
and produces the incorrect output
e=something&v=s=somethingelse&
I'm not sure where I'm going wrong. Any suggestions would be appreciated.
How about:
#!/bin/bash
set -e
echo "${#}"
option=true
query_string=""
for arg in "${#}" ; do
if $option ; then
query_string+="${arg}="
option=false
else
if [[ "${arg}" =~ "-" ]] ; then
query_string+="blank&${arg}="
else
query_string+="${arg}&"
option=true
fi
fi
done
echo "${query_string::-1}" | tr -d '-'
Every iteration you have to check the previous arg to see if it was a switch, not a value:
#!/bin/bash
set -e
echo "${#}"
prev_arg=""
query_string=""
for arg in "${#}" ; do
if [[ $arg == -* ]] ; then
# Current arg is a switch and the previous one was also a switch
# which means the value for it is blank
if [[ $prev_arg == -* ]] ; then
query_string+="blank&"
fi
query_string+="${arg}="
else
query_string+="${arg}&"
fi
prev_arg="$arg"
done
echo "${query_string::-1}" | tr -d '-'
This produces the following output:
something -v -s somethingelse
e=something&v=blank&s=somethingelse

How to check if arguments are legit?

I have to write a bash script which will count all the commands in a text file. Arguments to a script are -p, -n num, and a file. This means that commands like:
script.sh -n 3 -p file.txt
script -p -n 3 file.txt
and similar are all legit.
However, I have to echo an error for any commands that are not similar to this: script.sh -n -k file.txt for example.
Here is a link to my code.
I managed to make it work, but it is way too long and redundant. Is there a way I can do this in a short way?
You may want to have a look at one of the following standard commands:
getopts is a Bash builtin. It is newer and simple to use, but does not support long options (--option).
getopt is an external program which may involve a little more glue code. There are different implementations. getopt usually supports long options.
This is a small getopts example (modified one of the examples from this external site):
#!/bin/bash
flag=off
dir=
# iterate over each option with getopts:
while getopts fd: opt
do
case "$opt" in
f) flag=on;;
d) dir="$OPTARG";;
*) echo >&2 "usage: $0 [-f] [-d directory] [file ...]"
exit 1;;
esac
done
# remove all positional pararmeters we already
# handled from the command line:
shift $(( expr $OPTIND - 1 ))
# main part of your program, remaining arguments are now in
# $# resp. $0, $1, ...
I'd like to suggest another snippet that is a lot simpler to read than yours, because it exactly depicts the only two valid cases you specified in your comment:
If I want to "call" my script it has to look like this: script.sh -n +number -p file.txt. file.txt must be the last argument, however, -n and -p can be switched.
So the cases are ($0 to $4):
script.sh -n +number -p file.txt
script.sh -p -n +number file.txt
It uses only if and Bash's logical operators:
#!/bin/bash
if ! { [[ "$1" = "-n" ]] && [[ "$2" =~ ^-[0-9]+$ ]] && [[ "$3" = "-p" ]] && [[ "$4" =~ ".txt"$ ]] ; } &&
! { [[ "$2" = "-n" ]] && [[ "$3" =~ ^-[0-9]+$ ]] && [[ "$1" = "-p" ]] && [[ "$4" =~ ".txt"$ ]] ; }
then
echo "Error" && exit 1
fi
Notes:
The group ({, }) syntax expects a ; at the end of its list.
You have to use a regex to check for *.txt
The number regex you gave will require the number to start with a -, while in your specification you say +.

BASH: Why `printf` returns 1?

I have a problem that gaslights me.
Here comes my bash script "foo" reduced to the problem:
#!/bin/bash
function Args()
{
[[ "$1" == "-n" ]] && [[ -d "$2" ]] && printf "%s\n" "new ${3}"
[[ "$1" == "-p" ]] && [[ -d "$2" ]] && printf "%s\n" "page ${3}"
}
[[ $# -eq 3 ]] && Args "$#"
echo $?
Now when I execute this code, the following happens:
$ ./foo -n / bar
new bar
1
This, however, works:
$ ./foo -p / bar
page bar
0
Please, can anybody explain?
Sorry if this is a known "thing" and my googleing skills must be improved...
It is returning 1 in first case only because 2nd condition:
[[ "$1" == "-p" ]] && [[ -d "$2" ]] && printf "%s\n" "page ${3}"
won't match/apply when you call your script as:
./foo -n / bar
And due to non-matching of 2nd set of conditions it will return 1 to you since $? represents most recent command's exit status which is actually exit status of 2nd set of conditions.
When you call your script as:
./foo -p / bar
It returns status 0 to you since 2nd line gets executed and that is also the most recently executed one.

bash - Possible to 'override' the test ([[)-builtin?

Is it possible to override Bash's test builtin? So that
[[ $1 = 'a' ]]
not just does the test but also outputs which result was expected when it fails? Something like
echo "Expected $1 to be a.'
EDIT
I know this is bad :-).
The test expression compound command does real short-circuiting that affects all expansions.
$ set -x
$ [[ 0 -gt x=1+1 || ++x -eq $(tee /dev/fd/3 <<<$x) && $(echo 'nope' >&3) ]] 3>&1
+ [[ 0 -gt x=1+1 ]]
++ tee /dev/fd/2
2
+ [[ ++x -eq 2 ]]
So yes you could do anything in a single test expression. In reality it's quite rare to have a test produce a side-effect, and almost never used to produce output.
Also yes, reserved words can be overridden. Bash is more lenient with ksh-style function definitions than POSIX style (which still allows some invalid names).
function [[ { [ "${#:1:${##}-1}" ]; }; \[[ -a -o -a -o -a ]] || echo lulz
Yet another forky bomb.
if function function if function if if \function & then \if & fi && \if & then \function & fi && then \function fi
Something like this?
if [[ $1 == 'a' ]]; then
echo "all right";
else
echo 'Expected $1 to be "a"'
fi
Anyway, what's the point of the test if you only expect one answer? Or do you mean that for debugging purposes?
[[ 'a' = 'a' ]] || echo "failed"
[[ 'b' = 'a' ]] || echo "failed"
failed

Resources