Which rule applies?
Just by trying:
So the outer quotes matter. Common shells behave in the same way: sh
, bash
, dash
, zsh
, … It's because of this general rule:
If the current character is <backslash>, single-quote, or double-quote and it is not quoted, it shall affect quoting for subsequent characters up to the end of the quoted text.
(source)
Only the outer quotes are not quoted, the rule applies to them. The inner quotes are quoted, they do not affect subsequent characters.
Are there any exceptions, quirks, surprises?
(Well, echo
may introduce few surprises, see Why is printf
better than echo
? Still, this is a separate issue. For readability the answer uses echo
with a safe value of $variable
).
The above code gives us either '123'
or "$variable"
. What if we need "123"
or '$variable'
?
To get "123"
:
variable=123
echo "\"$variable\""
# ^^ ^^ escaped inner quotes
or
variable=123
echo '"'"$variable"'"'
# 1 12 23 3 outer quotes (numbered pairs)
# ^ ^^^^^^^^^ ^ quoted fragments
To get '$variable'
:
variable=123
echo "'"'$variable'"'"
# 1 12 23 3 outer quotes (numbered pairs)
# ^ ^^^^^^^^^ ^ quoted fragments
The question notices that $
retains its special meaning in double-quotes. This is important:
The input characters within the quoted string that are also enclosed between $(
and the matching )
shall not be affected by the double-quotes, but rather shall define that command whose output replaces the $(…)
when the word is expanded.
(source)
It means quoting within $(…)
should be considered independently, despite the outer double-quotes. Indeed:
$ variable=123
$ echo "$(echo 'foo $variable')"
$ # ^ ^ single-quotes matter!
foo $variable
$
(Note echo $(stuff)
is bad code in general; it's here to illustrate the current issue).
In these two cases:
echo "'$variable'"
(tried way above)
echo "$(echo 'foo $variable')"
(discussed here)
there is $variable
inside single-quotes inside double-quotes, but $(…)
changes a lot. Similar situation with echo "`echo 'foo $variable'`"
(although $(…)
and backticks are not exactly equivalent).
Solution
Yes. My idea is to call a shell function (upon a keystroke) that will manipulate READLINE_LINE
using the ${variable@Q}
feature.
Relevant parts of documentation:
${parameter@operator}
The expansion is either a transformation of the value of parameter
or information about parameter
itself, depending on the value of operator
. Each operator
is a single letter:
Q
The expansion is a string that is the value of parameter
quoted in a format that can be reused as input.
(source)
READLINE_LINE
The contents of the Readline line buffer, for use with bind -x
[…].
(source)
The following works in Bash 4.4.20:
_quote_all() { READLINE_LINE="${READLINE_LINE@Q}"; }
bind -x '"\C-x\C-o":_quote_all'
To test the solution, prepare a command in the command line (do not execute), for example
d="$(LC_ALL=C date)"; printf 'It'\''s now %s\n' "$d"
(Quoting and the entire command could be simplified. It's deliberately like this. And you can execute it to make sure it's a valid command, but place it back in the command line before you proceed.)
Hit Ctrl+x,Ctrl+o and it will be properly quoted/escaped for our purpose. It will look like this:
'd="$(LC_ALL=C date)"; printf '\''It'\''\'\'''\''s now %s\n'\'' "$d"'
Now all you need to do is to add watch
(or ssh …
, or whatever) in front and execute. If it's watch
then note the header is like
Every 2.0s: d="$(LC_ALL=C date)"; printf 'It'\''s now %s\n' "$d"
It contains the original command. The command properly got to watch
, no part was interpreted prematurely.
Improvements
For convenience consider this variant:
_quote_all() { READLINE_LINE=" ${READLINE_LINE@Q}"; READLINE_POINT=0; }
It will prepare the line and place the cursor at the beginning, so you can type watch
right away. Or maybe even this variant (it deliberately goes under a different name, we're creating a separate binding for it):
_prepend_watch() { READLINE_LINE="watch ${READLINE_LINE@Q}"; READLINE_POINT=6; }
bind -x '"\C-x\C-w":_prepend_watch'
Now Ctrl+x,Ctrl+w handles quoting, inserts watch
automatically and places the cursor in the right position for you to type options.
With yet another function using READLINE_POINT
it's possible to handle the following scenario: type watch
(or ssh …
) followed by a command, where quoting/escaping is as if the command was going to be executed directly. Place the cursor where the command begins, hit the keystroke and let the function modify everything from the cursor to the end of the line. I'm not providing such function here; write it by yourself if you need it.
Yo Dawg, I heard you like quotes
You can stack the solution. I mean you can go from this
df -h | grep -v tmpfs
to this
watch 'df -h | grep -v tmpfs'
to this
ssh hostB -t 'watch '\''df -h | grep -v tmpfs'\'''
to this
ssh -t hostA 'ssh -t hostB '\''watch '\''\'\'''\''df -h | grep -v tmpfs'\''\'\'''\'''\'''
(yes, I know ssh -J
)
by only hitting Ctrl+x,Ctrl+o and appending one or few words in front in each step.
Best Answer