Menu

Category Archives: pericmd-tut

Post navigation

The latest release of Perinci::CmdLine (1.68) supports viewing program’s output in an external program. And also a new output format is introduced: html+datatables. This will show your program’s output in a browser and table data is shown as HTML table using jQuery and DataTables plugin to allow you to filter rows or sort columns. Here’s a video demonstration:

Your browser does not support the video tag, or WordPress filters the VIDEO element.

In Rinci, function can express in its metadata that it supports various features or options. These feature-/option-related information will later be passed back to the function during function call in the form of special arguments. These arguments are prefixed with “-” (dash) with predefined names and values, and will only be passed if the function already expresses the support, and if the function accepts named arguments (as hash or hashref).

There are several such special arguments, one that I will cover today is -dry_run.

A function can express that it supports dry-run (simulation) mode, via the dry_run feature inside the features property in the Rinci function metadata:

The special argument -dry_run need not be declared in the args property. It will automatically be passed when program is run in dry-run mode.

In Perinci::CdmLine, a common command-line option --dry-run will automatically be added if function supports dry_run feature. This means, if user passes --dry-run (or, alternatively, setting DRY_RUN environment variable to true), Perinci::CmdLine will call the function with -dry_run => 1.

If function is passed -dry_run => 1 in the arguments, it should perform the operation but without actually doing it. Lots of programs have this feature, like rsync, make, or svn merge (note: git merge also supports dry-run operation but with options named --no-commit --no-ff instead of --dry-run. They are useful for testing/trial, especially when the associated operation is rather dangerous (like deleting stuffs or sending mass email).

We could, of course, manually define a dry_run argument ourselves. But the advantage of specifying the dry_run feature instead is, aside from standardization and automatic addition of –dry-run and DRY_RUN parsing, is that in transactions, the dry-run functions can have special treatment. We will cover transaction in the future.

Continuing from previous post, if we use Perinci::CmdLine::Classic as a backend, there are a few other options to customize table output. Let’s use the same list-files script, but use the classic backend:

By default, Text::ANSITable colors columns differently according to data type. The second column, since it contains only numbers and thus is a numeric column, is colored cyan by default. While string columns are colored light grey by default.

Of course, like the lite backend, the classic backend supports reordering columns:

The mentioned list-files3 is exactly the same as list-files2 except that it adds a column modified containing mtime Unix timestamp of file. By default will be shown as a number (cyan), but with the above FORMAT_PRETTY_TABLE_COLUMN_TYPES hint the column is shown as a date (yellow).

Note that there is some heuristics employed, so if you name the column “mtime” or “something_date”, you don’t have to give any hint to show the column as date.

There is also FORMAT_PRETTY_TABLE_COLUMN_FORMATS to apply some formatting to columns, for example:

With Text::ANSITable you can also customize cell padding/spacing, column widths, or alignments. You can hide some columns/rows, repeat some columns/rows, or even do conditional styles involving Perl code. For more available options, refer to the POD.

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):

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):

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:

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:

The functions we use as backend of our CLI application return pure data structure, and Perinci::CmdLine’s formatter figures out how to best display this information. There are, however, some ways to customize how the output looks in our CLI application by setting some attributes in the result metadata.

As you might remember, result metadata is the fourth element in the enveloped result structure:

[$status, $message, $actual_result, $meta]

The result metadata is a hash (a DefHash actually, but for most purposes you don’t care about the difference). There are some attributes (keys) you can set in this metadata to give hints to Perinci::CmdLine on how to render the result in CLI application.

cmdline.result

The first one is cmdline.result. This sets alternative result to use when in CLI context. For example:

sub func {
[200, "OK", "foo", {'cmdline.result'=>'bar'}];
}

This way, if you are calling the function, you’ll get “foo” (in the third element), but if this function is run on the command-line, user will see “bar”.

Why would this be useful? An example would be functions that return bool values, like for example user_exists(). In Perl, we probably will only care about getting 1/0. But in CLI, you might want to display a more user-friendly message. So instead of:

Another example where this is applied is in
Git::Bunch. In function check_bunch, the result is a hash of every repo in the bunch and their check statuses, e.g.:

[200, "OK", {repo1=>[200,"clean"], repo2=>[500,"Needs commit"], ...}]

The function also happens to use progress bar to report unclean repositories as the checking is being done. Unclean repos get reported/logged to the screen. Thus, it is not very useful to display this hash on the CLI (but useful when we are using the function from Perl). So check_bunch() sets the CLI output to empty string:

[200, "OK", ..., {'cmdline.result'=>''}]

cmdline.default_format

This attribute picks the default format. For example:

[200, "OK", ..., {'cmdline.default_format'=>'json'}]

This way, when CLI is run, the output defaults to JSON instead of text, unless user explicitly specify the output format that she wants, e.g. --format text.

One common use-case for this is to force the simple or pretty version of text format. By default, for DWIM-ness, the text format becomes simpler when the program is run through pipes (e.g. formatted ASCII table becomes lines of tab-separated values). For example (I’m using the list-files script mentioned in pericmd 039):

Sometimes you always want to default to the pretty version (even though your CLI program is run through pipes), and sometimes the other way around. To do this you can instruct in the result metadata 'cmdline.default_format' => 'text-pretty' (or text-simple).

Note that the cmdline.default_format attribute can also be specified in the Rinci function metadata, but specifying this in the result metadata is more flexible as we can customize on a per-invocation basis.

cmdline.exit_code

