bash multiple expressions in an if statement with "$#" - bash

I want to make an exclusive or in bash. When I define variables a and b, I can use this:
> a=1
> b=
> [ -z "$b" -a "$a" -o "$b" -a -z "$a" ] && echo yes
yes
However, when I try the same construction with the shell variables I'm actually interested in, it fails:
> [ -z "$BASH_ARGV" -a "$#" -o "$BASH_ARGV" -a -z "$#" ] && echo sourced
bash: [: argument expected
What's going on?

"$#" expands to one "word" per script argument, which means that it will expand to nothing if there are no script arguments. On the other hand, "$BASH_ARGV" expands to the first value in the array $BASH_ARGV if that array exists and otherwise empty. So I'm not sure what you actually want to compare.
But the main point is that "$#" expands to zero or more words. If it doesn't expand to a single word, then your expression is going to be syntactically incorrect. (Probably. It's possible that the entire set of arguments happens to be a valid expression.)
If you wanted to concatenate all the arguments together, you should use "$*". But that's still not comparable to "$BASH_ARGV". It's not really comparable to "${BASH_ARGV[*]}" either, but the latter would be somewhat more similar.
You might want to check out BASH_ARGC, too.

"$#" expands to individual words. You want either "$*" or $# -gt 0
With that "or" in the middle, you probably want:
[ -z "$BASH_ARGV" -a "$*" ] || [ "$BASH_ARGV" -a -z "$*" ]
Or, use the bash-specific
[[ (-z "$BASH_ARGV" -a "$*") || ("$BASH_ARGV" -a -z "$*") ]]
Nevermind: -a has higher priority than -o

Related

Why do you need quotes here in Bash?

