Where to negate logical expression in bash - bash

How do you negate a test in bash if you want to combine multiple tests?
The code is
if ! [ $(pgrep Xvfb) ] || [ ! -v DISPLAY ]; then
echo starting xvfb
mkdir -p /tmp/xvfb
Xvfb :1 -fbdir /tmp/xvfb > /tmp/xvfb_output 2>&1 &
export DISPLAY=:1
fi
It is supposed to be sourced and to start Xvfb if not already running.
Previously, it lacked the || [ ! -v DISPLAY ] part to check the existence of said variable.
To negate a test, you could do either ! [ ... ] or [ ! ... ], which both seem to work.
Is the reasoning correct that you should use [ ! ...] because it is inside the test and thus clearer (and a bit more efficient)?

I'm pretty sure it doesn't matter where you put the negation in terms of efficiency. As for readability, you could write your if statement like this:
if ! (test "$(pgrep Xvfb)" -a -n "${DISPLAY:+1}"); then
This way you have only one negation on one test.
I agree with you that
if ! [ $(pgrep Xvfb) ] || [ ! -v DISPLAY ]; then
is ambiguous and
if [ ! $(pgrep Xvfb) ] || [ ! -v DISPLAY ]; then
is not.

Related

How to test multiple variables for empty string

I want to test multiple variables to see if they are empty. So I did the following:
if [ test -z "$VAR1" ] || [ test -z "$VAR2" ]
then
echo "Empty!"
fi
However, it doesn't work. The output is:
[: -z: binary operator expected
[: -z: binary operator expected
What have I done wrong? The code above works fine if I leave out the OR (||) condition.
Use either brackets or the test command, don't use both. Either of the following will work:
if [ -z "$VAR1" ] || [ -z "$VAR2" ]
then
echo "Empty!"
fi
Or:
if test -z "$VAR1" || test -z "$VAR2"
then
echo "Empty!"
fi
In some older shells [ was just an alias for the test command. You could even miss out the closing ] (or add it after test) and everything would be fine. Nowadays, on bash this will give a syntax error. However, the two (correct) syntaxes are still functionally equivalent and can be used interchangeably but not at the same time.
You should either use test, or [; they are synonymous, except [ requires a ] as last argument.
[ -z "$VAR1" ] || [ -z "$VAR2" ]
or
test -z "$VAR1" || test -z "$VAR2"

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.

Bash giving message that there are too many arguments when if statement is continued onto newline

I have a long if clause in my Bash script that is giving me a warning of script.sh: line 1: [: too many arguments
Here is the if clause:
if [ ! -f first_"$today".csv.gz ] || [ ! -f second_"$today".csv.gz ] || [ ! -f third_"$today".csv.gz ] || [ ! -f fourth_"$today".csv.gz ] || [ ! -f fifth_"$today".csv.gz ] || [ ! -f sixth_"$today".csv.gz ]\
[ ! -s first_"$today".csv.gz ] || [ ! -s second_"$today".csv.gz ] || [ ! -s third_"$today".csv.gz ] || [ ! -s fourth_"$today".csv.gz ] || [ ! -s fifth_"$today".csv.gz ] || [ ! -s sixth_"$today".csv.gz ];
So I worked backwards by commenting out each if bracket/statement from the end. When I ended up commenting out the entire second line and ending the if clause at the end of the first line, I stopped receiving the error/warning message. I have similar if statements elsewhere in my script, and they all run perfectly fine.
My script does not break at all, as it continues on with the rest of the script. I would like to fix this so that the warning message stops popping up. Is there something I'm doing wrong with the newline character? Anything else I missed?
The operator (||) is missing where you break the line.

use multiple conditions in 'test' command

I have been trying to convert the following code to use 'test' instead of 'if'
if [ -e ./blah ] && [ ! -L ./blah ];
then
exit 1
fi
My intention is to use test instead, so that I dont have to exit 1 explicitly. I am trying something like this:
test -e ./blah && ! -L ./blah;
instead of &&, I have tried -a, with different bracket combinations, but I am not successful. I am sure there should be a simple way to do this. Can anyone help me ?
test does not understand logical operators && and ||. You can use test -e ./blah -a ! -L ./blah, but if you are using bash, you can also switch to its more powerful [[ .. ]] construct:
[[ -e ./blah && ! -L ./blah ]]
To make if [ -e ./blah ] && [ ! -L ./blah ]; work, use the following
if [ -e ./blah -a ! -L ./blah ]; (-a stands for and) http://tldp.org/LDP/abs/html/comparison-ops.html
Demo -- http://ideone.com/GR8UiK
But, as others have pointed out, [[ .. ]] construct is more powerful than [...].
You can combine them all (including if then fi):
[[ -e ./blah && ! -L ./blah ]] && exit 1
Use [[ keyword as it is more powerful.
if [[ -e ./blah && ! -L ./blah ]]
then
...
fi
However, to ensure portability, you can do something like this too
if [ -e ./blah ] && [ ! -L ./blah ]
then
...do something
fi
As you ask to use test, you can do like so:
test -e ./blah && test -L ./blah || ( echo 'First action' ; echo 'Second action )
The different operators (&&, ||, etc...) are first parsed by the shell, so you can't use it in command parameter(s).
if [ -e ./blah ] && [ ! -L ./blah ];
is equivalent to
if test -e ./blah && test ! -L ./blah;
Therefore you can simply write
test -e ./blah && test ! -L ./blah
To wit:
$ help [\[] | tail -n +3
[: [ arg... ]
Evaluate conditional expression.
This is a synonym for the "test" builtin, but the last argument must
be a literal `]', to match the opening `['.
Do the following:
$ ls -i /bin/test
54008404 /bin/test
$ ls -i /bin/[
54008404 /bin/test
That 54008404 is the inode number. This is the real name of a file. The /bin/test simply points to the inode and the inode contains all file file information.
The thing to note is that /bin/[ and /bin/test are the same inode. That means, they're the same command.
Thus:
if [ -f "$foo" ]
is the same as:
if test -f "$foo"
The if command executes the command given, and then will execute the if clause if the command returns true and doesn't execute the clause if the command it false.
For example:
if grep -q "foo" $foo
then
echo "File $foo contains the regular expression /foo/"
fi
Is completely valid. The grep -q command (in many variants of grep means search for the regular expression, and if that regular expression is in the file, return an exit code of 0 (which means the command succeeded and is true).
Note there is no square brackets.
The test command (or [...]) merely runs a test as specified, and returns with an exit code of 0 (thus the command is a success) if the test is true. That's all it does.
You may also see this construct:
[ "$foo" = "$bar" ] && echo "$foo is equal to $bar"
The && means execute the next command (and return the exit code) if the first command returns an exit code of zero. Otherwise, simply return the first command's exit code.
Thus:
if [ -e ./blah ] && [ ! -L ./blah ];
is saying run test -e ./blah and if that is true (that is, the file exists) execute test ! -L ./blah and if that is also true, run the if clause of the statement.
Note that [ -e ./blah] and [ ! -L ./blah ] are two separate commands. The && strings together the two commands:
[ "$foo" = "$bar" ] && some_command;
This says, run test "$foo" = "$bar" and if that is true, run the command some_command. Note that this is equivalent to:
if [ "$foo" = "$bar" ]
then
some_command
fi
The other list structure is the ||. This means that if the first command succeeds, return an exit code of 0 and don't run the second command. Thus:
[ "$foo" = "$bar" ] || some_command;
Is the same as:
if [ "$foo" = "$bar" ]
then
:
else
some_command
fi
Let's get back to your _original question:
if [ -e ./blah ] && [ ! -L ./blah ];
then
exit 1
fi
Is the same as:
if test -e ./blah && test ! -L ./blah
then
exit 1
fi
Which is the same as
test -e ./blah && test ! -L ./blah && exit 1
This means: If test -e ./blah is true (./blah is a file), then execute the command after the && list operator. This is test -! -L ./blah. If this test also is true, run the command after the && list operator again.
This could also be rewritten as:
test -e ./blah && test -L ./blah || exit 1
This says that if test -e ./blah is true, run the command after the && list operator. If test -L ./blah is false, run the command after the || operator.

bash if with or and negation

why does:
#!/bin/bash
wtf=false
if [ $wtf ] || [ ! -f filethatexists.whatever ]
then
echo "WTF1"
fi
if [ ! -f filethatexists.whatever ]
then
echo "WTF2"
fi
print:
WTF1
instead of nothing? It is especially perplexing that the second form works as expected and the first not.
The basic test
[ $wtf ]
tests whether the string in the middle is empty or not.
Since $wtf contains the string 'false', the test returns true, or exit status 0 for success, because 'false' is not the same as the empty string '' — and hence you get WTF1 as the response.
Try with:
wtf=''
As pointed out by Gordon Davisson (and Dennis Williamson), it is a good idea to be careful with strings that you are testing. Indeed, I should have stated that I would always use [ -n "$wtf" ] or [ -z "$wtf" ] to test whether a variable is set, because that was necessary when I was learning shell, once upon a quarter century ago. I've had counter stories from Bash afficionados that you don't have to worry about it in Bash - however, I think the code here provides a counter-example that in fact you do still have to worry about it.
So, some best practices:
Enclose tested variables in double quotes, or
(In Bash), use [[ $wtf ]] which does know how to handle the variable expansion.
Use the -n or -z tests to test for non-empty or empty values.
There can be exceptions to the rules - but you will not go far wrong following them.
Consider the code:
wtf="1 -eq 0"
[ $wtf ] && echo "WTF0"
[[ $wtf ]] && echo "WTF1"
wtf="false"
[ $wtf ] && echo "WTF2"
[[ $wtf ]] && echo "WTF3"
wtf=""
[ $wtf ] && echo "WTF4"
[[ $wtf ]] && echo "WTF5"
wtf="false"
[ "$wtf" ] && echo "WTF6"
[[ "$wtf" ]] && echo "WTF7"
wtf=""
[ "$wtf" ] && echo "WTF8"
[[ "$wtf" ]] && echo "WTF9"
That produces:
WTF1
WTF2
WTF3
WTF6
WTF7
with both bash and ksh (as found on MacOS X 10.6.4, when run with 'bash testcode.sh' or 'ksh testcode.sh'). A real Bourne shell (if you can still find such a thing) would object to the double-bracket operations - it would not be able to find the command '[[' on $PATH.
You can extend the testing to cover more cases ad nauseam.
Here's a handy little trick:
wtf=false
if $wtf || [ ! -f filethatexists.whatever ]
In this form, the contents of the variable are executed and the return value determines whether the test passes or fails. It happens that true and false are Bash builtins that return the appropriate value.
if [ $wtf = true ] || [ ! -f . .

Resources