views:

115

answers:

4

I discovered this while using ruby printf, but it also applies to C's printf.

If you include ANSI colour escape codes in an output string, it messes up the alignment.

Ruby:

ruby-1.9.2-head > printf "%20s\n%20s\n", "\033[32mGreen\033[0m", "Green"
      Green          # 6 spaces to the left of this one
               Green # correctly padded to 20 chars
 => nil

The same line in a C program produces the same output.

Is there anyway to get printf (or something else) to align output and not add spaces for non-printed characters?

Is this is a bug, or is there a good reason for it?

Thanks!

Update: Since printf can't be relied upon to align data when there's ANSI codes and wide chars, is there a best practice way of lining up coloured tabular data in the console in ruby?

+3  A: 

It's not a bug: there's no way ruby should know (at least within printf, it would be a different story for something like curses) that its stdout is going to a terminal that understands VT100 escape sequences.

If you're not adjusting background colours, something like this might be a better idea:

GREEN = "\033[32m"
NORMAL = "\033[0m"
printf "%s%20s%s\n", GREEN, "Green", NORMAL
Jack Kelly
I'm not sure I follow your answer -- the terminal understands the escape sequences, since the text comes out green. It just mysteriously adds 9 spaces (which I don't get -- there are only 7 chars in the escape sequence.)
Stewart Johnson
@Stewart, the _terminal_ may understand them but it's not the terminal doing the formatting. The only thing that `printf` knows is characters, not escape sequences. You're working at two different levels of abstraction.
paxdiablo
Jack, I'd even go as far as to do `#define green "\033[32m"` (or whatever the ruby equivalent was) and use `green` directly in the `printf`: `printf "%s%20s%s\n", green, "Green", normal`. That would make the code much more readable.
paxdiablo
I get what you're saying now. What I don't get is how printf is translating 7 chars of escape sequence into 9 space chars.
Stewart Johnson
@Stewart, there are 9 characters in the escape sequences: \033[32m is 5, \033[0m is 4.
Mark Tolonen
So it puts all the escape sequence chars *after* the word Green even though it's in the middle? That doesn't seem right.
Stewart Johnson
@Jack, @paxdiablo: there's a ruby gem called rainbow that allows you to define strings with colours like this: "foo".foreground(:red).background(:white). It inserts the appropriate ANSI colour codes around the string, so it has the same problems when using printf.
Stewart Johnson
@paxdiablo: Sure.@Stewart: No. It's not doing that at all. It's putting in the sequence for green, then the string (with spacing), then the sequence for normal. R.. is correct, `printf` is simply not the correct tool for the job. I'm not sure what is.
Jack Kelly
@Stewart Johnson: `printf` just sees a 14 character sequence that you want to print in a 20 character field, so it puts out 6 spaces and then the string. It just doesn't realise that the escape sequences will be "invisible". It also doesn't matter where in the string the escape sequences are - beginning, middle or end, the effect will be the same.
caf
+3  A: 

printf field width specifiers are not useful for aligning tabular data, interface elements, etc. Aside from the issue of control characters which you have already discovered, there are also nonspacing and double-width characters which your program will have to deal with if you don't want to limit things to legacy character encodings (which many users consider deprecated).

If you insist on using printf this way, you probably need to do something like:

printf("%*s\n%*s\n", bytestopad("\033[32mGreen\033[0m", 20), "\033[32mGreen\033[0m", bytestopad("Green", 20), "Green");

where bytestopad(s,n) is a function you write that computes how many total bytes are needed (string plus padding spaces) to result in the string s taking up n terminal columns. This would involve parsing escapes and processing multibyte characters and using a facility (like the POSIX wcwidth function) to lookup how many terminal columns each takes. Note the use of * in place of a constant field width in the printf format string. This allows you to pass an int argument to printf for runtime-variable field widths.

R..
Thanks - so what is a the best practice in lining up tabular data in the console in ruby?
Stewart Johnson
+1 for the `*` as a field width. I didn't know about that.
Jack Kelly
FWIW, you can also use the `*` in place of precision, e.g. `"%.*s"` to truncate a string to at most the number of bytes specified by the argument, or `%.*f` for runtime-configurable floating point precision. And you can use both, as in `%*.*f`, in which case the width argument is passed first.
R..
+3  A: 

I disagree with your characterization of '9 spaces after the green Green'. I use Perl rather than Ruby, but if I use a modification of your statement, printing a pipe symbol after the string, I get:

perl -e 'printf "%20s|\n%20s|\n", "\033[32mGreen\033[0m", "Green";'
      Green|
               Green|

This shows to me that the printf() statement counted 14 characters in the string, so it prepended 6 spaces to produce 20 characters right-aligned. However, the terminal swallowed 9 of those characters, interpreting them as colour changes. So, the output appeared 9 characters shorter than you wanted it to. However, the printf() did not print 9 blanks after the first 'Green'.


Regarding the best practices for aligned output (with colourization), I think you'll need to have each sized-and-aligned field surrounded by simple '%s' fields which deal with the colourization:

printf "%s%20.20s%s|%s%-10d%s|%s%12.12s%s|\n",
       co_green, column_1_data, co_plain,
       co_blue,  column_2_data, co_plain,
       co_red,   column_3_data, co_plain;

Where, obviously, the co_XXXX variables (constants?) contain the escape sequences to switch to the named colour (and co_plain might be better as co_black). If it turns out that you don't need colourization on some field, you can use the empty string in place of the co_XXXX variables (or call it co_empty).

Jonathan Leffler
Well spotted. I can see what's going on now thanks to @caf's explanation.
Stewart Johnson
+2  A: 

I would separate out any escape sequences from actual text to avoid the whole matter.

# in Ruby
printf "%s%20s\n%s%20s\n", "\033[32m", "Green", "\033[0m", "Green"

or

/* In C */
printf("%s%20s\n%s%20s\n", "\033[32m", "Green", "\033[0m", "Green");

Since ANSI escape sequences are not part of either Ruby or C neither thinks that they need to treat these characters special, and rightfully so.

If you are going to be doing a lot of terminal color stuff then you should look into curses and ncurses which provide functions to do color changes that work for many different types of terminals. They also provide much much more functionality, like text based windows, function keys, and sometimes even mouse interaction.

nategoose
You have an extra quote in your C example and it's making the syntax highlighting go weird.
Jack Kelly
@Jack Kelly: Fixed. Thanks
nategoose