Search code examples
bashvariablesvariable-expansion

Expand bash array with globbing and extra words


In a CI/CD job, I have a shell variable X defined. It contains one or more words, each of which might have glob operators:

X="foo bar* 'a b c'"

Suppose bar* matches 3 files, bard, bare, and barf. And the file a b c exists and has 2 spaces in it.

I want to create a Bash array Y with these contents:

Y=(--arg foo --arg bard --arg bare --arg barf --arg 'a b c')

In other words: do glob/quote expansion, then map each word w to --arg $w.

A clean solution would allow spaces in the expanded names (to be honest, I'm never going to have that situation in this CI/CD code - but I or someone else might easily copy/paste this technique somewhere else where it does matter).

It would also be awesome if I could do glob/quote expansion without invoking all other possible shell expansions (e.g. process substitution & subshells) - I'm having trouble thinking of how to do that, though.

The only solution I've come up with so far is to use unprotected expansion:

Y=()
for a in $X; do
    Y+=(--arg "$a")
done

Is that the best I can do? Is it safe? It works well on the foo and bar* cases, but not well on the a b c case.


Solution

  • To expand globs while honoring quotes (for grouping but not glob-suppression), but not expand variables or handle process substitutions or other dangerous syntax...

    X="foo bar* 'a b c'"
    
    IFS=
    args=( )
    while read -r -d '' word; do
      for item in $word; do
        args+=( --arg "$item" )
      done
    done < <(xargs printf '%s\0' <<<"$X")
    

    See this running in an online sandbox (where bard, bare and barf exist) at https://replit.com/@CharlesDuffy2/UnlawfulDecentApplicationserver#main.sh

    The use of xargs printf '%s\0' has xargs do the work of word-splitting in an almost POSIX-compliant way (if you want something completely POSIX-compliant, you need to use the Python shlex module instead -- other Q&A on the site demonstrates how), and the unquoted expansion with an empty IFS performs globbing only.