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.


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 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.