Search code examples
bashif-statementnestedvariable-assignmentside-effects

bash: assignment AND condition test in nested statement?


What I'm looking for is a compact line of code that uses the value of an expression twice in one nested statement

  • for an assignment to a variable and

  • a condition test of the variable's new value against another value

The questions are:

  1. Is this compact form generally possible in bash?

  2. If it is, do I have to fear negative side effects (unexpected results)?

You can certainly make the assignment before:

$ local bus_connection="$(lsblk -dn -o TRAN /dev/sdd)"
$ test "$bus_connection" = "usb" || { echo "not usb"; exit 1 }
$ echo "$bus_connection"
usb

--> Works!

If variable 'bus_connection' is only needed in case of success you also can do:

$ test "$(lsblk -dn -o TRAN /dev/$DEVICE)" = "usb" \
    && local bus_connection="usb" \
    || { echo "not usb"; exit 1 }
$ echo "$bus_connection"
usb

--> Also works! (Don't use 'local' on the command line!)

In C(++) and some other compiled high level languages you can do it more compact (let's suppose a c function 'lsblk'):

if( ( bus_connection = lsblk( ... ) ) == "usb" ) ; else {printf("not usb"); exit 1;}

In bash I tried:

$ test $(bus_connection="$(lsblk -dn -o TRAN /dev/sdd)") = "usb" || echo "not usb" 
bash: test: =: unary operator exspected.
not usb

--> Error message and wrong result!

I tested with an integer case and found:

$ sum=0
$ result=12
$ test $((sum=$sum+$result)) -gt 20 && echo "true"
$ test $((sum=$sum+$result)) -gt 20 && echo "true"
true

--> Works! Negative side effects?

Analog:

$ test $((bus_connection="$(lsblk -dn -o TRAN /dev/sdd)")) = "usb" || echo "not usb"
not usb

--> No error message but wrong result!

$ echo "$bus_connection"
0

--> Ah, obviously bash treats variable 'bus_connection' as of type integer now.

Any suggestions for strings?


Solution

  • Is this compact form generally possible in bash

    No, it is not, at least not without a custom code to handle it. Command substitution is run in a subshell - changes will not be visible in parent shell. Arithmetic expansion does only arithmetic operations - all strings are converted to numbers (as if by atoi()/strtol()), if no digits are scanned they are converted to 0.

    The usual way is just to assign the variable with a && followed by test - that allows to check the exit status of the command and grab the result.

    if ! {
         bus_connection="$(lsblk -dn -o TRAN /dev/sdd)" &&
         test "$bus_connection" = "usb"
    }; then
       echo "not usb"
       exit 1
    fi
    

    or just ignore the exit status of lsblk:

    if
       bus_connection="$(lsblk -dn -o TRAN /dev/sdd)"
       ! test "$bus_connection" = "usb"
    then
       echo "not usb"
       exit 1
    fi
    

    Still you could write your own function that uses bash namereference to assign the first argument to the second and return with tests exit status:

    test_and_assign() {
       declare -n _test_and_assign_ref=$1
       shift
       _test_and_assign_ref="$1"
       test "$@"
    }
    
    $ test_and_assign bus_connection "$(echo 123)" = "usb"
    $ echo "$? $bus_connection"
    1 123
    $ test_and_assign bus_connection "$(echo usb)" = "usb"
    $ echo "$? $bus_connection"
    0 usb
    

    Note that here, as in your first code snippet with local bus_connection=$(...), the exit status of the command substitution is ignored.