pericmd 045: Customizing table output (1)

Data structures like array of arrays of strings (aoaos), hash, or array of hashes of strings (aohos) will render as tables under Perinci::CmdLine. There are some ways to customize this table output, either from outside the script or from inside the script.

Let’s revisit the list-files script that made an appearance some posts ago (pericmd 039):

#!/usr/bin/env perl

use 5.010;
use strict;
use warnings;

use Perinci::CmdLine::Any;

our %SPEC;

$SPEC{list_files} = {
    v => 1.1,
    args => {
        'verbose' => {
            cmdline_aliases => {v=>{}},
            schema => 'bool',
        },
        'all' => {
            cmdline_aliases => {a=>{}},
            schema => 'bool',
        },
    },
};
sub list_files {
    my %args = @_;
    my $verbose = $args{verbose};
    my $all     = $args{all};

    my @files;
    opendir my($dh), ".";
    for (sort readdir($dh)) {
        next if !$all && /\A\./;
        if ($verbose) {
            my $type = (-l $_) ? "l" : (-d $_) ? "d" : (-f _) ? "f" : "?";
            push @files, {name=>$_, size=>(-s _), type=>$type};
        } else {
            push @files, $_;
        }
    }

    [200, "OK", \@files];
}

my $app = Perinci::CmdLine::Any->new(url => '/main/list_files');
delete $app->common_opts->{verbose};
$app->common_opts->{version}{getopt} = 'version|V';
$app->run;

When we run this script:

% ./list-files -v --format json-pretty
[
   200,
   "OK",
   [
      {
         "name" : "hello",
         "size" : 1131,
         "type" : "f"
      },
      {
         "name" : "list-files",
         "size" : 988,
         "type" : "f"
      },
      {
         "name" : "list-files~",
         "size" : 989,
         "type" : "f"
      },
      {
         "name" : "mycomp",
         "size" : 902,
         "type" : "f"
      },
      {
         "name" : "mycomp2a",
         "size" : 608,
         "type" : "f"
      },
      {
         "name" : "mycomp2b",
         "size" : 686,
         "type" : "f"
      },
      {
         "name" : "mycomp2b+comp",
         "size" : 1394,
         "type" : "f"
      },
      {
         "name" : "pause",
         "size" : 4096,
         "type" : "d"
      },
      {
         "name" : "perl-App-hello",
         "size" : 4096,
         "type" : "d"
      }
   ],
   {}
]

%  ./list-files -v
+----------------+------+------+
| name           | size | type |
+----------------+------+------+
| hello          | 1131 | f    |
| list-files     | 988  | f    |
| list-files~    | 989  | f    |
| mycomp         | 902  | f    |
| mycomp2a       | 608  | f    |
| mycomp2b       | 686  | f    |
| mycomp2b+comp  | 1394 | f    |
| pause          | 4096 | d    |
| perl-App-hello | 4096 | d    |
+----------------+------+------+

Column order

We didn’t specify the ordering of columns, because our data is an array of hashes (instead of array of arrays). But in this case, the order happens to be the way we want (filename, then size and type). By default, the order is asciibetical. But if we modify the script and add another field links (for number of hardlinks):

#!/usr/bin/env perl

use 5.010;
use strict;
use warnings;

use Perinci::CmdLine::Any;

our %SPEC;

$SPEC{list_files} = {
    v => 1.1,
    args => {
        'verbose' => {
            cmdline_aliases => {v=>{}},
            schema => 'bool',
        },
        'all' => {
            cmdline_aliases => {a=>{}},
            schema => 'bool',
        },
    },
};
sub list_files {
    my %args = @_;
    my $verbose = $args{verbose};
    my $all     = $args{all};

    my @files;
    opendir my($dh), ".";
    for (sort readdir($dh)) {
        next if !$all && /\A\./;
        if ($verbose) {
            my $is_sym = (-l $_); # will do an lstat
            my @st = stat($_);
            my $type = $is_sym ? "l" : (-d _) ? "d" : (-f _) ? "f" : "?";
            push @files, {name=>$_, size=>(-s _), type=>$type, links=>$st[3]};
        } else {
            push @files, $_;
        }
    }

    [200, "OK", \@files];
}

my $app = Perinci::CmdLine::Any->new(url => '/main/list_files');
delete $app->common_opts->{verbose};
$app->common_opts->{version}{getopt} = 'version|V';
$app->run;

then the result will be:

% ./list-files -v
+-------+----------------+------+------+
| links | name           | size | type |
+-------+----------------+------+------+
| 1     | hello          | 1131 | f    |
| 1     | list-files     | 988  | f    |
| 1     | list-files2    | 1086 | f    |
| 1     | list-files2~   | 988  | f    |
| 1     | list-files~    | 989  | f    |
| 1     | mycomp         | 902  | f    |
| 1     | mycomp2a       | 608  | f    |
| 1     | mycomp2b       | 686  | f    |
| 1     | mycomp2b+comp  | 1394 | f    |
| 6     | pause          | 4096 | d    |
| 5     | perl-App-hello | 4096 | d    |
+-------+----------------+------+------+

What if we want the name column to stay as the leftmost? Here’s also where the result metadata comes in handy. From inside the script (function), we can embed this formatting hints when returning the enveloped result as follow:

[200, "OK", \@files, {
    format_options => {any => {table_column_orders=>[[qw/name type links size/]]}},
}];

OK, that’s a mouthful. What the code above does is add a key to the result metadata (the fourth element of the enveloped result array, a hash) called format_options. The value of this key is a hash of format names and format specifications. We’ll use any for the format name to apply to any format (but you actually can specify different formatting for text vs for json and so on).

The format specification is another hash containing a key called table_column_orders. This key has a value of array of arrays (to be able to specify multiple tables). One element of that array contains the list of columns for our table: [qw/name type links size/]. Since the output table’s columns match this entry, the order is followed.

Aside from inside the script itself, you can actually specify the ordering from an environment variable (outside the script). For example:

% FORMAT_PRETTY_TABLE_COLUMN_ORDERS='[["size","links","type","name"]]' ./list-files2 -v
+------+-------+------+----------------+
| size | links | type | name           |
+------+-------+------+----------------+
| 1131 | 1     | f    | hello          |
| 988  | 1     | f    | list-files     |
| 1187 | 1     | f    | list-files2    |
| 1086 | 1     | f    | list-files2~   |
| 989  | 1     | f    | list-files~    |
| 902  | 1     | f    | mycomp         |
| 608  | 1     | f    | mycomp2a       |
| 686  | 1     | f    | mycomp2b       |
| 1394 | 1     | f    | mycomp2b+comp  |
| 4096 | 6     | d    | pause          |
| 4096 | 5     | d    | perl-App-hello |
+------+-------+------+----------------+

The value of the environment variable is a JSON-encoded array of arrays, just like in table_column_orders format specification above.

If we use the Perinci::CmdLine::Classic backend (which renders tables using Text::ANSITable), there are a few other options available to customize the table. We’ll discuss this in another blog post.

Leave a comment