This is not actually related to output format, but somewhat related. This attribute explicitly chooses an exit code for the CLI program. By default, as you might also remember, status code is determined as follow: “if status is 2xx or 304, then 0, else status-300”.

cmdline.skip_format

If you set this attribute to true, the result will be printed as-is without any formatting. You might want to use this if you are outputting a preformatted text. Which defeats the whole point of convenience given by Perinci::CmdLine, but sometimes it’s useful.

cmdline.page_result and cmdline.pager

This is also not directly related to formatting, but somewhat related. If you set cmdline.page_result to true, you can instruct Perinci::CmdLine to run a pager (like less). This might be useful for programs that output long text. The cmdline.pager can be used to specifically choose another program instead of the default $ENV{PAGER} (or less).

In the next blog post I’ll discuss more ways to customize table output.

If you analyze the output, the abstract is also written for you. This is taken from the Rinci metadata which is retrieved by gen-pericmd-script via a Riap meta request.

If you specify -o option, e.g. -o /home/s1/bin/gen-array, the generated script is written to the specified path and also set chmod 0755 as well as tab completion is activated (if you have shcompgen installed). There are of course several options to customize the script, like the Perinci::CmdLine backend module to use, whether to activate logging, specify subcommands, whether to add some code before instantiating Perinci::CmdLine object, and so on.

App::GenPericmdScript is actually best used with Dist::Zilla. There’s a plugin called DZP:Rinci::ScriptFromFunc which uses to App::GenPericmdScript to generate scripts for you during build. If some have a dist.ini like this:

After you run dzil build, you’ll get something like this in App-GenArray-0.01/bin/gen-array:

#!perl
# Note: This script is a CLI interface to Riap function /Perinci/Examples/gen_array
# and generated automatically using App::GenPericmdScript version 0.04
# DATE
# VERSION
use 5.010001;
use strict;
use warnings;
use Perinci::CmdLine::Any;
$ENV{PERL_LWP_SSL_VERIFY_HOSTNAME} = 0;
Perinci::CmdLine::Any->new(
url => "/Perinci/Examples/gen_array",
)->run;
# ABSTRACT: Generate an array of specified length
# PODNAME: gen-array
__END__
=pod
=head1 SYNOPSIS
Usage:
% gen-array [options] <len>
=head1 DESCRIPTION
Also tests result schema.
=head1 OPTIONS
C<*> marks required options.
=over
=item B<--config-path>=I<s>
Set path to configuration file.
Can be specified multiple times.
=item B<--config-profile>=I<s>
Set configuration profile to use.
=item B<--format>=I<s>
Choose output format, e.g. json, text.
=item B<--help>, B<-h>, B<-?>
Display this help message.
=item B<--json>
Set output format to json.
=item B<--len>=I<i>*
Array length.
Default value:
10
=item B<--naked-res>
When outputing as JSON, strip result envelope.
By default, when outputing as JSON, the full enveloped result is returned, e.g.:
[200,"OK",[1,2,3],{"func.extra"=>4}]
The reason is so you can get the status (1st element), status message (2nd
element) as well as result metadata/extra result (4th element) instead of just
the result (3rd element). However, sometimes you want just the result, e.g. when
you want to pipe the result for more post-processing. In this case you can use
`--naked-res` so you just get:
[1,2,3]
=item B<--no-config>
Do not use any configuration file.
=item B<--version>, B<-v>
=back
=head1 ENVIRONMENT
GEN_ARRAY_OPT
=head1 FILES
~/gen-array.conf
/etc/gen-array.conf
=cut

When you run perldoc on this script, you’ll get something like:

GEN-ARRAY(1) User Contributed Perl Documentation GEN-ARRAY(1)
SYNOPSIS
Usage:
% gen-array [options] <len>
DESCRIPTION
Also tests result schema.
OPTIONS
"*" marks required options.
--config-path=s
Set path to configuration file.
Can be specified multiple times.
--config-profile=s
Set configuration profile to use.
--format=s
Choose output format, e.g. json, text.
--help, -h, -?
Display this help message.
--json
Set output format to json.
--len=i*
Array length.
Default value:
10
--naked-res
When outputing as JSON, strip result envelope.
By default, when outputing as JSON, the full enveloped result is returned,
e.g.:
[200,"OK",[1,2,3],{"func.extra"=>4}]
The reason is so you can get the status (1st element), status message (2nd
element) as well as result metadata/extra result (4th element) instead of just
the result (3rd element). However, sometimes you want just the result, e.g.
when you want to pipe the result for more post-processing. In this case you
can use `--naked-res` so you just get:
[1,2,3]
--no-config
Do not use any configuration file.
--version, -v
ENVIRONMENT
GEN_ARRAY_OPT
FILES
~/gen-array.conf
/etc/gen-array.conf

Since Perinci::CmdLine uses Riap behind the scenes (from getting the Rinci metadata to calling the function), it is possible to use a remote server as the Riap server, even when the server side is not Perl. Below are two examples. The first one uses piping (stdin/stdout) to access a Ruby program on the same server, and the second one uses TCP server written in Node.js. Note that the two programs are just quick-hacks and very ad-hoc, I haven’t actually developed any Riap libraries on those languages. Their main goal is to demonstrate the simplicity of the Riap::Simple protocol.

Note that in this blog post we are using Perinci::CmdLine::Classic instead of Perinci::CmdLine::Any because the default backend Perinci::CmdLine::Lite does not yet support the URL schemes riap+pipe:/ or riap+tcp://. This will be rectified sometime in the future.