Search code examples
macosbashshellescapingquotes

How do I keep single quotes intact when returning the output of a Bash subshell to the OSX "defaults" utility?


Context/Goal:

I'm trying to write an array to a key in a .plist file via the OSX defaults utility. My goal is to rewrite the array with contents of my choosing, not to modify its existing contents. The new contents are the product of another command, and are supposed to be single-quoted, space-separated strings in the shell, each one corresponding to an array element.

I have bash 3.2.57, and OSX 10.10.

Problem:

I can't seem to get bash's quote-literal expansion to work right. I've simplified the command I'm using via a subshell to an echo in these examples. The problematic behavior is the same.

For example, if I write the whole array like so:

/usr/bin/defaults write com.apple.systemuiserver menuExtras -array 'foo bar' 'baz quux'

I can read it out, like so:

/usr/bin/defaults read com.apple.systemuiserver menuExtras
(
    "foo bar",
    "baz quux"
)

The defaults utility adds the double-quotes.

That's fine. However, if I use a subshell to generate the array contents, things go wrong:

/usr/bin/defaults write com.apple.systemuiserver menuExtras -array $( echo -e 'foo bar' 'baz quux' )
/usr/bin/defaults read com.apple.systemuiserver menuExtras
(
    foo,
    bar,
    baz,
    quux
)

It detects the space in the string as delimiting the fields. Ok, I thought, maybe that's naïve; Bash will remove single quotes whenever it can, so I need to "hint" that it should keep them around.

I then tried every combination of literal-quote suggestions for Bash I could think of, with the same result. All of the below, if assigned to a variable and echoed, yield 'foo bar', with the quotes intact.

$( echo -e "'foo bar'" )
$( echo -e "\x27foo bar\x27" )
$( echo -e '\x27foo bar\x27' )
$( echo $'\'foo bar\'' )
$( echo \'foo bar\' )
$( echo \''foo bar'\' )
$( echo "'"'foo bar'"'" )
$( echo ''"'"'foo bar'"'"'' )

Question:

How can I get a series of single-quoted strings out of a subshell and into a bash command with their quotes intact, so that defaults can use it without messing up the field separator and splitting on spaces when it shouldn't?

The subshell will need to be able to return more than one quoted array element. I need to use the shell, and ideally just POSIX shell features (this is part of a larger script). I would prefer to avoid, if possible, hacks like storing things in a temp file or <(...) on-demand pipes. The strings also need to be single, not double, quoted, since their contents may at some point contain double quotes, and I don't want to have to fight with quote escaping twice.


Solution

  • Further messing around yielded the answer: xargs instead of a subshell:

    echo \'foo bar\' \'baz quux\' | xargs  /usr/bin/defaults write com.apple.systemuiserver menuExtras -array