9
$ printf "hi"
hi$ printf "hi\n"
hi
$ printf "hi\\n"
hi

Why doesn't the last line print hi\n?

Adam
  • 257
  • So this is undefined behaviour? – Adam Jan 30 '20 at 20:38
  • That's odd. I though when enclosed in single quotes everything took on a literal value. I would have expected printf 'hi\n' to print hi\n – Adam Jan 30 '20 at 20:42
  • 2
    @jesse_b Other characters can be used in the format string as long as it is constant. Assuming that this is based off of the behavior of C's printf (which is almost surely true), printf "hi\\n" is perfectly valid. – S.S. Anne Jan 30 '20 at 20:45
  • 1
    @jesse_b POSIX spec does not say something about anything similar to printf "hi\\n" being invalid. – S.S. Anne Jan 30 '20 at 20:50
  • 6
    @jesse_b printf with a single argument is perfecty valid. The format string can contain other characters and does not need to include any format specifiers. The OP's problem has to do with how the argument is escaped by the shell, not with the way printf is being used. – Grodriguez Jan 31 '20 at 09:17
  • @Grodriguez: Technically correct but if OP just used printf correctly there would be no issue ;). There is also technically nothing wrong with using unquotted variables, parsing ls, and doing rm -rf * (in a perfect world), but if you want to avoid unexpected behavior you should just do things properly every time. – jesse_b Jan 31 '20 at 13:58

3 Answers3

32

This is nothing to do with printf, and everything to do with the argument that you have given to printf.

In a double-quoted string, the shell turns \\ into \. So the argument that you have given to printf is actually hi\n, which of course printf then performs its own escape sequence processing on.

In a double-quoted string, the escaping done through \ by the shell is specifically limited to affecting the ␊, \, `, $, and " characters. You will find that \n gets passed to printf as-is. So the argument that you have given to printf is actually hi\n again.

Be careful about putting escape sequences into the format string for printf. Only some have defined meanings in the Single Unix Specification. \n is defined, but \c is actually not, for example.

Further reading

JdeBP
  • 68,745
  • 2
    In other words, you need to double-escape your backlashes because they're going through two rounds of interpolation, the shell's and then printf's. printf "hi\\\\n" will print the single literal backslash. – BallpointBen Jan 31 '20 at 17:02
  • 3
    It's usually simplest to put the format string in single quotes. That prevents all escape character processing by the shell, and leaves it to printf. The exception would be if you want to have a literal single quote in the format string. – Barmar Jan 31 '20 at 19:55
  • 2
    @BallpointBen It's enough with 3 backslashes as the shell would leave the third untouched. To the shell, \n is the same as \\n, so \\\n is the same as \\\\n. Honestly, it's better to just use single-quotes to avoid this shell behaviour. – JoL Jan 31 '20 at 21:19
  • 1
    @ilkkachu It seems it does: You will find that \n gets passed to printf as-is. –  Feb 01 '20 at 17:08
  • @JdeBP The sequence \c is defined (in POSIX point 7) when used with the %b conversion specifier. –  Feb 01 '20 at 17:14
  • 1
    In bash and zsh and when csh-style history substitution is enabled, backslash within double quotes also escapes ! (or whatever the first character of $histchars is), but in the case of bash, the backslash is not removed. (echo -E "\!foo" still outputs \!foo in bash even though backslash served its purpose at disabling history substitution) – Stéphane Chazelas Feb 01 '20 at 18:05
  • @Isaac, ah right, it's just unclearly written. – ilkkachu Feb 01 '20 at 19:26
17

Within double quotes, \\n is an escaped (quoted) backslash followed by a n. This is given to printf as \n and printf will output a newline.

Within double quotes (still), \n is the string \n. Again, printf receives a \n string and will print a newline.

Within double quotes, backslash is special only when preceding another backslash, a newline, or any of $, ` or ". "Special" means that it removes the special meaning of the next character. If a backslash precedes any other character (n for example), then it's just a backslash character.

This is explained in the POSIX standard.

To print \n in a printf format string, use printf '\\n' or printf "\\\\n", or use printf '%s' '\n'

In general, the printf format string should be single quoted and any variable data should be given as additional arguments to be inserted into the format string:

printf 'This is how you write a newline: %s\n' '\n'
Kusalananda
  • 333,661
1

Ok, lets add another point of view.

There are two levels of interpretation here at play. One is the shell, the other is the command (in this case printf) interpretation of the arguments received.

Inside double quotes The shell will leave alone most of the sequences backslash-character, this is the common result:

$ printf '%s\n'    "\a \b \c \d ... \z     \$ \` \\ "
\a \b \c \d ... \z     $ ` \

Except with $, `, and \ which, being especial to the shell, get their \ removed.

So, testing both the strings you used (and others), we get:

$ printf '%s\n'     "hi\n"     "hi\\n"    "hi\\\n"    "hi\\\\n"    "hi\\\\\n"
hi\n
hi\n
hi\\n
hi\\n
hi\\\n

The shell converts pairs of \\ to one \. And leaves alone \n as \n.

Now, printf has an special relation to the first argument, it is explicitly set to be the format. In the format argument, some characters are special (to printf), for example: valid sequences that start with a % character and some sequences of backslash-character like:

\\  \a  \b  \f  \n  \r  \t  \v  and the special \ddd

So, the string \n generates a newline but a \\n don't:

$ printf "    hi\n    hi\\n    hi\\\n   hi\\\\n"; echo
hi
hi
hi\n   hi\n