Search code examples
bashquoting

How to quote nested sub-shell arguments correctly?


How to pass a string with spaces to a command which returns itself a string with spaces?

I tried the following four versions.

arg='one arg'

arg() { echo "arg: $1"; }

printf '1 |%s|\n' $(arg "$arg")
printf '2 |%s|\n' "$(arg $arg)"
printf '3 |%s|\n' "$(arg \"$arg\")"
printf '4 |%s|\n' "$(arg '$arg')"

They all fail:

1 |arg:|
1 |one|
1 |arg|
2 |arg: one|
3 |arg: "one|
4 |arg: $arg|

How to get this result?

? |arg: one arg|

Solution

  • The Syntax

    Using $() creates a new quoting context. Thus, double quotes inside a command substitution are completely independent of those outside it, and inside of closing the outer double quotes start a new and independent pair.

    arg='one arg'
    
    arg() { echo "arg: $1"; }
    
    printf '? |%s|\n' "$(arg "$arg")"
    

    ...properly emits:

    ? |arg: one arg|
    

    The Rationale (And History)

    With the above syntax, adding additional nesting layers is easy:

    printf '%s |%s|\n' "$(arg "$(arg "$arg")")"
    

    With the pre-POSIX backtick syntax instead of $(), your attempt #3 would have been correct:

    printf '3 |%s|\n' "`arg \"$arg\"`"
    

    However, needing to backslash-escape both quotes and nested backticks gets unworkable quickly as your nesting depth increases. Adding just one more nested arg makes it:

    printf '3a |%s|\n' "`arg \"\`arg \\\"$arg\\\"\`\"`"
    

    Adding two additional layers (thus, three arg function invocations total) is even worse, getting you into:

    printf '3b |%s|\n' "`arg \"\`arg \\\"\\\`arg \\\\\\\"$arg\\\\\\\"\\\`\\\"\`\"`"
    

    Whereas with the modern syntax, it's just:

    printf '3b |%s|\n' "$(arg "$(arg "$(arg "$arg")")")"
    

    Much, much easier.