Perinc...amples(3pm)
๐ NAME ๐
Perinci::CmdLine::Manual::Examples โ a whole lil collection of examples, bestie. ๐
It lives in the Perinci::CmdLine::Manual::Examples (3pm) manpage, under User Contributed Perl Documentation. iykyk.
โจ VERSION โจ
This doc describes version 2.000.0 of Perinci::CmdLine::Manual::Examples (from Perl distribution Perinci-CmdLine), released on 2021-12-19. ๐
๐ฆ DESCRIPTION ๐ฆ
Okay so, in the examples, Perinci::CmdLine::Any gets used whenever the example works for either Perinci::CmdLine::Classic or Perinci::CmdLine::Lite. Slay, versatile. โจ
But when an example is more appropriate for โ or only works with โ one specific implementation, then that specific module gets used instead. No pretending, we keep it honest. ๐
For short, Perinci::CmdLine::Classic is called "PC::Classic" here, and Perinci::CmdLine is called "PC::Lite". Nicknames, cute.
๐ BASICS ๐
๐ธ Simplest application ๐ธ
So Perinci::CmdLine is function- and metadata-based, right? Which means you literally need at least one function, and you gotta add some metadata for it. And (this is the part everyone forgets ๐ญ) you need to return the result as an enveloped response.
The simplest version of that is something like this:
#!perl
use strict;
use warnings;
use Perinci::CmdLine::Any;
our %SPEC;
$SPEC{hello} = {
v => 1.1,
summary => 'Say hello',
};
sub hello {
[200, "OK", "Hello, world!"];
}
Perinci::CmdLine::Any->new(url => '/main/hello')->run;
Let's break it down, bestie. ๐ซถ
The "url" attribute says where the function lives, in URL format (go peek at Riap if you want the full tea on the syntax of URL). Basically it's a fully-qualified function name, but with "::" swapped out for /. And because it's URL-based, you can totally use a remote function and/or a non-Perl function for your CLI application. Main character energy, honestly. ๐
That hash sitting in $SPEC{hello} is called a Rinci metadata. Its keys are called properties. There are two properties in there:
"v"โ this one is always required, and you give it the value1.1to specify the version."summary"โ this one is actually optional, and it describes the function.
In this example, the function and its metadata are both chilling inside the same script. You can of course put them in a separate Perl module and use them with something like url => '/Your/Module/func'. ๐
Also worth mentioning (because it's lowkey the best part): if you use the Perinci::CmdLine framework, your functions can also be used directly by other Perl modules/code, since they're just regular Perl functions. No extra work. Iconic.
The function returns a 3-element array containing:
- an HTTP-like status code
- a status message
- the actual result
So if you save that script above as hello and run it on the command line:
% ./hello
Hello, world!
Yup. Not very interesting. ๐ But here's the thing โ you get a help message for free:
% ./hello --help
% ./hello -h
As well as some common options like "--format", so you can get the result back in a different format:
% ./hello --json
[200,"OK","Hello, world!"]
% ./hello --format perl; # only in PC::Classic, not available in PC::Lite
[200, "OK", "Hello, world!"]
๐ Not me pointing out that --format perl is only in PC::Classic and is not available in PC::Lite. Read the comment, bestie, it's right there.
๐ FUNCTION ARGUMENTS AND COMMAND-LINE OPTIONS ๐
โจ Basics โจ
okay so here's the whole vibe: your function's arguments become your command-line options. that's it, that's the magic. watch:
#!perl
use strict;
use warnings;
use Perinci::CmdLine::Any;
our %SPEC;
$SPEC{hello} = {
v => 1.1,
summary => 'Say hello',
args => {
name => {
summary => 'Name to say hello to',
},
},
};
sub hello {
my %args = @_;
[200, "OK", "Hello, $args{name}!"];
}
Perinci::CmdLine::Any->new(url => '/main/hello')->run;
and when you run it:
% ./hello --name Jimmy
Hello, Jimmy!
and if you run ./hello --help, bestie, that option is now mentioned in the help message too. it just shows up. iconic.
unknown arguments? not allowed, sorry:
% ./hello --gender m
ERROR 400: Unknown option '--gender'
you want an argument to be mandatory? just drop a req property in the argument spec with a true value:
args => {
name => {
summary => 'Name to say hello to',
req => 1,
},
},
so when you run the app:
% ./hello
ERROR 400: Missing required argument 'name'
and if you want an argument to also be specifiable as a positional command-line argument (not just a --option), add the pos property:
args => {
name => {
summary => 'Name to say hello to',
req => 1,
pos => 0,
},
},
so now both of these hit:
% ./hello --name Jimmy
Hello, Jimmy!
and also:
% ./hello Jimmy
Hello, Jimmy!
extra arguments? also a no, also an error:
% ./hello Jimmy Gideon
ERROR 400: Extra argument 'Gideon'
โจ Argument schema (and more on text output formats) โจ
okay following up from the last one, here's another example with more arguments. also side note, i'm using PC::Classic here because PC::Lite doesn't do schema validation. just so you know. ๐
#!perl
use 5.010;
use strict;
use warnings;
use Perinci::CmdLine;
our %SPEC;
$SPEC{hello} = {
v => 1.1,
summary => 'Say hello',
args => {
name => {
summary => 'Name(s) to say hello to',
schema => [array => {of => 'str', min_len=>1}],
req => 1,
pos => 0,
slurpy => 1,
},
gender => {
summary => 'The gender of the name(s)',
schema => [str => {in => ['m','f']}],
},
},
};
sub hello {
my %args = @_;
my $g = $args{gender};
my @res;
for my $name (@{ $args{name} // [] }) {
push @res, join("",
"Hello, ",
(!$g ? "" : $g eq 'm' ? "Mr. " : "Mrs. "),
$name, '!',
);
}
[200, "OK", \@res];
}
Perinci::CmdLine->new(url => '/main/hello')->run;
if you run this program:
% ./hello Jimmy Sion Habil
% ./hello --name Jimmy --name Sion --name Habil
Hello, Jimmy!
Hello, Sion!
Hello, Habil!
% ./hello --name-json '["Jimmy","Sion","Habil"]' --gender m
Hello, Mr. Jimmy!
Hello, Mr. Sion!
Hello, Mr. Habil!
okay so a few things you might've clocked:
First โ there's a schema property for every argument. name is typed as an array of strings. to set this argument from the CLI you can either pass it multiple times (like --name NAME1 --name NAME2 ...) or throw it in as JSON (like --name-json JSONSTR). both work, choose your fighter.
Second โ the name argument has the slurpy property. it works together with pos. it declares that the argument gobbles up command-line arguments from pos all the way to the end. so you can also set the name values with ARG1 ARG2 .... it literally just eats them. ๐
Third โ if you pass a value that doesn't validate, you get an error:
% ./hello --name-json '[]'
ERROR 400: Invalid value for argument 'name': Length must be at least 1
% ./hello --name Jimmy --name Sion --name Habil --gender x
ERROR 400: Invalid value for argument 'gender': Must be one of ["m","f"]
for more about the schema syntax, go look at Data::Sah. ๐ง
Fourth โ you return the result as a data structure (an array) instead of printing it directly with print() or say(). this is on purpose, bestie โ it makes your function way more reusable outside the CLI context. PC::Classic will format your data structure all nicely using Data::Format::Pretty. your array gets printed as a multicolumn ANSI table by default, when you're in interactive mode. but if you pipe the output of your program, you'll get a simpler text output by default instead. and you can pick explicitly with the --format common option:
% ./hello Jimmy Sion Habil --format text; # will output pretty or simple depending on whether interactive
% ./hello Jimmy Sion Habil --format text-simple; # will still output simple table even when interactive
% ./hello Jimmy Sion Habil --format text-pretty; # will still output pretty table even when piped
โจ Short option aliases โจ
want short options? sure. use the cmdline_aliases property in the argument spec:
name => {
...
cmdline_aliases => { n => {} },
},
gender => {
...
cmdline_aliases => { g => {} },
},
so instead of:
% ./hello --name Jimmy --name Sion --name Habil --gender m
you can also do:
% ./hello -n Jimmy -n Sion -n Habil -g m
way shorter, way cuter. ๐
โจ More on command-line option aliases โจ
and you're not limited to one alias, or even one letter:
gender => {
...
cmdline_aliases => { g => {}, sex => {} },
},
so now all of these are equivalent:
% ./hello ... --gender m
% ./hello ... -g m
% ./hello ... --sex m
now suppose you want to make -m mean --gender m and -f mean --gender f instead. you can do that too:
gender => {
...
cmdline_aliases => {
m => { schema=>'bool', code => sub {my $args=shift; $args->{gender} = 'm' } },
f => { schema=>'bool', code => sub {my $args=shift; $args->{gender} = 'f' } },
},
},
and now you can say:
% ./hello Jimmy Sion -m
Hello, Mr. Jimmy!
Hello, Mr. Sion!
% ./hello Nunung Misye -f
Hello, Mrs. Nunung!
Hello, Mrs. Misye!
obsessed. ๐ญ
โจ My function has some cmdline_aliases or cmdline_src defined but I want to change it! โจ
okay so say your f1 function metadata looks like this:
package Package::F1;
our %SPEC;
$SPEC{f1} = {
v => 1.1,
args => {
foo => {
cmdline_aliases => { f=> {} },
},
bar => { ... },
fee => { ... },
},
};
sub f1 { ... }
1;
and your command-line script f1 looks like this:
#!perl
use Perinci::CmdLine;
Perinci::CmdLine->new(url => '/Package/F1/f1')->run;
and now you wanna build a command-line script interface for this function, but with -f as an alias for --fee instead of --foo. the best way is to modify the metadata and make a wrapper function to do it. like, your command-line script f1 becomes:
package main;
use Perinci::CmdLine;
use Package::F1;
use Data::Clone;
our %SPEC;
$SPEC{f1} = clone $Package::F1::SPEC{f1};
delete $SPEC{f1}{args}{foo}{cmdline_aliases};
$SPEC{f1}{args}{fee}{cmdline_aliases} = {f=>{}};
*f1 = \&Package::F1::f1;
Perinci::CmdLine->new(url => '/main/f1')->run;
and this also shows off why having the metadata as a plain data structure is so convenient โ you can manipulate it however you want. there's also a handy helper function in Perinci::Sub::Util for when you want to make a modified subroutine based on another one:
package main;
use Perinci::CmdLine;
use Perinci::Sub::Util qw(gen_modified_sub);
gen_modified_sub(
output_name => 'f1',
base_name => 'Package::F1::f1',
modify_args => {
foo => sub { my $as = shift; delete $as->{cmdline_aliases} },
fee => sub { my $as = shift; $as->{cmdline_aliases} = {f=>{} },
},
);
Perinci::CmdLine->new(url => '/main/f1')->run;
โจ Overriding common option โจ
example: my function has an argument named format, but it's blocked by the common option --format! ๐
to add/remove/rename common options, go read the docs on the common_opts attribute. in this case you want:
delete $cmd->common_opts->{format};
#delete $cmd->common_opts->{format_options}; # you might also want this
or maybe you wanna rename it instead:
$cmd->common_opts->{output_format} = $cmd->common_opts->{format};
delete $cmd->common_opts->{format};
Accepting input from STDIN (or files)
okay so imagine you just want your argument value to show up from stdin ๐
You set cmdline_src to 'stdin' on a 'str' argument, and then if nobody actually specifies the value, it gets pulled straight from standard input. Like this:
use Perinci::CmdLine;
$SPEC{cmd} = {
v => 1.1,
args => {
arg => {
schema => 'str*',
cmdline_src => 'stdin',
},
},
};
sub cmd {
my %args = @_;
[200, "OK", "arg is '$args{arg}'"];
}
Perinci::CmdLine->new(url=>'/main/cmd')->run;
and when you run it from the command line:
% cat file.txt
This is content of file.txt
% cat file.txt | cmd
arg is 'This is content of file.txt'
If your function argument happens to be an array, then you get an array of lines handed to your function instead. ๐
watch out bestie โ this globs the whole content of the input into memory. All of it. If you want streaming, that's literally the next section, keep reading.
Accept streaming input
To get streaming input you mark one or more of your arguments with stream=>1. Also, those arguments have to declare where their source is โ from file, STDIN, or STDIN/files โ by setting cmdline_src => file|stdin|stdin_or_files. Because otherwise you'd just be receiving a value from a command-line option like --arg val, and ngl that's not very interesting :-). What you get instead is your function argument as a coderef, and you can call it over and over until the input is exhausted โ and at that exact point the coderef returns undef. Iconic.
$SPEC{perl_wc} = {
v => 1.1,
args => {
input => {
schema => 'str*',
stream => 1,
cmdline_src => 'stdin_or_files',
},
},
};
sub perl_wc {
my %args = @_;
my $input = $args{input};
my $chars = 0;
my $words = 0;
my $lines = 0;
while (my $line = $input->()) {
$lines++;
$chars += length($line);
chomp $line;
my @w = split /[ \t]+/o, $line; $words += @w;
}
[200, "OK", {chars=>$chars, words=>$words, lines=>$lines}];
}
and when you run it:
% ls -l | perl_wc
+-------+-------+
| key | value |
+-------+-------+
| chars | 1995 |
| lines | 42 |
| words | 61 |
+-------+-------+
Note: by default you'll get string/text input line-by-line, and for binary (buf) it's per-64k. That's gonna be configurable in the future. ๐ฎ
If the argument type isn't simple (say, an array or a hash), then JSON stream input is assumed โ meaning every single line of input gets parsed as JSON. Obsessed, honestly. โจ
๐ OUTPUT ๐
Default output format
TODO
Removing borders
By default the text format produces bordered tables when you're in interactive mode. So like, if you've got this program:
$SPEC{foo} = {v=>1.1};
sub foo {
[200, "OK", [[1,2], [3,4], [5,6]]];
}
use Perinci::CmdLine::Any;
Perinci::CmdLine::Any->new(url => '/main/foo')->run;
and you run it, you get:
% foo
+---+---+
| 1 | 2 |
| 3 | 4 |
| 5 | 6 |
+---+---+
and only if you pass --format text-simple does it turn into a tab-separated format:
% foo --format text-simple
1 2
3 4
5 6
But if you're not feeling this formatting and want the simpler one to be the default, you can add the cmdline.default_format attribute to your function metadata:
$SPEC{foo} = {v=>1.1, 'cmdline.default_format' => 'text-simple'};
and with that same attribute you can also default to JSON, and so on, if that's your vibe.
You can also do it on a per-result basis, by adding a cmdline.default_format attribute in your result metadata, like:
sub foo {
[200, "OK", [[1,2], [3,4], [5,6]], {'cmdline.default_format'=>'text-simple'}];
}
Streaming output
To produce streaming output, set stream=>1 in the result spec in your function metadata. Then, inside your function, return a subroutine reference that lets the caller read data from it. ๐ซถ
$SPEC{nat} = {
v => 1.1,
summary => 'Generate an infinite sequence of natural numbers',
args => {},
result => {
stream => 1,
schema => 'str*',
},
};
sub nat {
my $n = 1;
[200, "OK", sub { $n++ }];
}
If the result type isn't simple (an array or a hash, for example), then each record gets encoded into JSON, which produces a JSON stream.
And if you want to return file content as a stream instead of slurping the whole thing into memory:
$SPEC{catfile} = {
v => 1.1,
summary => "Display file contents",
args => { file => { schema => 'filename*', req=>1, pos=>0 } },
result => { stream => 1, schema => 'str*' },
};
sub catfile {
my %args = @_;
my $file = $args{file};
open my $fh, "<", $file or return [500, "Can't open file '$file': $!"];
[200, "OK", sub { scalar(<$fh>) }];
}
Adding support for new format
TODO
Pager
TODO
๐ CONFIGURATION ๐
Basics
In the function-centric world of Perinci::CmdLine, configuration is literally just another way to supply values to function arguments (and it happens before those values can potentially get overridden by command-line arguments, so know the order bestie). Configuration files are written in IOD format, which is basically "INI with extra features". By default, config files get searched in /etc and then in your home directory, under the name program_name + ".conf". So if you've got:
# ~/prog.conf
foo=1
bar=2
and:
# prog
#!perl
use Perinci::CmdLine::Any;
$SPEC{prog} = {
v => 1.1,
args => {
foo => {},
bar => {},
},
};
sub prog {
my %args = @_;
[200, "OK", "foo is $args{foo}, while bar is $args{bar}"];
}
Perinci::CmdLine::Any->new(url=>'/main/prog')->run;
then when you run:
% prog
you'll get:
foo is 1, while bar is 2
Multiple configuration files get merged, so if you have:
# /etc/prog.conf
foo=1
bar=2
# ~/prog.conf
foo=10
you'll get:
foo is 10, while bar is 2
Configuration profiles
A configuration file can store more than one set of arguments, through specially named sections called profiles:
# ~/prog.conf
foo=1
bar=2
[profile=p1]
foo=21
bar=22
[profile=p2]
foo=31
bar=32
and running the program:
% prog
foo is 1, while bar is 2
% prog --config-profile p1
foo is 21, while bar is 22
% prog --config-profile p2
foo is 31, while bar is 32
look at her go ๐ญ๐
Configuration with subcommands
TODO
Ignoring configuration files
If you don't want to use any configuration files at all, you can just use:
% prog --noconfig ...
DEBUGGING
TODO
REMOTE FUNCTION
(nothing here yet โ the page just moves on, bestie ๐)
๐ฆ SHELL COMPLETION ๐ฆ
Custom completion
By default, Perinci::Sub::Complete's complete_arg_val() can employ some heuristics to complete argument values, e.g. from the in clause or from max and min:
$SPEC{set_ticket_status} = {
v => 1.1,
args => {
ticket_id => { ... },
status => {
schema => ['str*', in => [qw/new open stalled resolved rejected/],
},
},
}
But if you want to supply custom completion, the Rinci::function specification lets you specify a completion property for your argument. For example:
use Complete::Util qw(complete_array_elem);
$SPEC{del_user} = {
v => 1.1,
args => {
username => {
schema => 'str*',
req => 1,
pos => 0,
completion => sub {
my %args = @_;
# get list of users from database or whatever
my @users = ...;
complete_array_elem(array=>\@users, word=>$args{word});
},
},
...
},
};
and then you can use completion in your command-line program:
% del-user --username <tab>
% del-user <tab> ; # since the 'username' argument has pos=0
My custom completion does not work, how do I debug it?
Completion works by the shell invoking our (the same) program with the COMP_LINE and COMP_POINT environment variables. You can do something like this to see debugging information:
% COMP_LINE='myprog --arg x' COMP_POINT=13 PERL5OPT=-MLog::ger::App TRACE=1 myprog --arg x
You can also use the testcomp utility (it ships inside the App::CompleteUtils distribution) to help debug your custom completion:
% testcomp myprog --arg x^
Drop that "^" caret character in there to simulate where the cursor is sitting when tab-completion gets attempted. โจ
๐ธ I18N ๐ธ
๐ฆ OTHERS ๐ฆ
okay so bestie, we're in the extras now! Let's go. ๐
Modifying common options
Customizing help message
Dealing with binary data
(Client) (Server) Dealing with binary data
The choice as JSON as the network transport protocol for Riap (because it is literally the lowest common denominator across languages like JavaScript, PHP, Python, Ruby, and Perl) means dealing with binary data requires an extra step or two. But the Perinci::CmdLine framework is equipped with some features to make this simpler and more convenient. ๐ซถ
First, to make a function that accepts binary data (in its arguments), you need to specify the argument type as buf. To return binary data as result, you need to specify result's schema type as buf. Example:
package MyLib;
our %SPEC;
$SPEC{gzip} = {
v => 1.1,
summary => 'Gzip some data',
args => {
data => {
summary => 'Data to compress',
schema => 'buf*',
req => 1,
},
},
};
sub gzip {
require IO::Compress::Gzip;
my %args = @_;
my $compressed;
IO::Compress::Gzip::gzip($args{data} => $compressed)
or return [500, "Compression failed"];
[200, "OK", $compressed];
}
If you use this function in Perinci::CmdLine, you will get the command-line option --data-base64 in addition to the usual --data. With --data-base64, you can specify binary data including NUL bytes from the command-line. โจ
If you specify the argument as accepting data from stdin or files like this:
args => {
data => {
summary => 'Data to compress',
schema => 'buf*',
req => 1,
cmdline_src => 'stdin_or_files',
},
},
you can pass binary data, e.g.:
% yourprog < /some/bindata
Perinci::CmdLine will take care of encoding this data to network server when you specify riap_version attribute to 1.2. So this process is transparent to you. ๐
When outputting binary result, in the text output formats, Perinci::CmdLine will also print the binary result from server as-is without any newline added. So you can pipe binary data to files/processes unmodified. No cap. ๐ฅ
๐ SUBCOMMANDS ๐
Default subcommand ๐
A default subcommand can be defined. This subcommand is selected without user specifying it the first command-line argument. A real-world example of this is from File::Trash::Undoable. The trash-u command is by default selecting the trash subcommand:
% trash-u file1 file2
is equivalent to:
% trash-u --cmd trash file1 file2
To select another subcommand other than trash, an explicit option is needed:
% trash-u --list-contents ; # select the list_contents subcommand
% trash-u --cmd empty ; # select the empty subcommand
This is done via something like:
Perinci::CmdLine->new(
subcommands => {
trash => { url=>... },
empty => { url=>... },
list_contents => { url=>... },
},
default_subcommand => 'trash',
)->run;
Default subcommand (override via first command-line argument) ๐
There is also a choice to specify a default subcommand which is overrideable via first command-line argument. A real-world example of this is from App::GitUtils. If the gu command is specified without any argument:
% gu
then it is equivalent to:
% gu info
but user can specify other subcommands:
% gu post-commit
This is accomplished by setting:
Perinci::CmdLine::Any->new(
subcommands => {
info => {...},
run_hooks => {...},
post_commit => {...},
...
},
default_subcommand => 'info',
get_subcommand_from_arg => 2,
)->run;
Dynamic list of subcommands ๐
TODO
๐ธ HOMEPAGE ๐ธ
Please visit the project's homepage at <https://metacpan.org/release/Perinci-CmdLine>. ๐
๐ฆ SOURCE ๐ฆ
Source repository is at <https://github.com/perlancar/perl-Perinci-CmdLine>. โจ
๐ SEE ALSO ๐
Perinci::CmdLine::ManualPerinci::Examples
๐ AUTHOR ๐
perlancar <perlancar@cpan.org>
๐ซถ CONTRIBUTING ๐ซถ
To contribute, you can send patches by email/via RT, or send pull requests on GitHub.
Most of the time, you don't need to build the distribution yourself. You can simply modify the code, then test via:
% prove -l
If you want to build the distribution (e.g. to try to install it locally on your system), you can install Dist::Zilla, Dist::Zilla::PluginBundle::Author::PERLANCAR, and sometimes one or two other Dist::Zilla plugin and/or Pod::Weaver::Plugin. Any additional steps required beyond that are considered a bug and can be reported to me. ๐ญ
๐ COPYRIGHT AND LICENSE ๐
This software is copyright (c) 2021, 2018, 2017, 2016, 2015 by perlancar <perlancar@cpan.org>.
This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.
๐ BUGS ๐
Please report any bugs or feature requests on the bugtracker website <https://rt.cpan.org/Public/Dist/Display.html?Name=Perinci-CmdLine>. ๐ซถ
When submitting a bug or request, please include a test-file or a patch to an existing test-file that illustrates the bug or desired feature.
๐ That's the tea! This page is from Perinc...amples(3pm), powered by perl v5.32.1, dated 2022-01-14. Slay! ๐
๐ฆ where this page comes from
- ubuntu jammy: libperinci-cmdline-perl 2.000.0-1 ยท Artistic-1.0-Perl OR GPL-1.0-or-later
- ๐ girlified by deepseek-ai/DeepSeek-V4.1-Flash on : a language model's rewrite of the original, under its terms