Perinc...xamples(3pm)

πŸ“¦
version 2.000.2 Β· 2 versions
, in ubuntu stonking

✨ 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::Classic is referred to as "PC::Classic" ✨
  • Perinci::CmdLine is 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 is 1.1 to specify the version
  • summary β€” 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:

  1. an HTTP-like status code
  2. a status message
  3. 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::Manual
  • Perinci::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. πŸ’œ

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