1

I've looked at the similar posts about this problem, but cannot figure out how to get the executed code to be in the correct format, which needs to be foo --bar "a='b'". My best attempt at this was

#!/bin/bash -x

bar='--bar ''"''a='"'"'b'"'"'"'
cmd=(foo $bar)
echo ${cmd[@]}
eval ${cmd[@]}

The output from this is correct for the echo, but incorrect for eval

+ bar='--bar "a='\''b'\''"'
+ cmd=(foo $bar)
+ echo foo --bar '"a='\''b'\''"'
foo --bar "a='b'"
+ eval foo --bar '"a='\''b'\''"'
++ foo --bar 'a='\''b'\'''

What is the correct way to execute the command with the option?

Paul Grinberg
  • 1,184
  • 14
  • 37
  • `cmd=(foo "$bar")`, surely? (Unless you have a good reason *not* to quote variable expansion.) – AlexP Dec 13 '19 at 00:06
  • 2
    Don't store complex lists of arguments as plain strings, use an array: `bar=(--bar "a='b'")`; `foo "${bar[@]}"`. And avoid `eval`, it's a huge footcannon. See [this question](https://stackoverflow.com/questions/7454526/bash-variable-containing-multiple-args-with-quotes) – Gordon Davisson Dec 13 '19 at 00:07
  • 1
    [BashFAQ #50](https://mywiki.wooledge.org/BashFAQ/050) is pertinent as a whole. – Charles Duffy Dec 13 '19 at 00:19
  • 2
    BTW, note that `--bar "a='b'"` is usually wrong -- most commands are used like `--bar a='b'`, in which case there aren't any literal quotes at all, but *only* syntactic ones (there's no difference *whatsoever* between `a='b'` and `a=b` or `'a=b'`, because they all turn into the same array of C strings passed to the `execve()` syscall when invoking the command to which those arguments are passed). – Charles Duffy Dec 13 '19 at 00:20
  • ...btw, once you've finished BashFAQ #50 (as linked above), [BashFAQ #48](https://mywiki.wooledge.org/BashFAQ/048) is also pertinent. – Charles Duffy Dec 13 '19 at 00:25
  • What do you mean by `which needs to be foo --bar "a='b'"`. I suspect you mean that you want to execute `foo` with the arguments `--bar` and `a='b'`. The double quotes are normally removed by the shell before executing `foo`. – William Pursell Dec 13 '19 at 00:27
  • @PaulGrinberg, btw, you might also find the output of `if [[ 'a='\''b'\''' = "a='b'" ]]; then echo "they're exactly the same"; else echo "the strings differ"; fi` informative. – Charles Duffy Dec 13 '19 at 00:27

2 Answers2

3

If you must store command fragments, use functions or arrays, not strings.

An example of best-practice code, in accordance with BashFAQ #50:

#!/usr/bin/env bash
bar=( --bar a="b" )
cmd=(foo "${bar[@]}" )
printf '%q ' "${cmd[@]}" && echo  # print code equivalent to the command we're about to run
"${cmd[@]}"                       # actually run this code

Bonus: Your debug output doesn't prove what you think it does.

"a='b'" and 'a='\''b'\''' are two different ways to quote the exact same string.

To prove this:

printf '%s\n' "a='b'" | md5sum -
printf '%s\n' 'a='\''b'\''' | md5sum -

...emits as output:

7f183df5823cf51ec42a3d4d913595d7  -
7f183df5823cf51ec42a3d4d913595d7  -

...so there's nothing at all different between how the arguments to echo $foo and eval $foo are being parsed in your code.

Why is this true? Because syntactic quotes aren't part of the command that's actually run; they're removed by the shell after it uses them to determine how to interpret a command line character-by-character.

So, let's break down what set -x is showing you:

'a='\''b'\'''

...consists of the following literal strings concatenated together:

  • a= (in a single-quoted context that is entered and ended by the single quotes surrounding)
  • ' (in an unquoted context, escaped by the backslash that precedes it)
  • b (in a single-quoted context that is entered and ended by the single quotes surrounding)
  • ' (in an unquoted context)

...everything else is syntactic, meaningful to the shell but not ever passed to the program foo.

Gordon Davisson
  • 118,432
  • 16
  • 123
  • 151
Charles Duffy
  • 280,126
  • 43
  • 390
  • 441
0

If you want exactly the same expansion to happen as in echo ${cmd[@]}, just run the command then:

${cmd[@]}

It will execute:

+ foo --bar '"a='\''b'\''"'

Note that because it is unquoted, for example * will be expanded according to filename expansion.

KamilCuk
  • 120,984
  • 8
  • 59
  • 111
  • 3
    Why in the world would you do that round-trip, though? `printf '%q '` creates a string that's safe to `eval`, sure, but the end result is exactly the same as just running `"${cmd[@]}"`, just slower and with more complexity. – Charles Duffy Dec 13 '19 at 00:19
  • ...and `eval "${cmd[@]}"`, by contrast, is just *wrong* in ways your original formulation wasn't. `eval` concatenates all its arguments together into a single string before parsing that string. – Charles Duffy Dec 13 '19 at 00:22
  • If you want a test case that demonstrates this, try `cmd=( echo '* *' )` -- after that assignment, `"${cmd[@]}"` properly emits `* *` on output, whereas `eval "${cmd[@]}"` prints all the files in the current directory on stdout twice. – Charles Duffy Dec 13 '19 at 00:24
  • We can debate, while I think it really depends what is "the correct way to execute the command with the option" OP has in mind. I assumed (at first) OP wants the `*` to be expanded, as that happens in `cmd=('*'); echo ${cmd[@]}`. – KamilCuk Dec 13 '19 at 00:25
  • 1
    Why is unquoted `${cmd[@]}` *ever* correct/desirable? You get a lot of serious bugs that way -- it's not just glob expansion. – Charles Duffy Dec 13 '19 at 00:32
  • 1
    As an example, try `cmd=( printf ' - %s\n' 'hello world' 'goodbye world' )`; if it's not quoted, the output is... surprising. – Charles Duffy Dec 13 '19 at 00:33