Home > front end >  Passing arbitrarily complex bash to `bash -c` - a one liner that doesn't work as expected
Passing arbitrarily complex bash to `bash -c` - a one liner that doesn't work as expected

Time:01-02

My problem is that I want to pass a string containing arbitrarily complex bash to bash -c "<command>", but I've found one case that doesn't behave as expected.

bash -c "s=\"hi\"; for _ in 1 2 3; do echo $s; s=\"$s hi\"; done"

I'd expect the command to output a little right angled triangle of hi's, like so:

hi
hi hi
hi hi hi

But instead the command outputs:




Which presumably means $s is unset. My suspicion is that it's because of how bash is parsed, in that it's parsed line by line.

How could I make this one line bash -c command work? Is this a downside / something to be wary of with this approach of simply passing a potentially quite complex bash script in one line to bash -c?

Thanks very much in advance :) Luke

CodePudding user response:

Why using another bash shell ?

You simply need:

s="hi"
for _ in 1 2 3; do
  echo "$s"
  s="$s hi"
done

Or using here-doc with single quote to prevent shell expansion:

bash<<'EOF'
s="hi"
for _ in 1 2 3; do
  echo "$s"
  s="$s hi"
done
EOF

Or reusing your command with proper single quotes and no escaping:

bash -c 's="hi"; for _ in 1 2 3; do echo $s; s="$s hi"; done'

Learn how to quote properly in shell, it's very important :

"Double quote" every literal that contains spaces/metacharacters and every expansion: "$var", "$(command "$var")", "${array[@]}", "a & b". Use 'single quotes' for code or literal $'s: 'Costs $5 US', ssh host 'echo "$HOSTNAME"'. See
http://mywiki.wooledge.org/Quotes
http://mywiki.wooledge.org/Arguments
http://wiki.bash-hackers.org/syntax/words
when-is-double-quoting-necessary

Output

hi
hi hi
hi hi hi

CodePudding user response:

The immediate problem is that the $ characters aren't escaped, so $s is getting expanded (to the empty string) before the command is passed to bash -c. You can see this by replacing bash -c with echo:

$ printf '%s\n' "s=\"hi\"; for _ in 1 2 3; do echo $s; s=\"$s hi\"; done"
s="hi"; for _ in 1 2 3; do echo ; s=" hi"; done

You can see that the echo $s; part turned into just echo ;, and s=\"$s hi\" into s=" hi".

You can either escape the $ characters, or switch to single-quoting the entire command instead of double-quoting it (and remove the other escapes).

More generally, anytime you have a situation like this where a string/command/whatever is passing through multiple levels of parsing (here, it's first parsed by your interactive shell, then again by the bash -c shell), it's critical to understand what's being expanded/applied by each level vs what's passed through to the next level. You often need multiple levels of quoting and/or escaping to control exactly when in the parsing process a particular thing gets acted on (vs just passed through to the next level). Above, I used printf to see what had happened after just the first level of parsing, and doing tests like this can help a lot with understanding what's going on. (Note: I prefer printf '%s\n' over echo, because some versions of echo will try to interpret escape sequences themselves, which can add confusion.)

  • Related