Home > OS >  Constructing command from array elements and checking exit code with if in Bash
Constructing command from array elements and checking exit code with if in Bash

Time:10-18

Here is a minimal example:

#!/bin/bash

testFunc() (
    shopt -s extglob
    local posArgs=();
    for((i=1; i<="$#";   i))
    do
        posArgs =("${!i}");
    done
    #<uncomment to see> echo "${posArgs[@]:0:$((${#posArgs[@]} - 1))}";
    if ${posArgs[@]:0:$((${#posArgs[@]} - 1))};
    then 
        return 0;
    fi
    return 1;
)
  • Function body uses () instead of {} to limit the scope of shopt
  • Function discards the last argument and executes the rest and checks exit code, if 0 returns 0, else returns 1. Quotes and command substitution are not used on purpose.

Here is what works:

testFunc "echo" "whatever" "discardThis"; #whatever
testFunc "pidof" "memcached" "discardThis"; #12435
testFunc "pwd" "discardThis"; #path/to/pwd
testFunc "pwd" "discardThis"; #path/to/pwd
testFunc "/bin/bash" "-c" "pwd" "discardThis"; #path/to/pwd
testFunc "/bin/bash" "-c" "pwd;pwd" "discardThis"; #path/to/pwd\npath/to/pwd NOTE:no space between ; and pwd

Here is what does not work:

testFunc "/bin/bash" "-c" "[[ 2 -eq 2 ]]" "discardThis"; #error
testFunc "/bin/bash" "-c" "\"[[ 2 -eq 2 ]]\"" "discardThis"; #error
testFunc "/bin/bash" "-c" "\"echo helloo\"" "discardThis"; #error
testFunc "/bin/bash" "-c" "echo helloo" "discardThis"; #exits with 0 but logs nothing
testFunc "/bin/bash" "-c" "pwd; pwd" "discardThis"; #logs pwd ONCE
testFunc "/bin/bash" "-c" "\"pwd; pwd\"" "discardThis"; #error

Above logs error like: helloo": -c: line 0: unexpected EOF while looking for matching... Interestingly to make the above work, you can pass eval:

testFunc "eval" "/bin/bash" "-c" "\"echo helloo\"" "discardThis"; #helloo
testFunc "eval" "/bin/bash" "-c" "\"[[ 2 -eq 2 ]]\"" "discardThis"; #exits with 0
testFunc "eval" "/bin/bash" "-c" "pwd; echo haha; pwd;" "discardThis"; #path/to/pwd\nhaha\npath/to/pwd
testFunc "eval" "/bin/bash" "-c" "pwd; pwd" "discardThis"; #Correctly logs pwd TWICE

Seems like when there is a command like echo that expects an argument, or some other complex command eval is needed. How can I avoid this? How can I make them work without eval.

I am not interested in implementing what I want, I am interested in why bin/bash fails to parse a string like echo hi.

CodePudding user response:

why bin/bash fails to parse a string like echo hi.

If you interested why the following (which was not in your examples):

testFunc "/bin/bash" "-c" "echo hi" "discardThis"

outputs nothing, then let's dive in:

${posArgs[@]:0:$((${#posArgs[@]} - 1))

The result of unquoted expansion undergoes word splitting (and filename expansion!! Lucky you, you didn't test with *). Word splitting splits the stuff on spaces (and tabs and newlines, with default IFS), no matter what you input. The 3 words argument:

"/bin/bash" "-c" "echo hi"

become 4 words, echo hi is split on spaces:

/bin/bash -c echo hi

Or more visible, so that you can see:

/bin/bash -c 'echo' hi

From man bash:

-c       [...] If there  are  arguments  after  the command_string, the first argument is assigned to $0 [...]

hi is assigned to $0 for bash, and then bash executes echo with no arguments.

How can I avoid this?

Use quotes.

How can I make them work without eval.

Use quotes.

Check your script with shellcheck.

  • Related