1

This question is related to How do I print backslash followed by newline with printf?, where OP tries to print \\\n as single backslash followed by newline (not literal \n).

While by shell rules it makes sense that \\ would be expanded as \ and \n as n (i.e., shell performs backslash escape to preserve the literal form of the following character), when I perform strace it appears as if shell performs something entirely different and I'm struggling to interpret what I'm seeing.

$ strace -e execve printf "\\\n"
execve("/usr/bin/printf", ["printf", "\\\\n"], [/* 42 vars */]) = 0
\n+++ exited with 0 +++

In other words, what I am seeing is that instead of reduced number of characters that goes into argv portion of execve syscall, the number actually increases and there's appended an extra backslash.

Passing single quoted '\\\\n' is even more confusing:

$ strace -e execve printf '\\\\n'
execve("/usr/bin/printf", ["printf", "\\\\\\\\n"], [/* 42 vars */]) = 0
\\n+++ exited with 0 +++

In other words, I'd expect that here shell would pass everything unaltered to printf just as in execve() from previous command and get same output as printf "\\\n", but it's different.

To some extent I seem to circle around understanding that pure printf itself ( the one executed by system) would interpret the argument \\\n placed in argv portion of execve() as backslash and newline. Meanwhile, shell would need to convert \\\n typed in by user to match its own rules, but I am struggling to verbalize what exactly is happening with multiple backslashes.

4

strace displays the string in C string syntax, where a single backslash is shown as \\, a newline as \n, and so on.

What is passed to execve is what the puts function would print when given the string literal that strace prints as arguments in the C source code.

  • 1
    This is by far the more intelligible answer here. Terse and up to the point. I took the liberty to add a paragraph. +1 – Rui F Ribeiro Jan 23 '18 at 20:30
0

The shell is doing very little to your strings that are delimited with double quotes, since the only escaping that applies is the unescaping of \\ to \ before passing the argument to the invoked program (one of the few escaping rules that applies in double-quoted strings), and nothing at all to your strings that are delimited with single-quotes.

strace, on the other hand, is trying to write out something that looks roughly like C source code, albeit that this is not precisely what C language function calls look like, re-escaping any \s.

So:

  • In
    % strace -e execve printf '!\n'
    execve("/usr/bin/printf", ["printf", "!\\n"], [/* 35 vars */]) = 0
    !
    +++ exited with 0 +++
    %
    the argument passed to strace, and thence to printf, is exactly the three character long string !\n. strace is printing it as a C language string constant, where \ inside the string is doubled-up, yielding ".\\n". It is printf that is interpreting \n to mean a linefeed, of course.
  • In
    % strace -e execve printf '!\\\n'
    execve("/usr/bin/printf", ["printf", "!\\\\\\n"], [/* 35 vars */]) = 0
    !\
    +++ exited with 0 +++
    %
    exactly the same is happening, save that there are more \s to double up in the C language string and printf is recognizing \\ followed by \n.
  • In
    % strace -e execve printf '!\\\\n'
    execve("/usr/bin/printf", ["printf", "!\\\\\\\\n"], [/* 35 vars */]) = 0
    !\\n+++ exited with 0 +++
    %
    exactly the same is happening, save that there are more \s to double up in the C language string and printf is recognizing \\ followed by \\ followed by n.
  • In
    % strace -e execve printf \!"\\\\n"
    execve("/usr/bin/printf", ["printf", "!\\\\n"], [/* 35 vars */]) = 0
    !\n+++ exited with 0 +++
    %
    the shell is reducing the argument to !\\n because of the escaping rules for double-quoted words in shell language that reduce \\ and the \! quoting to prevent recognition of the history expansion character; the C language string is thus "!\\\\n"; and printf is seeing \\ followed by n.
  • In
    % strace -e execve printf \!$'\007'"\\\\n"
    execve("/usr/bin/printf", ["printf", "!\7\\\\n"], [/* 35 vars */]) = 0
    !\n+++ exited with 0 +++
    %
    much the same is happening, except that the C language string escaped form of the character looks nothing like the shell's, which is using a third form of quoting.
  • Quick question, what's your strace and bash version ? My output for strace -e execve printf "\!\\\\n" (4th example) doesn't match up with yours: I get execve("/usr/bin/printf", ["printf", "\\!\\\\n"], [/* 42 vars */]) = 0 , i.e. where you have ["printf", "!\\\\n"] I have ["printf", "\\!\\\\n"] – Sergiy Kolodyazhnyy Jan 23 '18 at 19:29
  • I was trying several ideas for examples, including different shells, and I copied the wrong one. Here's the right one. – JdeBP Jan 23 '18 at 20:27
  • What are those "ln"? – Rui F Ribeiro Jan 23 '18 at 20:32
  • 1
    @RuiFRibeiro It's supposed to mean "In the command blah...". Technically unnecessary, but makes for a complete sentence. – Sergiy Kolodyazhnyy Jan 23 '18 at 20:40
  • @SergiyKolodyazhnyy Thanks. I thought it was a typo. – Rui F Ribeiro Jan 23 '18 at 20:41