if [ "$a" -gt "$b" ]; then
...
fi
Why can't I have something like:
if [ $a -gt $b ]; then
...
fi
for Bash?
With arithmetic operator -gt (greater-than) $a and $b must contain integers and no quotes are necessary.
You need quotes here for correction -- for example, to avoid code injection. Let's say you have that script unquoted and you let me input numbers to tell me which one is greater.
I input:
a="1"
b="1 -o -e /home/foo"
Since you didn't quote, you're now going to evaluate
[ 1 -gt 1 -o -e /home/foo ]
and tell me that b is greater if /home/foo exists.
Your number comparison script has suddenly become a simplistic file browser that I can use to examine your system to find active users, running processes, installed software, hardware configuration and such.
This may not matter in practice, but when it's so easy to do right, you might as well quote.
TL;DR
You have provided no sample input, but it's likely that your variables are unset, empty, or don't contain integers. To make your scripts more robust, you should always quote your variables, test for conditions like empty or unset variables, or perform other validations if you can't guarantee the contents of your variables or are consuming user input.
Invalid Expansions
Unless you've explicitly declared your variables as integers, and sometimes even when you do, there are several cases where your comparison will be invalid. For example:
$ unset a b; a=1; [ $a -gt $b ]
-bash: [: 1: unary operator expected
You have a number of options, but the easiest is to use the Bash expression operators instead of the single brackets which are actually equivalent to /bin/test. This handles empty expansions and other use cases more gracefully. For example:
# Here, *b* is unset so *a* is "greater."
$ unset a b; a=1; [[ $a -gt $b ]]; echo $?
0
# This time *a* is unset, so the expression is false.
$ unset a b; b=1; [[ $a -gt $b ]]; echo $?
1
Declaring Variables as Integers May Help
You still have to beware of unexpected casts, though. For example:
$ unset a b; a='foo'; b='1'; [[ $a -gt $b ]]; echo $?
1
$ echo $a
foo
Better is the use of declare -i to force integer casting, but that will still cause unexpected results if you don't validate your inputs. For example:
$ unset a b; declare -i a='foo' b='1'; [[ $a -gt $b ]]; echo $?
1
$ echo $a
0
By declaring your variables, you force the values to be integers, but strings will be assigned as zero. This is likely not what you're expecting.
Validations and Defensive Quoting
While overkill for simple scripts, this is what a more robust script would look like.
unset a b
a='foo'
b='1'
for var in "$a" "$b"; do
if [[ -z "$var" ]] || [[ ! "$var" =~ ^[[:digit:]]+$ ]]; then
echo "Variable invalid: ${var}" > /dev/stderr
fi
done
[[ "$a" -gt "$b" ]]
echo "$?"
Note that in this case, the quotes and braces are not strictly necessary, but are there to show good defensive programming. You can rarely go wrong in Bash by using them liberally, and they will often save you a great deal of head-scratching and debugging.

bash [: too many arguments greater than symbol

This isn't really a question (though I have one at the end), but rather a solution to a problem that I wanted to share in case it helps someone else.
For the longest time I had been getting bash: [: too many arguments when opening a new terminal (specifically iTerm2 on OS X with the bash-completion macport installed). This error originated from the line if [ -n "$BASH_VERSION" -a -n "$PS1" -a -z "$BASH_COMPLETION_COMPAT_DIR" ]; then in the file /opt/local/etc/bash_completion. I have finally tracked down the problem to the fact that I had export PS1='>' in my .bash_profile. Changing PS1 to something else (e.g. '> ') fixes the problem with bash completion.
Some experimenting in OS X and Debian reveals that this problem occurs when adding extra expressions (with -a or -o) into a test ([ ]) after the expression involving '>'. E.g.,
> A='>'; if [ -n "$A" ]; then echo "yes"; fi
yes
> A='>'; if [ -n "$A" -a -n "$A" ]; then echo "yes"; fi
bash: [: too many arguments
> A='> '; if [ -n "$A" -o -n "$A" ]; then echo "yes"; fi
yes
> A='>'; if [ -n "$A" -o -n "Hello" ]; then echo "yes"; fi
bash: [: too many arguments
> A='>'; if [ -n "Hello" -a -n "$A" ]; then echo "yes"; fi
yes
Is this a (known) bug in bash?
Your workaround is effective, as long as the string stored in $A is not an operator that [ / test recognizes - simply adding a space is sufficient, as you've discovered.
Surely the "greater than" should be interpreted as just a string? It works with '> ' after all.
No, the content of $A is not interpreted as just a string. (If you wanted that, you'd have to use [[ instead, which is parsed in a special context, more like you'd expect from traditional programming languages.)
[ (test) is a builtin (also exists as an external utility on most systems) and is therefore parsed with command syntax, which means:
the shell performs its expansions first - $A references are replaced with the content of the variable in this case.
the result is then passed to [
Thus, from the perspective of [, it doesn't matter whether or not the operator it ultimately sees - > in your example - came from a literal or was stored in a variable.
But note that whitespace matters: passing > (no spaces) is interpreted as an operator; >, by contrast, ><space> is not - because that exact literal is more than just the operator.
The bottom line is:
The bash-completion script you're using is not robust.
As #chepner states in a comment on the question, POSIX recommends not using -o / -a to avoid the ambiguity you encountered (emphasis mine):
The XSI extensions specifying the -a and -o binary primaries and the '(' and ')' operators have been marked obsolescent. (Many expressions using them are ambiguously defined by the grammar depending on the specific expressions being evaluated.)
Specifically, using separate [ ... ] expressions joined with && (instead of -a) and || (instead of -o) solves the problem:
[ -n "$BASH_VERSION" ] && [ -n "$PS1" ] && [ -z "$BASH_COMPLETION_COMPAT_DIR" ]
Or, more simply, taking advantage of a non-empty string evaluating to true:
[ "$BASH_VERSION" ] && [ "$PS1" ] && [ -z "$BASH_COMPLETION_COMPAT_DIR" ]
Note that while -a and -o introduce ambiguities, they are not a security concern - you cannot inject arbitrary code through their use.
If you want to use two or more condition you should use
if [ condition1 ] && [condition2 ]
or
if [ condition1 ] || [condition2 ]
so in your case (first if "and"):
A='>'; if [ -n "$A" ] && [ -n "$A" ]; then echo "yes"; fi
for the "or" if:
A='>'; if [ -n "$A" ] || [ -n "Hello" ]; then echo "yes"; fi
But be aware that that the second check [ -n "Hello" ] is always true, so it's better to remove it.
You may be interested in shellcheck to validate your bash script syntax.

basic shell bash checking/switch arguments

This is a beginner question, I have already checked that Check existence of input argument in a Bash shell script but it doesn't fully explain what I want to do.
gcc -Wall cx17.$1.c -o cx17.$1
if [ -z "$1" ]
then
echo "No argument supplied"
else if [ -z "$2"]
then
echo "Data file is missing!!"
else if [ -z "$3"]
then
./cx17.$1 $2 > ./cx17.$1.$2
else
./cx17.$1 $2 $3 > ./cx17.$1.$2
fi
So you understand this very basic use case, depending on arguments (if there is 1, 2 or 3) the script will perform a different task.
I know it's really simple that's why I think I'm missing something obvious.
Thanks for your help
The answered I validate gave me some errors but lead me to the right stuff:
if [ -z "$1" ]; then
echo 'No argument supplied';
elif [ -z "$2" ]; then
echo 'Data file is missing!!';
elif [ -z "$3" ]; then
./cx17.$1 $2 >./cx17.$1.$2;
else
./cx17.$1 $2 $3 >./cx17.$1.$2;
fi;
Replace else if with elif:
if [[ -z "$1" ]]; then
echo 'No argument supplied';
elif [[ -z "$2" ]]; then
echo 'Data file is missing!!';
elif [[ -z "$3" ]]; then
"./cx17.$1" "$2" >"./cx17.$1.$2";
else
"./cx17.$1" "$2" "$3" >"./cx17.$1.$2";
fi;
Other recommendations:
Always double-quote words that contain variable substitutions, otherwise word splitting and shell globbing can take effect on the expanded variable content.
Always use [[ instead of [, since the former is more powerful, and it's good to be consistent.
If interpolation is not required, use single-quotes rather than double-quotes, since single-quotes do not interpolate anything; it's just safer that way.
You can dispense with the if statement altogether using the ${var:?msg} construct, which will exit the script if the given variable doesn't have a non-null value.
: ${1:?No argument given}
: ${2:?Data file is missing!}
# $1 and $2 guaranteed to be non-null; the program
# will receive 1 or 2 arguments, depending on how many
# arguments are present in $#
./cx17."$1" "${#:2:2}" > "./cx17.$1.$2"

unset variable check in bash

I have two variables declared but unset:
__var1=
__var2=
Now I set __var2 to have some value:
__var2=1
When I try to do a check like this:
[ -z "$__var1" -a -z "$__var2" ] || echo "Both missing!"
I am getting that message Both missing!. But that's incorrect.
Why is that? And how to do a proper check, to see if both of them are missing?
And if the user wants to check if the variable is really unset and not just having an empty value, you can do:
$ A=1234
$ [[ -z ${A+.} ]] && echo "Variable is unset."
$ A=
$ [[ -z ${A+.} ]] && echo "Variable is unset."
$ unset A
$ [[ -z ${A+.} ]] && echo "Variable is unset."
Variable is unset.
In which in your case it could be
[[ -z ${__var1+.} && -z ${__var2+.} ]] && echo "Both variables are unset!"
#Dave Schweissguth's answer makes a good point about the logic of your code, but there are also things to observe about the syntax:
[Update: The original form of the question used assignments such as $__var1= - this has since been corrected] In Bourne-like/POSIX-compatible shells you do not use the $ prefix when assigning a value, only when referencing it; thus, your assignments should read:
__var1=
__var2= # or, later: __var2=1
Your question is tagged bash, so the best bash way to write your could would be:
[[ -z $__var1 && -z $__var2 ]] && echo "Both missing!"
Note the use of [[ ... ]] rater than [ ... ], which obviates the need to double-quote the operands to -z.
By contrast, the most portable (POSIX-compliant) way is:
[ -z "$__var1" ] && [ -z "$__var2" ] && echo "Both missing!"
Your code prints "Both missing!" if it's not true (||) that both (-a) variables are empty (-z). You want to print the message if that IS true. Do that like this:
[ -z "$__var1" -a -z "$__var2" ] && echo "Both missing!"
I don't recall ever seeing a version of bash or test (what sh uses to evaluate the same expressions) without -z or -a, so as far as I know the above will work on any Unix-like system you're likely to find.

Escaping in test comparisons

In the following, I would like check if a given variable name is set:
$ set hello
$ echo $1
hello
$ echo $hello
$ [[ -z \$$1 ]] && echo true || echo false
false
Since $hello is unset, I would expect the test to return true. What's wrong here? I would assume I am escaping the dollar incorrectly.
TYIA
You are testing if \$$1 is empty. Since it begins with a $, it is not empty. In fact, \$$1 expands to the string $hello.
You need to tell the shell that you want to treat the value of $1 as the name of a parameter to expand.
With bash: [[ -z ${!1} ]]
With zsh: [[ -z ${(P)1} ]]
With ksh: tmp=$1; typeset -n tmp; [[ -z $tmp ]]
Portably: eval "tmp=\$$1"; [ -z "$tmp" ]
(Note that these will treat unset and empty identically, which is usually the right thing.)

Resources