Escaping strings in Bash using !:q
til.simonwillison.net
til.simonwillison.net
Non interpolating quotes:
q/I wasn't surprised when he said "boo" and game me $5/;
q@I wasn't surprised when he said "boo" and game me $5@;
q!I wasn't surprised when he said "boo" and game me $5!;
q(I wasn't surprised when he said "boo" and game me $5);
q[I wasn't surprised when he said "boo" and game me $5];
Interpolating quotes: qq/Hello, $name!/;
qq!Hello, $name\!!;
qq@Hello, $name!@;
qq(Hello, $name!);
qq[Hello, $name!];
qq'Hello, $name!';
Rule of thumb, single q for single quote string (non-interpolating normally), and two q's for a double quote string (normally interpolates). s/replace #/with $/
s#replace /#with _# irb(main):001:1/ puts(%r
irb(main):002:1* a+
irb(main):003:0> .match? %q aaa )
true $ ruby -e 'a=5; puts "[#{a}]"'
[5]
$ ruby -e 'a=5; puts %q/[#{a}]/'
[#{a}] $ ruby -e 'a=5; puts %Q/[#{a}]/'
[5]
$ ruby -e 'a=5; puts %/[#{a}]/'
[5]And qw too:
# ['this', 'is', 'a', 'list']
qw/this is a list/; ['but', 'boy', 'am', 'I', 'glad', 'I', 'don\'t', 'have', 'to', 'read', 'stuff', 'like', 'that']
qw/but boy am I glad I don’t have to read stuff like that/
But why? split("but boy am I glad I don't have to read stuff like that")
But I guess you saved three bytes, so that's nice.You fail to explain why other languages do not grow these vestiges. I can only assume the implicit explanation is that you believe them to be incompetently designed, whereas I believe the exact complement: it looks like a good idea, but it isn’t.
Another quote: «Data types with core syntactic support are said to be "sugared types." Common examples include quote-delimited strings, curly braces for object and record types, and square brackets for Arrays.»
Does that actually mean they are bad ideas? No. It only means that particular language didn't copy that particular idea.
There may be bad ideas that are in many languages, that doesn't somehow make them good.
---
Quote-words are used regularly in Perl because they are clear and useful.
for (qw' alpha beta charlie ') {
say
}
for (split '', 'alpha beta charlie') {
say
}
for ( 'alpha', 'beta', 'charlie' ) {
say
}
The `qw` emphasizes that we are dealing with `alpha`, `beta`, and 'charlie`. The other ones have extra noise that is only there to satisfy the compiler.Now imagine you have to add `delta` to the list.
With the `qw` you only have to press the spacebar and the letters `d e l t a`. You don't have to worry if you accidently left off a `'`, because you didn't need to add one.
To be fair, the `split` would have the same benefit. But it is still more error-prone. For example I wonder how many people didn't notice that the first argument to `split` was an empty string when it should have been a string with one space in it.
I didn't even notice it, and I've been programming in Perl for decades.
For example, the ord() Perl function is evaluated at compile time if the argument is a constant string.
(It's not worth it because split() syntax isn't as clean as qw() for this purpose. split's first arg, if present, is the pattern to split on.)
Rust does too, to some degree: https://github.com/rust-lang/rust/pull/9674
%q@is@
%q!strongly!
%q(inspired)
%q[by]
%q<perl>
%Q~#{"interpolating quotes"}~
%&non interpolating by default&
R"/(I wasn't surprised when he said "boo" and game me $5)/"
R"@(I wasn't surprised when he said "boo" and game me $5)@"
R"|(I wasn't surprised when he said "boo" and game me $5)|"
https://en.cppreference.com/w/cpp/language/string_literal Q/I got $5 \sigh / # literal
Q:s/Hello $name/ # just scalars
Q:b/hello world\n/ # just backslash escape sequences
https://docs.raku.org/syntax/QSuppose you have an executable "queue".
you'd generally run it as
./queue
With this rule, wouldn't that be parsed as ./q'e'e
?Finding a reserved character (or even sequence) that doesn't have collisions or unintended behavior beyond what the semi-reserved ' and " can do will be hard (but not necessarily impossible :)! ).
@"
For help, type "get-help"
"@
https://docs.microsoft.com/en-us/powershell/module/microsoft...SILLY_VAR="
My silly username is "$USER"
"
echo ${SILLY_VAR}
Because editor tooling is important to me, I'm willing to see an ugly "\"quoted\" string" from time to time, if it means that I get better tools as a result.
Adding an additional :p modifier prevents the complaint about command not found:
$ # This string 'has single' "and double" quotes and a $
$ !:q
'# This string '\''has single'\'' "and double" quotes and a $'
# This string 'has single' "and double" quotes and a $: command not found
$
$ # This string 'has single' "and double" quotes and a $
$ !:q:p
'# This string '\''has single'\'' "and double" quotes and a $'
$ : !:q
The appeal (to me) would be less clutter in the output. #!/bin/bash
printf '%q' "$(cat)"
echo function bashquote () {
printf '%q' "$(cat)"
echo
} bashquote() { printf '%q\n' "$(cat)" ; } bashquote() { printf '%q\n' "$(</dev/stdin)"; }
Bash treats the string "/dev/stdin" as magic, so that works even if /dev/stdin doesn't exist. However, bash (unlike ksh) spawns a subshell for $(</dev/stdin), so using cat is actually lighter. (Also, it's not clear to me why $(<&0) doesn't work in bash.) $ echo "text with spaces" | bash -c 'printf "%q" "$(cat)"'; echo
text\ with\ spaces
For me, this escapes spaces with backslashes; the example in the article escapes them by quoting the whole string. Is this a difference in bash versions, or do these uses differ somehow?(I replaced the single quotes with double so i could pass this to bash -c, but i get the same result if i use your code verbatim as a script)
And the difference is not negligible. With quotes, i can type or paste more into the middle, and the string is still valid. With escapes, i have to be careful to escape the added text. It's significantly more ergonomic to use quoted text.
$'This string \'has single\' "and double" quotes and a $'
vs bash's %q, This\ string\ \'has\ single\'\ \"and\ double\"\ quotes\ and\ a\ \$Here's an example function for quoting from stdin:
function bashquotesingle() {
printf "'";
sed "s/'/'\\\\''/g";
printf "'";
}
Though it does have a bug, at least on macOS, where sed adds a trailing newline. (See https://stackoverflow.com/questions/13325138/why-does-sed-ad...) #!/bin/bash
printf '%q' "$(pbpaste)" | pbcopy # zsh using flags ${(flags)name}
% string="This is a string with \"\"\" and ''' and \"\" again ''. Also such stuff as & % # ;"
% echo $string
This is a string with """ and ''' and "" again ''. Also such stuff as & % # ;
% echo ${(q)string}
This\ is\ a\ string\ with\ \"\"\"\ and\ \'\'\'\ and\ \"\"\ again\ \'\'.\ Also\ such\ stuff\ as\ \&\ %\ \#\ \;
# bash using operators ${name@operator}
% string="This is a string with \"\"\" and ''' and \"\" again ''. Also such stuff as & % # ;"
% echo $string
This is a string with """ and ''' and "" again ''. Also such stuff as & % # ;
% echo ${string@Q}
'This is a string with """ and '\'''\'''\'' and "" again '\'''\''. Also such stuff as & % # ;'
Wow, so easy then, I remember struggling so many times in the past when e.g. iterating over filenames with unusual cahracters./edit: fix markup
$ echo $'hey there, it\'s "double quotes", \'single quotes\', and some \\, \', ", $ chars'
hey there, it's "double quotes", 'single quotes', and some \, ', ", $ chars find ... -print0 | while read -d $'\0' f; do ...; done find ... -print0 | while read -d '' f; do ...; done
And since Bash 4.4, also "readarray"/"mapfile" can specify a delimiter, which is great to read files safely into an array: readarray -d '' -a arr < <(find ... -print0)WARNING: this is not true in bash!
You can have exactly one null byte in a bash string: the terminating null byte. Try this:
echo $’foo\0bar’
It prints “foo”.So practically you can’t have null bytes in bash strings, as it will be mistaken for the terminating null of the underlying C string.
In your example read -d ‘’ would work just the same; actually that’s the idiomatic way to iterate on zero-delimited input (or xargs -0). Why does the empty string work? Because -d takes the first char of the string as the delimiter, which for empty C strings is the terminating \0 - this is how bugs become features.
quote-line (ESC-’) (unbound) (unbound)
Quote the current line; that is, put a ‘’’ character at the beginning and the end, and convert all ‘’’ characters to ‘’\’’’.
http://zsh.sourceforge.net/Doc/Release/Zsh-Line-Editor.html!:q<ESC>^
(<ESC>^ is Escape Shift-6 in my keyboard)
$ echo !:q
echo '# This string '\''has single'\'' "and double" quotes
and a $'
# This string 'has single' "and double" quotes and a $ $ # here be quote ' " chars
(use :p to print but not execute)
$ :!q:p
'# here be quote '\'' " chars'
(use :s to modify – no :q here because we modify the already quoted text !!, unquoted is now !-2)
$ !:s/be/are/:p
'# here are quote '\'' " chars'echo !:q > doagain.sh
Still, i can use ctur's script approach.
For instance, to produce a double quote inside single quotes, you can do this
echo "'"'"'"'"
That's three quoted strings next to each other that produce
'"'
echo "'"'"'"'"
'"'
[1] https://news.ycombinator.com/formatdoc: - Open command prompt.
q - Quit.
! - Yes, I'm sure.
% cat /tmp/sh
var=variables
x=$(
cat <<EOT
This string has 'single' and "double" quotes and can interpolate '$var'
EOT
)
echo $x
% bash /tmp/sh
This string has 'single' and "double" quotes and can interpolate 'variables' ## Cleanup $out
# only 1 of the next 4 'left trims' is expected to change $out:
out=${out#+++++ }
# if $FUNCNAME is eval'd three times there are five "+" chars
out=${out#++++ }
# if $FUNCNAME is eval'd twice there are four "+" chars
out=${out#+++ }
# if $FUNCNAME is eval'd there are three "+" chars
out=${out#++ }
# xtrace prepends the '++ '; ': ' is from our code shown above.
# We strip these 5 left most chars.
# Would be nice to support any level of evals, (hence any no.
# of '+' chars) [...]Type a command, but don't press enter, e.g.
echo 'hello';
Type !#:s/hello/world/
Press enter. !# means the whole current command so far, and the s/a/b/ modifier replaces the first instance of "a" with "b".Press up to get the command that ran:
echo 'hello'; echo 'world';
To see such history expansion things before they are run, press M-^ (probably Alt-Shift-6).