LJ Archive

Work the Shell

All about printf

Dave Taylor

Issue #264, April 2016

Dave describes a super-useful scripting command stolen from the C standard I/O library.

In my last article, I explored the surprising ability of the Linux shell to convert numeric bases on the fly, including this sweet little snippet that converts FF hexadecimal into decimal notation:

$ echo $(( 0xFF ))
255

And, I discussed how you even could use the handy printf command within scripts too, such as this command to display decimal numbers in octal and hexadecimal:

$ printf "octal: %o\nhex: %x\n" 42 42
octal: 52
hex: 2a

It's pretty neat stuff, but to be honest, I rarely find myself needing to convert numeric bases nowadays, so it's really something I file under “funky shell tricks”. Your experience may be different, so it's still well worth learning anyway.

In this article, I thought it would be interesting to take a closer look at the printf command, because it is so darn powerful, but before going there, here's a quickie: some neat ways you can make your if-then statements be more succinct.

If/Then Statements

If you're like me, then you find yourself frequently writing conditional statement blocks in your shell scripts. Um, I mean:

if [ you're like me ] ; then
   you find yourself...

Well, you get the idea. In fact, conditional expressions are where sequences of code turn into more sophisticated programs, whether they're a half-dozen lines long or hundreds of lines.

A typical conditional expression actually might look like this:

if [ $(date +%w) -eq 0 ]; then
   echo "It's Sunday"
else
   echo "It's not Sunday"
fi

This is clear and readable, but it sure takes up a lot of vertical space in a shell script.

Fortunately, there are some ways you can tighten up things by using the && and || notations in your shell scripts.

The && notation means if what's invoked prior to the && ends with a success return code, do what's subsequent—for example:


test $(date +%w) -eq 0 && echo "Sunday"

If it's Monday afternoon when I run this code, I'll get no output, and the echo statement isn't even evaluated. But if it's Sunday, the above command will output appropriately.

The || notation offers the same basic functionality but with the opposite logic: if the return code of the command prior to the || returns a fail (non-zero) return code, then the subsequent command will be invoked:

test $(date +%w) -eq 0 || echo "It's not Sunday yet"

You also can make this even more succinct by using the [] notational shortcut for a test—just remember to include the closing ] to ensure it's all well formed:

[ $(date +%w -eq 0 ] || echo "it's not Sunday yet"

The biggest limitation with this notation is that there's really no reliable and properly interpreted way to add an else clause.

You can try something like this:


cmd1 && cmd2 || cmd3

But because of precedence interpretation, it's likely to have cmd3 invoked if either cmd1 or cmd2 have a non-zero return code, which makes it functionality different from this:

if cmd1 ; then
  cmd2
else
  cmd3
fi

All is not lost, however, because you always can use a lot of semicolons to move that onto a single line:

if cmd1 ; then cmd2 ; else cmd3 ; fi

But, is it more readable? Is it really how you want to write your commands? Maybe. At least now you know!

The Ever-Helpful printf Command

Now, let's look at a completely different type of command, a command that is a built-in C programming language function that's so darn useful, it's now included in Linux as a standalone command.

In C and its brethren, the command shows up like this:

printf(formatstring, arg, arg);

This actually is a shortcut for the more general fprintf() command, which prepends the file handle and would look more like the following:

fprintf(stdio, formatstring, arg, arg);

It's not really relevant to this discussion, but hey, you should know this C programming nuance just so you know what's going on, right?

Okay, okay, back to the shell.

The printf command is basically the same, just without the parentheses and commas:

printf formatstring arg arg

Unlike the echo command, printf doesn't automatically append a carriage-return line-feed sequence, so you can end up with odd results like this:

$ printf "hello"
hello$

The format string allows a number of backslash-escaped sequences to alleviate this problem, notably \n to produce the end-of-line carriage return.

Indeed, go back to the first few paragraphs of this column, and you'll notice I included this sequence:

printf "octal: %o\nhex: %x\n" 42 42

Now you know what those \n sequences mean: each produces an end-of-line sequence.

Additional escape sequences include \a for a bell (try it!), \b for a backspace, \t for a tab and \\ for a backslash character itself.

Where things get more interesting is with the specifics of the format string. All of these are denoted with the % symbol followed by the specific letter that specifies how the associated argument should be interpreted and displayed. Give it a decimal value but use %o, and it'll be output as octal (as shown earlier).

The most important sequences are:

  • %c for a character.

  • %s for a string (a sequence of characters).

  • %d for a decimal value.

  • %f for a floating-point non-integer value.

There are nuances, of course, and in particular, displaying floating-point numbers can be quite complicated because of the various notational conventions used. You can read the printf man page for much more detail on that.

Just about every format sequence also allows you to specify a field width and a precision, which is where all of this gets both complicated and interesting.

Let's consider the floating-point number 3.141597 and how printf might display it in different ways:

$ pi=3.141597
$ printf "%d\n" $pi
-bash: printf: 3.141597: invalid number
0

That shouldn't be a surprise; you can't interpret a floating-point number as an integer. Use %f instead:

$ printf "%f\n" $pi
3.141597

That's the default, and printf is showing its default precision for the floating-point value.

Let's see what happens if you specify a zero precision (that is, zero digits subsequent to the decimal point):

$ printf "%.0f\n" $pi
3

That makes sense. But, what if it's actually currency you're working with and you want to be able to ensure that you don't get weird values like $20.4342434 as a value:

$ printf "%.2f\n" $pi
3.14

Where this really gets interesting is when you want to line up values in columns, allocating 10, 15, 20 or more characters of space per field. That's the field width, and it appears prior to the decimal point on the formatting string specifier or by itself if there's no decimal point:

$ printf "X%15fX\n" $pi
X       3.141597X

You can combine things too:

$ printf "X%10.2fX\n" $pi
X      3.14X

You also can use field width specifiers with strings, which is particularly interesting:

$ printf "|%20s|%20s|\n" "one" "two"; printf "|%20s|%20s|\n"
"three" "four"
|                 one|                 two|
|               three|                four|
$

I'm running out of space, but I encourage you to check out the printf command and its many tricks to help you create more attractive output from your shell scripts!

And don't forget, if you have an idea for a shell script I should tackle—or a game I should consider—please don't hesitate to send an e-mail to info@linuxjournal.com.

Dave Taylor has been hacking shell scripts since the dawn of the computer era. Well, not really, but still, 30 years is a long time! He's the author of the popular Wicked Cool Shell Scripts and Teach Yourself Unix in 24 Hours (new edition just released!). He can be found on Twitter as @DaveTaylor and at his tech site: www.AskDaveTaylor.com.

LJ Archive