Program: words (Perl Cookbook, 2nd Edition)

## 4.19. Program: words

Have you ever wondered how programs like ls generate columns of sorted output that you read down the columns instead of across the rows? For example:

>     awk      cp       ed       login    mount    rmdir    sum
>     basename csh      egrep    ls       mt       sed      sync
>     cat      date     fgrep    mail     mv       sh       tar
>     chgrp    dd       grep     mkdir    ps       sort     touch
>     chmod    df       kill     mknod    pwd      stty     vi
>     chown    echo     ln       more     rm       su

Example 4-2 does this.

#### Example 4-2. words

>       #!/usr/bin/perl -w
>       # words - gather lines, present in columns

>       use strict;

>       my ($item, $cols, $rows, $maxlen);
>       my ($xpixel, $ypixel, $mask, @data);

>       getwinsize( );

>       # first gather up every line of input,
>       # remembering the longest line length seen
>       $maxlen = 1;
>       while (<>) {
>           my $mylen;
>           s/\s+$//;
>           $maxlen = $mylen if (($mylen = length) > $maxlen);
>           push(@data, $_);
>       }

>       $maxlen += 1;               # to make extra space

>       # determine boundaries of screen
>       $cols = int($cols / $maxlen) || 1;
>       $rows = int(($#data+$cols) / $cols);

>       # pre-create mask for faster computation
>       $mask = sprintf("%%-%ds ", $maxlen-1);

>       # subroutine to check whether at last item on line
>       sub EOL { ($item+1) % $cols =  = 0 }

>       # now process each item, picking out proper piece for this position
>       for ($item = 0; $item < $rows * $cols; $item++) {
>           my $target =  ($item % $cols) * $rows + int($item/$cols);
>           my $piece = sprintf($mask, $target < @data ? $data[$target] : "");
>           $piece =~ s/\s+$// if EOL( );  # don't blank-pad to EOL
>           print $piece;
>           print "\n" if EOL( );
>       }

>       # finish up if needed
>       print "\n" if EOL( );

>       # not portable -- linux only
>       sub getwinsize {
>           my $winsize = "\0" x 8;
>           my $TIOCGWINSZ = 0x40087468;
>           if (ioctl(STDOUT, $TIOCGWINSZ, $winsize)) {
>               ($rows, $cols, $xpixel, $ypixel) = unpack('S4', $winsize);
>           } else {
>               $cols = 80;
>           }
>       }

The most obvious way to print out a sorted list in columns is to print each element of the list, one at a time, padded out to a particular width. Then when you're about to hit the end of the line, generate a newline. But that only works if you're planning on reading each row from left to right. If you instead expect to read it down each column, this approach won't do.

The words program is a filter that generates output going down the columns. It reads all input, keeping track of the length of the longest line seen. Once everything has been read in, it divides the screen width by the length of the longest input record seen, yielding the expected number of columns.

Then the program goes into a loop that executes once per input record, but the output order isn't in the obvious order. Imagine you had a list of nine items:

>     Wrong       Right
>     -----       -----
>     1 2 3       1 4 7
>     4 5 6       2 5 8
>     7 8 9       3 6 9

The words program does the necessary calculations to print out elements `(1,4,7)` on one line, `(2,5,8)` on the next, and `(3,6,9)` on the last.

To figure out the current window size, this program does an `ioctl` call. This works fine—on the system it was written for. On any other system, it won't work. If that's good enough for you, then good for you. Recipe 12.17 shows how to find this on your system using the ioctl.ph file, or with a C program. Recipe 15.4 shows a more portable solution, but that requires installing a CPAN module.

### 4.19.1. See Also

Recipe 15.4

Copyright © 2003 O'Reilly & Associates. All rights reserved.