Perinc...xamples(3pm)
β¨ Perinci::CmdLine::Manual::Examples β¨
okay bestie, this is Perinci::CmdLine::Manual::Examples β a whole Collection of examples, and it's giving "here's how you actually do the thing" π
πΈ VERSION πΈ
This doc is version 2.000.2 of Perinci::CmdLine::Manual::Examples, and it comes from the Perl distribution Perinci-CmdLine, released on 2026-03-30. periodt.
π¦ DESCRIPTION π¦
So in these examples, Perinci::CmdLine::Any is what we use whenever the example works for either Perinci::CmdLine::Classic or Perinci::CmdLine::Lite. But when an example is more of a specific-implementation kinda vibe, or only applies to one of them, we'll use that specific module instead. no cap.
Also, for short:
Perinci::CmdLine::Classicis referred to as "PC::Classic" β¨Perinci::CmdLineis referred to as "PC::Lite" β¨
that's it that's the naming lore, iykyk.
π BASICS π
π Simplest application π
Since Perinci::CmdLine is function- and metadata-based, you need at least one function, and you need to add some metadata for it. And β important β you gotta return the result as an enveloped response. The simplest version looks 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;
The url attribute says where the function lives, in URL format (peep Riap for the deets on URL syntax). Basically it's a fully-qualified function name where you swap "::" for "/". And because it's URL-based like that, you can literally use a remote and/or non-Perl function for your CLI app. iconic honestly.
The hash in $SPEC{hello} is called a Rinci metadata. Its keys are called properties. In this example there are two properties:
vβ which is always required, and its value is1.1to specify the versionsummaryβ which is actually optional, and describes the function
In this example, the function and its metadata are both chilling inside the same script. But you can obviously put them in a separate Perl module and use them with something like url => '/Your/Module/func'. Worth mentioning too: if you use the Perinci::CmdLine framework, your functions can also be used directly by other Perl modules/code, because they're just regular Perl functions. we love a multitasker π
The function returns a 3-element array containing:
- an HTTP-like status code
- a status message
- the actual result
If you save that script above as hello and run it on the command line:
% ./hello
Hello, world!
yup, not very interesting, bestie π but WAIT β you get the help message for free:
% ./hello --help
% ./hello -h
And also some common options like --format to return the result 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!"]
lowkey obsessed that --format perl is only in PC::Classic and not available in PC::Lite β don't come for me if it doesn't work there π
β¨ FUNCTION ARGUMENTS AND COMMAND-LINE OPTIONS β¨
π Basics
Okay bestie, here's the whole ~vibe~: your function arguments map straight onto command-line options. Like, one-to-one. 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!
If you run ./hello --help, the option shows up in the help message too. It's giving self-documenting. π
Now β unknown arguments? Yeah that's a whole error:
% ./hello --gender m
ERROR 400: Unknown option '--gender'
Wanna make an argument mandatory? Just toss the req property into the argument spec with a true value:
args => {
name => {
summary => 'Name to say hello to',
req => 1,
},
},
So then:
% ./hello
ERROR 400: Missing required argument 'name'
And if you want an argument to also be specifiable as a positional command-line argument instead of just a plain old option, add the pos property to the spec:
args => {
name => {
summary => 'Name to say hello to',
req => 1,
pos => 0,
},
},
Now you get to do:
% ./hello --name Jimmy
Hello, Jimmy!
and also:
% ./hello Jimmy
Hello, Jimmy!
Extra arguments? Also an error, no exceptions:
% ./hello Jimmy Gideon
ERROR 400: Extra argument 'Gideon'
πΈ Argument schema (and more on text output formats)
Following up from the last one β here's another example with way more arguments. Also quick note: I'm using PC::Classic here because PC::Lite doesn't do schema validation. lowkey important.
#!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;
Run it and:
% ./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 on each argument. name is declared as a type of array of strings. To feed that from the CLI you can either pass it multiple times (like --name NAME1 --name NAME2 ...) or hand it JSON (like --name-json JSONSTR). Your choice bestie.
Second β the name argument has the slurpy property set. This works together with pos. It means the argument gobbles up command-line arguments from pos alllll the way to the end. So you can set name's values with ARG1 ARG2 ... too.
Third β if you pass a value that doesn't validate, boom, 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"]
If you wanna know more about the schema syntax, go read Data::Sah. β¨
Fourth β you return the result as a data structure (an array here) instead of just printing it with print() or say(). Why? So your function stays reusable outside of CLI-land, that's why. PC::Classic formats the data structure all pretty for you using Data::Format::Pretty. Your array gets printed as a multicolumn ANSI table by default, in interactive mode. But if you pipe the output of your program, you'll get a simpler text output by default. 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
Wanna add short options? Use the cmdline_aliases property in the argument spec:
name => {
...
cmdline_aliases => { n => {} },
},
gender => {
...
cmdline_aliases => { g => {} },
},
So instead of this whole situation:
% ./hello --name Jimmy --name Sion --name Habil --gender m
you can just go:
% ./hello -n Jimmy -n Sion -n Habil -g m
π¦ More on command-line option aliases
And babe, you're not limited to one alias, or even one letter:
gender => {
...
cmdline_aliases => { g => {}, sex => {} },
},
Now all of these are the same thing:
% ./hello ... --gender m
% ./hello ... -g m
% ./hello ... --sex m
But say you wanna make -m mean --gender m and -f mean --gender f instead? Say less:
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:
% ./hello Jimmy Sion -m
Hello, Mr. Jimmy!
Hello, Mr. Sion!
% ./hello Nunung Misye -f
Hello, Mrs. Nunung!
Hello, Mrs. Misye!
iconic, honestly. β¨
π My function has some cmdline_aliases or cmdline_src defined but I want to change it!
Okay so picture this: your f1 function metadata looks kinda like:
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:
#!perl
use Perinci::CmdLine;
Perinci::CmdLine->new(url => '/Package/F1/f1')->run;
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 to do this? Modify the metadata and make a wrapper function. So 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 honestly? This right here is the glow-up of having metadata as a plain data structure β you can manipulate it however you want, periodt. π There's also a convenient helper function in Perinci::Sub::Util for when you wanna create 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
Real scenario for you: "My function has an argument named format, but it's blocked by the common option --format!!" π
So to add/remove/rename common options, go read the docs on the common_opts attribute. In this case, what you want is:
delete $cmd->common_opts->{format};
#delete $cmd->common_opts->{format_options}; # you might also want this
or maybe you'd rather rename it:
$cmd->common_opts->{output_format} = $cmd->common_opts->{format};
delete $cmd->common_opts->{format};
π₯ INPUT π₯
Accepting input from STDIN (or files)
ok so bestie, if you set cmdline_src to 'stdin' on a 'str' argument, that argument's value gets pulled from standard input whenever you don't actually specify it. slay. here's the vibe:
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'
and if your function argument is an array, bestie, you get an array of lines handed straight to your function. iconic.
β οΈ heads up though: this slurps the whole content of the input into memory. no cap. if you want streaming instead of the full snack, see the next section π
Accept streaming input
for streaming input you set one or more of your arguments to "stream=>1". also, those args have to declare their source β either from file, STDIN, or STDIN/files β by setting "cmdline_src => file|stdin|stdin_or_files", because otherwise just getting a value from a command-line option like --arg val is not very interesting :-) fr fr.
you'll receive your function argument as a coderef that you call over and over until the input is exhausted (and at that point the coderef returns undef). periodt.
$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 get string/text input line-by-line, and for binary ("buf") it's per-64k. this will be configurable in the future π¦
and if the argument type isn't simple (like an array or a hash), then JSON stream input is assumed, meaning each line of input gets parsed as JSON. iykyk.
π€ OUTPUT π€
Default output format
TODO
Removing borders
by default the text format makes bordered tables when you're in interactive mode. so if you have 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 use --format text-simple does it become a tab-separated format:
% foo --format text-simple
1 2
3 4
5 6
but if you don't vibe with that formatting and want to default to the simpler one, you add the cmdline.default_format attribute to your function metadata:
$SPEC{foo} = {v=>1.1, 'cmdline.default_format' => 'text-simple'};
and with this attribute you can also default to JSON and so on if that's your thing β¨
you can also do this 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 make streaming output, set stream=>1 in the result spec of the function metadata. then in your function you return a subroutine reference that lets the caller read data from it. main character energy honestly.
$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 (like an array or hash), then each record gets encoded into JSON, producing a JSON stream π
and if you want to return a file's 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
Word wrapping
if your table has long text that looks better in a multi-line cell, you can do:
% FORMAT_PRETTY_TABLE_BACKEND=Text::ANSITable your-app ...
to use Text::ANSITable, which by default wraps long text inside table cells. chef's kiss π
βοΈ CONFIGURATION βοΈ
Basics
in the function-centric world of Perinci::CmdLine, configuration is literally just another way to supply values to function arguments (before they get potentially overridden by command-line arguments, bestie). config files are written in IOD format, which is basically "INI with extra features".
by default, config files are searched in /etc and then your home directory, named program_name + ".conf". so if you have:
# ~/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;
when you run:
% prog
you'll get:
foo is 1, while bar is 2
multiple config 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 config 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
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
Configuration with subcommands
TODO
Ignoring configuration files
if you don't want to use any config files at all, just go:
% prog --noconfig ...
π DEBUGGING π
TODO
π REMOTE FUNCTION π
β¨ SHELL COMPLETION β¨
Custom completion
by default, Perinci::Sub::Complete's complete_arg_val() can use some heuristics to complete argument values, like 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 wanna supply your own custom completion, the Rinci::function specification lets you specify a "completion" property for your argument, like:
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 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 COMP_LINE and COMP_POINT environment variables set. you can do something like this to see debugging info π:
% COMP_LINE='myprog --arg x' COMP_POINT=13 PERL5OPT=-MLog::ger::App TRACE=1 myprog --arg x
you can also use the testcomp utility (included in the App::CompleteUtils distribution) to help debug your custom completion:
% testcomp myprog --arg x^
just place the "^" caret character to simulate where the cursor is when tab-completion is attempted π
π I18N π
β¨ OTHERS β¨
π Modifying common options π
π Customizing help message π
π Dealing with binary data π
π¦ (Client) (Server) Dealing with binary data π¦
okay bestie so the choice of JSON as the network transport protocol for Riap (because it's like the lowest common denominator across languages like JavaScript, PHP, Python, Ruby, Perl) means that dealing with binary data needs 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 a 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'll get the command-line option --data-base64 in addition to the usual --data. With --data-base64, you can specify binary data including NUL bytes right 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 the network server when you specify the riap_version attribute to 1.2. So this whole process is transparent to you. π«Ά
When outputting a binary result, in the "text" output formats, Perinci::CmdLine will also print the binary result from the server as-is, without any newline added. So you can pipe binary data to files/processes unmodified. π
β¨ SUBCOMMANDS β¨
π Default subcommand π
A default subcommand can be defined. This subcommand is selected without the user specifying it as 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 the 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 the 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, Pod::Weaver::PluginBundle::Author::PERLANCAR, and sometimes one or two other Dist::Zilla- and/or Pod::Weaver plugins. Any additional steps required beyond that are considered a bug and can be reported to me. π
β¨ COPYRIGHT AND LICENSE β¨
This software is copyright (c) 2026 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 a wrap, bestie! this page came from perl v5.40.1, dated 2026-04-05, and it's the Perinci::Examples(3pm) manpage. byeeee! π
π¦ where this page comes from
- ubuntu stonking: libperinci-cmdline-perl 2.000.2-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