How can I single-quote or escape the whole command line in Bash conveniently?
Introduction
Consider a tool like watch
that can take another command (e.g. ls -l
) like this:
watch ls -l
# or equivalently
watch 'ls -l'
If the command is more complicated, it's all about quoting/escaping if things like $variable
, *
, |
, ;
or &&
are interpreted by the current shell or get to watch
. These two commands are different:
watch echo "$$"
watch 'echo "$$"'
So are these:
watch date ; echo done
watch date \; echo done
ssh
is similar, it can take one or more arguments and built a command to be run on the server. It depends on quoting/escaping if the local shell interprets $variable
, |
and such, or the remote shell.
And there are commands like sh -c
which require a single argument containing code, but the need for quoting/escaping is similar. In fact watch
(or ssh
) builds this single argument for sh -c
or similar command.
Problem
I already have the exact command I want to provide to watch
or ssh
, or similar tool. The command is obtained from history or pasted into the command line or typed as if it's going to be executed directly; it's in my command line verbatim. Nothing in the command should be expanded or interpreted before it gets to watch
or similar tool. In case of watch
it means all the expansion and interpretation should occur later, periodically. In case of ssh
it means it should occur on the server.
Now I need to do two things:
- Add
watch
(or whatever) at the beginning. This is not a problem. I can do this last. -
Quote and/or escape the original command. In general the command can include single-quotes, so just embracing with single-quotes blindly is not a solution. I think I know the right general solution:
- replace every
'
with'"'"'
or with'\''
, - embrace the whole resulting string with single-quotes;
but it's tedious and error-prone to do this by hand.
- replace every
Question
Can I make Bash properly add one level of single-quoting/escaping to the entire command line on demand?
(Note: the question arose when I was answering this one.)
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 aboutparameter
itself, depending on the value ofoperator
. Eachoperator
is a single letter:
Q
The expansion is a string that is the value ofparameter
quoted in a format that can be reused as input.
(source)
READLINE_LINE
The contents of the Readline line buffer, for use withbind -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.