What does "test $2 &&" mean in this bash script?

That's an incorrect way to write:

#!/bin/sh -

set -e # Exit if any command fails

# If multiple args given run this script once for each arg
if [ "$#" -gt 1 ]; then
  for arg do
    "$0" "$arg"
  done
  exit
fi

I think what the author intended to do was check if the second argument was a non-empty string (which is not the same thing as checking whether there are more than 1 argument, as the second argument could be passed but be the empty string).

test somestring

A short form of

test -n somestring

Returns true if somestring is not the empty string. (test '' returns false, test anythingelse returns true (but beware of test -t in some shells that checks whether stdout is a terminal instead)).

However, the author forgot the quotes around the variable (on all the variables actually). What that means is that the content of $2 is subject to the split+glob operator. So if $2 contains characters of $IFS (space, tab and newline by default) or glob characters (*, ?, [...]), that won't work properly.

And if $2 is empty (like when less than 2 arguments are passed or the second argument is empty), test $2 becomes test, not test ''. test does not receive any argument at all (empty or otherwise).

Thankfully in that case, test without arguments returns false. It's slightly better than test -n $2 which would have returned true instead (as that would become test -n, same as test -n -n), so that code would appear to work in some cases.

To sum up:

  • to test if 2 or more arguments are passed:

    [ "$#" -gt 1 ]
    

    or

    [ "$#" -ge 2 ]
    
  • to test if a variable is non-empty:

    [ -n "$var" ]
    [ "$var" != '' ]
    [ "$var" ]
    

    all of which are reliable in POSIX implementations of [, but if you have to deal with very old systems, you may have to use

    [ '' != "$var" ]
    

    instead for implementations of [ that choke on values of $var like =, -t, (...

  • to test if a variable is defined (that could be used to test if the script is passed a second argument, but using $# is a lot easier to read and more idiomatic):

    [ "${var+defined}" = defined ]
    

(or the equivalent with the test form. Using the [ alias for the test command is more common-place).


Now on the difference between cmd1 && cmd2 and if cmd1; then cmd2; fi.

Both run cmd2 only if cmd1 is successful. The difference in that case is that the exit status of the overall command list will be that of the last command that is run in the && case (so a failure code if cmd1 doesn't return true (though that does not trip set -e here)) while in the if case, that will be that of cmd2 or 0 (success) if cmd2 is not run.

So in cases where cmd1 is to be used as a condition (when its failure is not to be regarded as a problem), it's generally better to use if especially if it's the last thing you do in a script as that will define your script's exit status. It also makes for more legible code.

The cmd1 && cmd2 form is more commonly used as conditions themselves like in:

if cmd1 && cmd2; then...
while cmd1 && cmd2; do...

That is in contexts where we care for the exit status of both those commands.


test $2 && some_command is composed of two commands:

  • test $2 is checking if the string after expansion of the second argument ($2) is of non-zero length i.e. it is necessarily checking test -n $2 ([ -n $2 ]). Note that, as you have not used quotes around $2, test will choke on values with whitespaces. You should use test "$2" here.

  • && is a short-circuit evaluation operator, which indicates that the command after && will only be run if the command before it is successful i.e. has exit code 0. So in the above example, some_command will only be run if test $2 succeeds i.e. if the string is of non-zero length, then some_command will be run.

Tags:

Scripting

Bash