2

I was looking for a way to escape a variable containing format specifiers and special characters like quotes, backslashes and line breaks so that when passing it to print -P it'll print out literally.

So essentially I want these two to print the same:

> cat file.txt
> my_var="$(cat file.txt)"
> print -P "${<magic>my_var}"

A good example file for test cases I used is this:

Backslash \
Double Backslash \\

Single Quote '
Double Quote "

-----------------------
Escaped Linebreak \n
-----------------------

Color codes: %F{red}not red%f

Variable expansion $SHELL

Closest I got is ${${(q+)my_var}//\%/%%} though that has issues with quotes, linebreaks, backslashes and variable expansion:

$'Backslash Double Backslash \

Single Quote '
Double Quote "

-----------------------
Escaped Linebreak \n
-----------------------

Color codes: %F{red}not red%f

Variable expansion /usr/bin/zsh'

I am aware of printf '%s\n' "$my_var". However in practice there's a lot of actual formatting going on in the print -P around the variable, so this isn't useful to me. Using print -P around the variable and printf for the actual variable sadly also doesn't work as there are cases where string manipulation is applied to the contents of the variable.

2 Answers 2

2

${(q)…} and its variants use '…', "…" or $'…' quoting, but print -P doesn't do any quote expansion, only backslash expansion and prompt expansion (which is percent-escape expansion, plus substitutions if prompt_subst is enabled). So I don't think ${(q)…} can help here.

With prompt_subst disabled, you need to protect backslashes and percent signs. With prompt_subst enabled, you also need to protect dollars and backquotes.

if [[ -o prompt_subst ]]; then
  print -P -- "${${${${my_var//\\/\\\\\\\\}//\%/%%}//\$/\\\\\$}//\`/\\\\\`}"
else
  print -P -- "${${my_var//\\/\\\\}//\%/%%}"
fi
4
  • Could it be that you forgot the variable in the upper case?
    – BrainStone
    Commented Feb 4, 2022 at 4:54
  • If you make it print -rP, you don't need to escape backslashes. Commented Feb 4, 2022 at 6:54
  • If prompt_bang is enabled, you'll need to escape !. Commented Feb 4, 2022 at 8:37
  • With current versions of zsh, you'd also need to do the substitutions under set +o multibyte as otherwise you'd still get a command injection vulnerability with promptsubst in some locales. For instance, on my system where the zh_HK uses the BIG5-HKSCS charset (and where the encoding of ε is $'\xa3\x60'), my_var=$'\xa3`reboot;#\xa8`' LC_ALL=zh_HK zsh -o promptsubst ./that-code reboots. Commented Feb 6, 2022 at 7:52
1

If the aim is to print some strings, part of which must undergo prompt expansion, or \x expansion, part of which don't, rather than doing:

print -P -- $string_to_undergo_both \
            ${string_to_undergo_prompt//\\/\\\\} \
            ${string_to_undergo_backslash//complex-brittle-expression} \
            ${string_to_undergo_none//complex-brittle-expression} \

You could break it down to:

print  -P -n -- $string_to_undergo_both' '
print -rP -n -- $string_to_undergo_prompt' '
print     -n -- $string_to_undergo_backslash' '
print -r     -- $string_to_undergo_none

(beware that with print -rP, if the promptsubst option is enabled, \ is still special as it's used to escape the $, ` and itself, so print -rP '\\' would then output \ instead of \\).

Or call print -r -- which prints raw (disables \x expansion which is on by default in print for compatibility with the Korn shell), but use parameter expansion flags to enable prompt expansion or backslash expansion where needed:

print -r -- ${(g[o]%)string_to_undergo_both} \
            ${(%)string_to_undergo_prompt} \
            ${(g[o])string_to_undergo_backslash} \
            $string_to_undergo_none

You can also enable those for literal text (not in variables), by using ${(flags):-literal-text} (${(%):-%F{red}} for instance for prompt expansion), or the $'...' form of quotes for backslash expansion.

See also the %% parameter expansion flag for full prompt expansion (including the applying of the dangerous promptsubst when enabled), and e to perform parameter expansions, command substitutions and arithmetic expansions.

With printf, backslash expansion is performed in the format argument and in arguments for the %b format directive (but for the latter in the echo style, not the normal C style). There is no format directive for prompt expansion though.

With prompt expansion however, you can also pass verbatim text (though beware the non-printable characters such as control ones are converted to some visual representation (\n for newline, ^[ for ESC, \ufffe for U+FFFE for instance) with the $psvar array.

$ psvar=( '\\' %% )
$ print -P 'first is %F{red}%1v%f, second is %F{green}%2v%f'
first is \\, second is %%

So, you could do some sort of prompt-expansion-enabled printf with something like:

pprintf() {
  local psvar=("${(@)argv[2,-1]}")
  print -P -- "$1"
}
$ pprintf 'cwd: %~, host: %m, arbitrary printable string: "%1v"' '\\%%'
cwd: ~, host: myhost, arbitrary printable string: "\\%%"

(here also doing backslash expansion, add -r to disable it).

4
  • Not quite applicable to my situation, but I like that approach!
    – BrainStone
    Commented Feb 5, 2022 at 23:38
  • I also find it very curious that line breaks are printed as \n with the psvar method.
    – BrainStone
    Commented Feb 6, 2022 at 1:40
  • Oh, I hadn't realised that. It look like non-printable characters are converted to some visual representations (U+FFFE to \ufffe, ESC to ^[...). Commented Feb 6, 2022 at 7:13
  • @BrainStone, it does actually make sense: those are meant to be used in your prompt, so if control characters were sent asis, it would mess up the line editor display. Commented Feb 6, 2022 at 7:20

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .