Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

bash variable expansion ${var:+"..."} in here-document removing double quotes?

I'm trying to understand why Bash removes double quotes (but not single quotes) when doing variable expansion with ${parameter:+word} (Use Alternate Value), in a here-document, for example:

% var=1
% cat <<EOF
> ${var:+"Hi there"}
> ${var:+'Bye'}
> EOF
Hi there
'Bye'

According to the manual, the "word" after :+ is processed with tilde expansion, parameter expansion, command substitution, and arithmetic expansion. None of these should do anything.

What am I missing? How can I get double quotes in the expansion?

like image 461
Andreas Luik Avatar asked Dec 06 '16 12:12

Andreas Luik


People also ask

How do I get rid of double quotes in bash?

Single quotes(') and backslash(\) are used to escape double quotes in bash shell script. We all know that inside single quotes, all special characters are ignored by the shell, so you can use double quotes inside it. You can also use a backslash to escape double quotes.

How do I remove a quote from a variable in bash?

A simple and elegant answer from Stripping single and double quotes in a string using bash / standard Linux commands only: BAR=$(eval echo $BAR) strips quotes from BAR . If you don't want anything printed out, you can pipe the evals to /dev/null 2>&1 .

Should you quote variables in bash?

General rule: quote it if it can either be empty or contain spaces (or any whitespace really) or special characters (wildcards). Not quoting strings with spaces often leads to the shell breaking apart a single argument into many.


3 Answers

tl;dr

$ var=1; cat <<EOF
"${var:+Hi there}"
${var:+$(printf %s '"Hi there"')}
EOF

"Hi there"
"Hi there"

The above demonstrates two pragmatic workarounds to include double quotes in the alternative value.
The embedded $(...) approach is more cumbersome, but more flexible: it allows inclusion of embedded double quotes and also gives you control over whether the value should be expanded or not.


Jens' helpful answer and Patryk Obara's helpful answer both shed light on and further demonstrate the problem.

Note that the problematic behavior equally applies to:

  • (as noted in the other answers) regular double-quoted strings (e.g., echo "${var:+"Hi there"}"; for the 1st workaround, you'd have to \-quote surrounding " instances; e.g., echo "\"${var:+Hi there}\""; curiously, as Gunstick points out in a comment on the question, using \" in the alternative value to produce " in the output does work in double-quoted strings - e.g., echo "${var:+\"Hi th\"ere\"}" - unlike in unquoted here-docs.)

  • related expansions ${var+...}, ${var-...} / ${var:-...}, and ${var=...} / ${var:=...}

  • Also, there's a related oddity with respect to \-handling inside double-quoted alternative values inside a double-quoted string / unquoted here-doc: bash and ksh unexpectedly remove embedded \ instances; e.g.,
    echo "${nosuch:-"a\b"}" unexpectedly yields ab, even though echo "a\b" in isolation yields a\b - see this question.

I have no explanation for the behavior[1] , but I can offer pragmatic solutions that work with all major POSIX-compatible shells (dash, bash, ksh, zsh):

Note that " instances are never needed for syntactic reasons inside the alternative value: The alternative value is implicitly treated like a double-quoted string: no tilde expansion, no word-splitting, and no globbing take place, but parameter expansions, arithmetic expansions and command substitutions are performed.

Note that in parameter expansions involving substitution or prefix/suffix-removal, quotes do have syntactic meaning; e.g.: echo "${BASH#*"bin"}" or echo "${BASH#*'bin'}" - curiously, dash doesn't support single quotes, though.

  • If you want to surround the entire alternative value with quotes, and it has no embedded quotes and you want it expanded,
    quote the entire expansion, which bypasses the problem of " removal from the alternative value:

    # Double quotes
    $ var=1; cat <<EOF
    "${var:+The closest * is far from   $HOME}"
    EOF
    "The closest * is far from   /Users/jdoe"
    
    # Single quotes - but note that the alternative value is STILL EXPANDED,
    # because of the overall context of the unquoted here-doc.
    var=1; cat <<EOF
    '${var:+The closest * is far from   $HOME}'
    EOF
    'The closest * is far from   /Users/jdoe'
    
  • For embedded quotes, or to prevent expansion of the alternative value,
    use an embedded command substitution (unquoted, although it'll behave as if it were quoted):

    # Expanded value with embedded quotes.
    var=1; cat <<EOF
    ${var:+$(printf %s "We got 3\" of snow at   $HOME")}
    EOF
    We got 3" of snow at   /Users/jdoe
    
    # Literal value with embedded quotes.
    var=1; cat <<EOF
    ${var:+$(printf %s 'We got 3" of snow at   $HOME')}
    EOF
    We got 3" of snow at   $HOME
    

These two approaches can be combined as needed.


[1] In effect, the alternative value:

  • behaves like an implicitly double-quoted string,
  • ' instances, as in regular double-quoted strings, are treated as literals.

Given the above,

  • it would make sense to treat embedded " instances as literals too, and simply pass them through, just like the ' instances.
    Instead, sadly, they are removed, and if you try to escape them as \", the \ is retained too (inside unquoted here-documents, but curiously not inside double-quoted strings), except in ksh - the laudable exception -, where the \ instances are removed. In zsh, curiously, trying to use \" breaks the expansion altogether (as do unbalanced unescaped ones in all shells).

    • More specifically, the double quotes have no syntactic function in the alternative value, but they are parsed as if they did: quote removal is applied, and you can't use (unbalanced) " instances in the interior without \"-escaping them (which, as stated, is useless, because the \ instances are retained).

Given the implicit double-quoted-string semantics, literal $ instances must either be \$-escaped, or a command substitution must be used to embed a single-quoted string ($(printf %s '...')).

like image 74
mklement0 Avatar answered Oct 18 '22 02:10

mklement0


The behavior looks deliberate--it is consistent across all Bourne shells I tried (e.g. ksh93 and zsh behave the same way).

The behavior is equivalent to treating the here-doc as double-quoted for these special expansions only. In other words, you get the same result for

$ echo "${var:+"hi there"}"
hi there
$ echo "${var:+'Bye'}"
'Bye'

There is only a very faint hint in the POSIX spec I found that something special happens for double quoted words in parameter expansions. This is from the informative "Examples" section of Parameter Expansion:

The double-quoting of patterns is different depending on where the double-quotes are placed.

"${x#*}"
The <asterisk> is a pattern character.
${x#"*"}
The literal <asterisk> is quoted and not special.

I would read the last line as suggesting that quote removal for double quotes applies to the word. This example would not make sense for single quotes, and by omission, there's no quote removal for single quotes.

Update

I tried the FreeBSD /bin/sh, which is derived from an Almquist Shell. This shell outputs single and double quotes. So the behavior is no longer consistent across all shells, only across most shells I tried.

As for getting double quotes in the expansion of the word after :+, my take is

$ var=1
$ q='"'
$ cat <<EOF
${var:+${q}hi there$q}
EOF
"hi there"
like image 5
Jens Avatar answered Oct 18 '22 03:10

Jens


$ cat <<EOF
${var:+bare alt value is string already}
${var:+'and these are quotes within string'}
${var:+"these are double quotes within string"}
${var:+"which are removed during substitution"}
"${var:+but you can simply not substitute them away ;)}"
EOF
bare alt value is string already
'and these are quotes within string'
these are double quotes within string
which are removed during substitution
"but you can simply not substitute them away ;)"

Note, that here-document is not needed to reproduce this:

$ echo "${var:+'foo'}"
'foo'
like image 3
Patryk Obara Avatar answered Oct 18 '22 01:10

Patryk Obara