When authoring shell scripts, the echo command is used to output text. A rather little known cousin is the printf command, that offers C-style printf string formatting. The printf builtin is more versatile than echo, and I tend to use it as the situation calls for it. But I haven't noticed many instances of printf in shells scripts caught in the wild. I met with printf recently, and had chance to notice some of its nuances. I thought I'd leave some notes on that.
The syntax of the printf builtin shouldn't come as a surprise, as it's very close to its C relative: The first argument is the format string, and the following arguments are parameters to the format specifiers. The notable deviation is that arguments to printf are whitespace-separated as opposed to commas in C. Below is an example of printf builtin in action.
printf "%s" "Hello"
One immediate use case is to print neat table-style outputs, using field widths. The following could make a nice table header. Achieving similar effects with echo would be clumsy.
printf "%-20s%-10s%s\n" "Name" "Age" "Address"
The printf builtin output to the console by default. But we could easily save the formatted output to a variable using the -v flag, without resorting to shell redirection or the usual capture output operator $().
printf -v row "%-10s%20s" "column1" "column2"
And the variable row will have the formatted string.
One behaviour that I noticed is that the builtin doesn't strictly follow what C printf() does it with arguments. C printf hops ever each % parameter in the format specifier, and goes on formatting the supplied argument, until there are no more %s left in the format specifier. Any surplus arguments will be ignored. Bash's printf differs here, in that it keeps on applying the format specifier until no arguments are left! Consider this:
printf "%s " "Hello" "There"
Going by the C printf behaviour, one would expect it'd only print Hello. But it'll print the string Hello There instead — simply because it hops over all supplied arguments. This looping behaviour is only documented in the respective info pages (see bottom), rather than the quick reference (help printf), so one's most likely to be left puzzled, until they open up the info page!
The looping behaviour has a surprising benefit: it can be use to repeat strings! With a clever play on field-width specifiers, you can have repeat strings as long as you want. Take this for an example:
printf "=%.0s" {1..10}
This rather gibberish command prints 10 = characters on the console. The shell expression {1..10} expands to numbers from 1 to 10, thus forming 10 distinct arguments to printf. The format specifier tells the printf to format the argument as a zero-width string, which effectively prints a = followed by nothing! And because the format specifier is applied to all of the supplied 10 arguments, the final effect is to have 10 =s printed.
Note that the format specifier has to have at least one % in it for this looping to take place; otherwise printf won't loop all arguments (and it has no reason to). The printf just needs as many arguments passed as the number of =s required; so even {200..210} would have the same effect.
As mentioned above, printf comes handy to output table-style content by using field width specifiers. However if things get a little bit tricky when using control characters in the formatted content. For example:
printf "%-20s%s\n" "Normal" "text" printf "%-20s%s\n" "$(tput setaf 2)Green$(tput setaf 9)" "text"
The format specifiers being the same for both lines, one would expect both lines to appear aligned perfectly at the output, but they don't! Instead, the first field of the second line appear to have sucked rest of the line in. The reason for that is that, on the second line, there are 5 controls characters each used around the text Green: one to switch to green color, and the other to restore default. These 10 extra, invisible, characters are counted in when determining the filed width, but alas are consumed (would not appear) at the shell output, causing a vacuum. To counter that, we ought to put in 5 whitespace characters each:
printf "%-20s%s\n" "Normal" "text" printf "%-20s %s\n" "$(tput setaf 2)Green$(tput setaf 9)" "text"
Note that, as with echo, printf is both a shell builtin and a command. You'd end up using the builtin unless prefixed with either command or env. There are differences in their capabilities and operation. See help printf and info coreutils 'printf invocation' to refer to the respective documentations.