1

In Bash, is it possible to pass a variable with a properly-quoted list of options to a command and not have it split on whitespace inside quotes? IOW, this script:

MYCONFIG="--hi FOO=bar 'X=ABC 123'"
printf '[%s]\n' $MYCONFIG

Outputs:

[--hi]
[FOO=bar]
['X=ABC]
[123']

What I would like it to instead output is

[--hi]
[FOO=bar]
['X=ABC 123']

I realize it can be done with arrays or as a function, but I'm working with a script that already depends on the creation of a string variable to hold args. It can't be replaced with a function because other users of the script will get errors.

Is it possible to construct a string with proper quotation to prevent breaking on spaces inside quoted strings?

Failing that, how can I modify the script to support both array configs and existing string configs, perhaps converting the one to the other?

theory
  • 127
  • 1
    Re-posting this question isn't going to change the answer. – muru Nov 05 '21 at 01:59
  • Perhaps then I should emphasize the secondary question, since the answer to the first seems to be "no it's not possible" (not with the constraints I have here). So perhaps I add support for array-based opts to the script, and have it convert the old string from existing installations. Just need to know when an array variable has been defined and when it hasn't. – theory Nov 05 '21 at 02:28
  • 1
    Does that string come from somewhere outside the script, from a user who is expecting to be able give a string with quotes? Or what's the specific scenario? The script you linked to has ./configure --prefix=$PGENV_ROOT/pgsql-$v $PGENV_CONFIGURE_OPTS with a comment saying "need to keep a single string to pass to configure or will get an 'invalid package'". But I'm not exactly sure what that's supposed to do, since the unquoted expansion will not stay a single string. (and in any case, the configure scripts I've seen often take some arguments). – ilkkachu Nov 05 '21 at 07:02
  • If you want to emphasize the secondary question, then please include relevant, minimal snippets of the script in the question, and expected inputs and how you run the script, etc. – muru Nov 05 '21 at 07:44
  • @ilkkachu Yeah, that's how it was designed: a user puts configs in a separate file that gets loaded up by the script. I don't know what that comment is about, either. – theory Nov 05 '21 at 15:52
  • @theory, if that configuration file is read in as a shell script, then yeah, you can just have an array definition there instead. I was also going to suggest eval array=("$string") as a not-optimal way to turn the string into an array. Wven if it's doable assuming the users are trusted, it's not exactly pretty. But in the end that's just as problematic as the script running source "$conf" to evaluate code from another file directly. – ilkkachu Nov 05 '21 at 16:36
  • Yep. Super interesting reading the answers here, which all seem to boil down to "no, you cannot do that in bash without an array, so fix the damn script to use arrays. So that's what I've done. – theory Nov 05 '21 at 17:26

2 Answers2

4

In zsh, you'd do:

$ MYCONFIG="--hi FOO=bar 'X=ABC 123'"
$ printf '[%s]\n' ${(z)MYCONFIG}
[--hi]
[FOO=bar]
['X=ABC 123']
$ printf '[%s]\n' "${(Q@)${(z)MYCONFIG}}"
[--hi]
[FOO=bar]
[X=ABC 123]

Where the z parameter expansion flag breaks the input using shell tokenisation (note that it's according the zsh syntax, that z flag can also be tuned with the Z flag wrt comment or newline handling, see info zsh flags for details), and the Q peels one level of quoting.

In bash, you could always do:

readarray -td '' words < <(
  export MYCONFIG
  zsh -c 'print -rNC1 -- ${(z)MYCONFIG}'
)
printf '[%s]\n' "${words[@]}"

To get zsh to do that tokenisation and pass the words to bash NUL-delimited.

Adding the Q flag in that case could be problematic as if $MYCONFIG contains $'\0', that would add an extra NUL. In any case though, bash, contrary to zsh can't have NULs in the contents of its variables, so you might as well tell zsh to remove them:

readarray -td '' words < <(
  export MYCONFIG
  zsh -c '
    quoted_words=( ${(z)MYCONFIG} )
    unquoted_words=( "${(Q@)quoted_words}" )
    without_nul=( "${(@)unquoted_words//$'\''\0'\''}" )
    print -rNC1 -- "${without_nul}"
  '
)
  • I'm a zsh user myself, but this script is written in bash and assumes no other shell is present. Kind of love the idea of shelling out to zsh to work around this bash issue tho. – theory Nov 05 '21 at 15:55
0

With a little tweak (single quotes will have to be sacrificed):

xargs -n1 printf "[%s]\n" <<<"$MYCONFIG"
[--hi]
[FOO=bar]
[X=ABC 123]

The xargs utility is able to distinguish parameters with -n flag. Unfortunately, the -I flag does not allow splitting by unquoted blanks.

nezabudka
  • 2,428
  • 6
  • 15
  • 1
    Note that xargs handles quoting in a different way from shells (its handling is closer to the Mashey shell's from the late 70s which is not in use anymore these days). For instance, it would choke on "foo\"bar". – Stéphane Chazelas Nov 05 '21 at 06:32
  • 1
    You forgot the quotes around $MYCONFIG (and (, )) again. Same note as on @user499944's answer about the critical implications of using eval on potentially external data here. – Stéphane Chazelas Nov 05 '21 at 07:45
  • Super interesting reading the answers here, which all seem to boil down to "no, you cannot do that in bash without an array, so fix the damn script to use arrays. So that's what I've done. – theory Nov 05 '21 at 15:58