0

As mentioned by others, strace prints the system call arguments quoted in the same way special characters are quoted in C. A newline is represented as \n, a literal backslash as \\, a etc. (man page)

Character pointers are dereferenced and printed as C strings. Non-printing characters in strings are normally represented by ordinary C escape codes.

It's probably easier to use set -x to see what the shell sends to the running command. Bash puts the xtrace output in single-quotes, and backslashes don't take effect within them.

In your first example:

$ set -x
$ printf "\\\n" > /dev/null
+ printf '\\n'

The first backslash escapes the second, producing a single \. The third backslash doesn't escape anything, since it's followed by a letter that doesn't need escaping, so it's taken literally. And the letter is also taken literally, so we get \\n. C-quoted, the backslashes are doubled.

The characters that are escaped by a backslash inside double quotes is explicitly listed in the standard, they're the dollar sign $, backtick `, double-quote ", backslash itself, \, and newline.

The exclamation point is special due to history expansion, but if it's escaped, the backslash preceding it is not removed in Bash. Zsh however does remove it if history expansion is enabled.

  • Do you have any reference to read about C-quoted strings ? I mean, I know about $'\n' type of stuff, I've been coding in both C and shell for a while, and I've done the set -x part, too, I just didn't include that into my question, but I still don't get what's the purpose of strace representing \ as \\ ; it would make sense to escape \ when I type it in at shell prompt, but I don't get why strace needs to print it as such, at least to me it appears as there is no need unless strace output is expected to be read again by the shell in some way. – Sergiy Kolodyazhnyy Jan 23 '18 at 20:47
  • I know this sounds stupid, but ... I don't get it – Sergiy Kolodyazhnyy Jan 23 '18 at 20:48
  • @SergiyKolodyazhnyy, I think strace just uses C-style backslashing to make the listing unambiguous. Especially with newlines, you'll have lots and lots of them in system call arguments anyway. And when you have newline as \n, you'll need some way to represent an actual backslash. C-style escapes are common, so why not that. The output of strace isn't valid as input for the shell, though, since it uses double-quotes. (and not $'...') – ilkkachu Jan 23 '18 at 20:59
  • OK, so to quote this "backlash escape sequences are recognized and expanded to their literal equivalent ", so strace effectively does the opposite - it takes the literal and prints it as escape sequence. Is that right ? – Sergiy Kolodyazhnyy Jan 23 '18 at 21:11
  • @SergiyKolodyazhnyy, mm, yeah, sort of. I think they've mostly tried to give the output as strings you'd use in a C language program, not as something you'd give to the shell. But the backslash-escapes are the same in C and in $'...'. – ilkkachu Jan 23 '18 at 21:17
0

The number of backslash is indeed reduced by the shell (in one instance). but expressed as a double backslash in execve "C-quoting".

In double quotes (man dash):

Double Quotes Enclosing characters within double quotes preserves the literal meaning of all characters except dollarsign ($), backquote (`), and backslash (\). The backslash inside double quotes is historically weird, and serves to quote only the following characters:
$ ` " \ <newline>.
Otherwise it remains literal.

So, in the line:

$ strace -e execve printf "\\\n"

the shell changes the arguments a little bit, and this is what strace receive:

$ strace -e execve printf "\ \n"     # Space added for emphasis, not real.

Only the first backslash will quote the next backslash. Then, the quoting mechanism of execve, where the string is expressed as a "C-quoted" string will double up the number of backslash used, two become four:

$ execve("/usr/bin/printf", ["printf", "\\\\n"], [/* 42 vars */]) = 0

That is what is seen.

In single quotes (man dash):

Single Quotes
Enclosing characters in single quotes preserves the literal meaning of all the characters (except single quotes, making it impossible to put single-quotes in a single-quoted string).

So, strace receives something like this string:

strace -e execve printf '\ \ \ \ n'    #space(s) added for emphasis.

The four (4) backslash become eight (8) when "C-quoted" by execve:

execve("/usr/bin/printf", ["printf", "\\\\\\\\n"], [/* 42 vars */]) = 0

This match what is seen in your examples.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.