Why do you need quotes here in Bash? - 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.

Related

Why are unset and empty variables numerically equal to zero with double brackets

The following code (tested with bash, zsh, and ksh, results may vary for other shells) returns a is 0 (num). The same happens for a=''. Results for assigning a=0 or a=1 are predictable. Quoting expressions does not change the result. So, why do double brackets treat null and empty variables as being numerically equal to zero?
unset a
#a=''
#a=0
#a=1
if [[ $a == 1 ]] ; then
echo 'a is 1 (string)'
fi
if [[ "$a" == 0 ]] ; then
echo 'a is 0 (string)'
fi
if [[ $a -eq 1 ]] ; then
echo 'a is 1 (num)'
fi
if [[ "$a" -eq 0 ]] ; then
echo 'a is 0 (num)'
fi
I'm deliberately avoiding the broader issues of single and double brackets, since it's very well covered other places on this site and elsewhere. Surprisingly, I've been unable to find anything that documents this particular behavior.
Further evidence:
unset a ; if [[ $a -gt -1 ]] ; then echo 'a > -1' ; fi
unset a ; if [[ $a -lt 1 ]] ; then echo 'a < 1' ; fi
I don't believe it is explicitly documented, but it's the -eq operator that forces a quasi-arithmetic context for its operands. Consider:
$ [[ "(3 + 5)" -eq 8 ]] && echo qed
qed
The behavior for variables is documented under ARITHMETIC EVALUATION:
A shell variable that is null or unset evaluates to
0 when referenced by name without using the parameter expansion syntax.
though it's not obvious that this should also apply to a string that results from a parameter expansion, and indeed the behavior is different in an arithmetic expression or arithmetic command itself:
$ unset a
$ (( a == 0 )) && echo zero
zero
$ (( $a == 0 )) && echo zero
bash: ((: == 0 : syntax error: operand expected (error token is "== 0 ")
Given that you already have $((...)) and ((...)) available for arithmetic, it's best to avoid -eq and the other arithmetic comparison operators inside [[; either use [ "$a" -eq 0 ] (which will raise an error if a is null or unset) or use (( a == 0 ).
It's well documented in the manual: 6.5 Shell Arithmetic
Within an expression, shell variables may also be referenced by name without using the parameter expansion syntax. A shell variable that is null or unset evaluates to 0 when referenced by name without using the parameter expansion syntax.
and
A null value evaluates to 0.
Your tests are running into the 2nd case.
I can't tell you the design rationale behind it. It is handy in practice though:
unset a
(( a++ ))
echo $a # => 1
Also, other languages do the same thing:
$ awk 'BEGIN {if (unset_variable == 0) print "zero"}'
zero
$ perl -E 'if ($unset_variable == 0) {say "zero"}'
zero
So, why do double brackets treat null and empty variables as being numerically equal to zero?
From bash shell manual 6.5 Shell Arithmetic:
A shell variable that is null or unset evaluates to 0 when referenced by name without using the parameter expansion syntax
The quotes inside bash extension [[ compound command do not matter, because word splitting expansion is not performed inside [[.

Print a comparison output in bash as a boolean value

I have a question
a=1
b=2
I want the comparison output to a variable. ie in windows languages you can write something like this. it should print false
print ($a == $b)
tries these below in console.
echo $a -eq $b
echo (( $a -eq $b ))
echo "$a" -eq "$b"
c= $(expr "$a" -eq "$b" )
echo $c
You can use arithmetic expansion.
echo $(( a == b ))
This will print 1 if the expression is true and 0 if the expression is false. No need to add $ before variable names, you can use operators like in C language and the spaces can be omitted. See Bash reference manual:Shell arithmetic for more info.
Having it to print a string "true" or "false" is a bit more tricky. Usually I go with the same as #Inian, but using if ... then .. else ... fi because I usually code under set -euo pipefail:
if (( a == b )); then echo true; else echo false; fi
but we can be smart and do an array:
to_bool[0]="false"
to_bool[1]="true"
echo ${to_bool[$(( a == b ))]}
but I see no difference then printing just 0 or 1.
I do not think it is possible to do it in bash directly.
But you can do something as the following based on the return code of the comparison operator:
res=0; [ "$s1" == "$s2" ] && res=1
echo $res
It sets res to zero first and then only if the comparison succedes sets the res variable to 1.
Alternatively, something more concise is the following:
[ $s1 -eq $s2 ]; echo $((!$?))
which literally prints the return code of the previously executed command. Note the ! not operator applied to the return code as 0 usually means success i.e. in this case the variable are the same.
Note that bash does not natively support a bool type operator. All the commands/built-ins return an exit code to the shell depending upon its success/failure status. A code 0 to the shell means the operation is success and any non-zero would represent a failure.
So in bash, you do the comparison and need to set the bool strings explicitly, something like
[[ $a == $b ]] && echo true || echo false
Note that using echo true should not confused with the built-ins /bin/true and /bin/false which explicitly set the exit code to the shell to 0 and 1 respectively. The echo statement just prints out the string mentioned to the standard output.
Also note that [[ is a shell keyword extension to the POSIX-ly available [ construct. The former is a added to the bourne again shell and may not available in other POSIX compliant shells. If you are looking for a portable way of doing this, use the [ with the case construct as
[ "$a" -eq "$b" ]
case $? in
0) printf '%s\n' "true" ;;
*) printf '%s\n' "false" ;;
esac

Parameters work properly when remove their quoting

I am puzzled about the verbose of quoting in the script. Take an example from the instruction I followed:
min_val=1
max_val=100
int=50
if [[ "$int" =~ ^-?[0-9]+$ ]]; then
if [[ "$int" -ge "$min_val" && "$int" -le "$max_val" ]]; then
echo "$int is within $min_val to $max_val."
else
echo "$int is out of range."
fi
else
echo "int is not an integer." >&2
exit 1
fi
Run it and come by:
$ bash test_integer3.sh
50 is within 1 to 100.
When I removed all the quoting in testing:
if [[ $int =~ ^-?[0-9]+$ ]]; then
if [[ $int -ge $min_val && $int -le $max_val ]]; then
echo "$int is within $min_val to $max_val."
else
echo "$int is out of range."
fi
else
echo "int is not an integer." >&2
exit 1
fi
It's still working properly.
$ bash test_integer3.sh
50 is within 1 to 100.
Why should live with the habit of writing redundant quoting?
The real problem comes when you start to use [ command over [[ in your scripts. [[ is bash's improvement to the [ command. It has several enhancements that make it a better choice to write scripts targeting bash.
One such improvement would be that you no longer have to quote variables because [[ handles empty strings and strings with white-space more intuitively. For example consider your script written with [ for the un-quoted case and for discussions sake, one of your variables is empty
#!/usr/bin/env bash
min_val=
max_val=
int=50
if [[ $int =~ ^-?[0-9]+$ ]]; then
if [ $int -ge $min_val -a $int -le $max_val ]; then
echo "$int is within $min_val to $max_val."
else
echo "$int is out of range."
fi
else
echo "int is not an integer." >&2
exit 1
fi
One thing to note is I've re-written the combined conditional using the -a syntax since [ does not support && operator within but could be combined using && as [ $int -ge $min_val ] && [ $int -le $max_val ]
You would see things going bad and seeing errors as below which means that one of the conditionals involving -le is gone wrong on seeing an empty string.
1_script.sh: line 7: [: -a: integer expression expected
50 is out of range.
whereas with same code for undefined variables and replacing the expression to use [[ would gracefully handle the empty strings to produce just an incorrect result as
50 is out of range.
So to sum it up, from the many advantages over using [[, the particular advantage on your case is to handle variables if there could be empty strings in your conditionals.
Quoting is used to to stop the word splitting. In the case above it is not necessary but consider a case like this: you have a directory and having theses files file1.txt, file2.txt, old.txt and file1 old.txt.
If you wish to remove the file file1 old.txt and run the command
rm file1 old.txt
then it will remove the file old.txt instead of what you expected.
In your piece of code you don't need quotes as you discovered. However, using quotes is considered "good practice" because unexpected things can happen without quotes. For example if you run the code with int equal to say "foo bar" you might get some strange results without quotes.
It is like the double and triple equals in JavaScript. You could probably get away with only double equals but some unexpected results might occur.

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

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

Why does [ a -gt b ] (not [ "$a" -gt "$b" ]) appear to work?

I have one problem in unix shell scripting. Let me ask you with very simple example.
suppose, I am getting user input and comparing two numbers.
echo "Enter the first number"
read a
echo "Enter the second number"
read b
if [ a -gt b ]---------------------->I have not used $ before variable name.
then
echo "a is greater"
else
echo "b is greater"
fi
In this example, i should have used $ to get the value of variable. By mistake, I forgot. Still, it gives me right result, how and why?
I tried to debug by using sh -x filename. but, it doesn't show any value while comparing(because i havn't used $ sign).
How the shell decide which is greater and vice versa?
How it works internally?
Thanks.
Notably, you could use this without a $ if you did the comparison in a numeric context.
In bash:
if (( a > b )); then
echo "a is greater"
else
echo "b is greater"
fi
...would be correct, as (( )) (double parens) enters a numeric context within which all textual strings are treated as variables and automatically dereferenced (and within which operators such as > and < have their typical arithmetic meanings, rather than performing redirections).
What you're doing now is heavily implementation-dependent. [ a '>' b ] would be a lexographic comparison between the letters a and b. [ a -gt b ] is an arithmetic test.
Many shells will not allow this at all. For instance:
$ a=2; b=3; [ a -gt b ]; echo $?
-bash: [: a: integer expression expected
2
Notably, this is different inside of bash's [[ ]] test context (which doesn't carry the POSIX semantics of [ ]). There, what you're doing actually does work, though it's harder to read than the (( )) form:
$ a=2; b=3; [[ a -gt b ]]; echo $?
1
$ a=3; b=2; [[ a -gt b ]]; echo $?
0
If you're limited to POSIX sh, you don't have (( )), but you do have $(( )), which you can use almost the same way:
$ a=2; b=3; result=$(( a > b ? 1 : 0 )); echo "$result"
0
$ a=3; b=2; result=$(( a > b ? 1 : 0 )); echo "$result"
1
There is a simple explanation why your test appears to work: it blindly executes the else clause.
The [ ... ] built-in has no way to report the error to the caller except by exiting with a non-zero exit status. Incidentally, a non-zero exit status is also used to indicate "false". This means that if cannot distinguish between a false and an erroneous evaluation of its condition, which is why your comparison always prefers the else branch and thus appears to work. You can easily test it by reverting the arguments:
$ if [ b -gt a ]
then
echo "b is greater"
else
echo "a is greater"
fi
dash: 11: [: Illegal number: b
a is greater
Note that the error message printed to standard error is, as far as the if statement is concerned, a mere side effect of the test command, and is entirely ignored.

Resources