7

I'm looking at a bash script with the following code:

#!/bin/sh

set -e # Exit if any command fails

# If multiple args given run this script once for each arg
test $2 && {
  for arg in $@
    do $0 $arg
  done
  exit
}
.
.
.

As mentioned in the comment the purpose is to "run the script on each argument if there is more than one" I've some runner code to test this out:

# install-unit
# If multiple args given run this script once for each arg
test $2 && {
  echo "\$0: $0"
  echo "\$1: $1"
  echo "\$2: $2"
  echo "test \$2 && is true"
}

test $2 && {
# note this is just a regular bash for loop
# http://goo.gl/ZHpBvT
  for arg in $@
    do $0 $arg
  done
  exit
}

echo "running: $1"

which gives the following:

sh ❯ ./multiple-run.sh cats and dogs and stuff
$0: ./multiple-run.sh
$1: cats
$2: and
test $2 && is true
running: cats
running: and
running: dogs
running: and
running: stuff
sh ❯ ./multiple-run.sh cats
running: cats

I'd like for someone to unpack the test $2 && {} part and the exit shell built in. In the beginning I was thinking that the test $2 was checking to see if there are more args, and maybe it is, but it looks weird because it seems like there's two expressions separated by the "and" &&, and I'm also just confused what the heck the exit builtin does.

Plain english explanations with examples and documentation references are appreciated :)

Thanks!

Edit: Thanks Stéphane Chazelas for that. For anyone who is confused by the change in for loop syntax, check out number 3 in this geekstuff article. To summarize:

echo "positional parameters"
for arg do
  echo "$arg"
done

echo "\nin keyword"
for arg in "$@"
  do echo "$arg"
done

gives us:

❯ ./for-loop-positional.sh cats and dogs          
positional parameters
cats
and
dogs

in keyword
cats
and
dogs
14

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.

5

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.

  • 1
    test "$2" is still not safe if $2 is -n or something. Also, Stéphane's answer is correct that this is just a bad method in the first place. (e.g. foo arg1 '' arg3 will fool it into thinking there's only one arg.) – Peter Cordes Sep 4 '16 at 15:38
  • 2
    @PeterCordes, note that both test "$2" and test -n "$2" are reliable in POSIX-compliant implementations of test. Now, for non-compliant tests, both will have issues (like some with test -n and some with test -n = that would return an error). Historically, test -t was meant (and documented) to test if stdout is a terminal, so test "$2" could not be reliable. POSIX changed that. ksh93's test -t still tests for stdout being a terminal (for backward portability) but only when the test and -t are litteral (not in test "$2" or cmd=test; "$cmd" -t or test $empty -t). – Stéphane Chazelas Sep 5 '16 at 6:32

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.