6

I thought

[ 1 -eq $1 ] && echo "yes" || echo "no"

Acts like

if [ 1 -eq $1 ]; then
    echo "yes"
else
    echo "no"
fi

But, when I run this script (nocmd is a non-existing command)

#!/bin/bash

[ 1 -eq $1 ] && nocmd "yes" || echo "no"

I get a strange output for parameter '1':

me@ubuntu:/tmp$ ./ddd.sh 0
no
me@ubuntu:/tmp$ ./ddd.sh 1
./sh.sh: line 3: nocmd: command not found
no

Looks like it acts like:

if [ 1 -eq $1 ]; then
    nocmd "yes"
    if [ $? -ne 0 ]; then
        echo "no"
    fi
else
    echo "no"
fi

Is it fine? Am I missing something?

Charles Duffy
  • 1,732
  • 15
  • 22
hudac
  • 779
  • 4
    It's no strange thing there, when you ran with ./ddd.sh 0 this part will check [ 1 -eq $1 ] it's not true (false) (1!=0), so this part won't run && nocmd "yes" and this part will run || echo "no", but when it's true 1=1 it will execute && nocmd "yes" part and since shell doesn't recognize nocmd command, it's reported error .... command not found and again second part will run || echo "no". – αғsнιη Jan 30 '18 at 17:00

3 Answers3

20

Except for the overall exit status, it acts like:

if
  ! { 
    [ 1 -eq $1 ] && nocmd "yes"
  }
then
  echo no
fi

In:

A || B

B is executed iff A fails. That's a OR operator.

In your case A is [ 1 -eq $1 ] && nocmd "yes" where nocmd is run iff [ succeeds (a AND operator), in which case the exit status of A will be that of nocmd. In other words echo no will be executed if either [ or nocmd "yes" fails (bearing in mind that nocmd is only run if [ succeeds).

Those x && y || z are dirty hacks. They are best avoided for that very reason. Use a if/then/else construct if you do want a if/then/else logic. Use x && y || z only if you want z to be unless both x and y succeeded.

Even in:

cmd && echo OK || echo >&2 KO

The echo OK could fail under some pathological conditions (like stdout going to a file on a full filesystem), and echo >&2 KO could end up being executed as well.

$ bash -c 'true && echo OK || echo KO >&2' > /dev/full
bash: line 0: echo: write error: No space left on device
KO
5

Well, what actually happens is this (and this is how && and || works)

test 1 -eq $1
ret=$?
if [ $ret -eq 0 ]; then
    nocmd "yes"
    ret=$?
fi
if [ $ret -ne 0 ]; then
    echo "no"
fi

In your case, if $1 does not equal 1, or is not a valid number, then you have a non-zero $?, and the first if is skipped, and the second if prints "no". If $1 equals 1 , then nocmd "yes" is executed, returning a non-zero $?, and "no" is also echoed.

ilkkachu
  • 138,973
Weijun Zhou
  • 3,368
  • That's not exactly true, and the reason is a bit weird. An if statement returns 0 if it doesn't run either of the then and else branches. So if the first test fails, $? is zero on the second test, always. if false ; then echo x; fi; echo $? prints 0. – ilkkachu Jan 30 '18 at 17:40
  • @ilkkachu Noted. I didn't know that before. – Weijun Zhou Jan 30 '18 at 17:41
  • It's really more like that, at least I get the same result for that construct and the && .. || chain – ilkkachu Jan 30 '18 at 17:43
  • @ilkkachu So you mean that my original version of code is valid but for more complicated reasons? – Weijun Zhou Jan 30 '18 at 17:45
  • 1
    no, with the test directly against $?, the double-if gives a different result from the && ||, since the first if-statement throws the second off. With the temporary variable the behavior matches. See: https://pastebin.com/q0Ct0Zw8 for the difference without the temp – ilkkachu Jan 30 '18 at 17:56
3

What you're missing is that && and || operate on exit status of commands to the left of them - left associativity. You have here some group of commands || echo "no", and no will be echoed if and only if that group of commands returns non-success exit status.

What is that group of commands ? In first case you have [ 1 -eq "$1" ] && echo "yes". If [ portion failed, that'd be counted as fail exit status for [ 1 -eq "$1" ] && echo "yes", therefore you'd echo "no" would run. Also because of left associativity, when "$1" is 1, the [ 1 -eq $1 ] returns success, then lets nocmd run which doesn't exist and shell will return error exit status, the whole group will have exit status of fail, hence `"no" is echoed.

By contrast, in your if statement

if [ 1 -eq $1 ]; then
    echo "yes"
else
    echo "no"
fi

which route to take depends only on exit status of [ portion. In [ 1 -eq "$1" ] && echo "Yes" || echo "no" echo also plays role as to whether or not echo "no" runs.

Your second if statement example is also different

if [ 1 -eq $1 ]; then
    nocmd "yes"
    if [ $? -ne 0 ]; then
        echo "no"
    fi
else
    echo "no"
fi

The echo "no" after else doesn't depend on what happens to nocmd as in the chaining of logical operators. Sure you still do the echo "no" part after doing nocmd, but here its exit status isn't grouped together with the whole if portion to which it belongs.