Yet Another CLI framework
Write CLI.pm, not cli.pl
TL;DR
Searching for CLI modules on MetaCPAN returns 1690 results. And still I wrote another, Yet Another CLI framework. This is about why mine is the one you want to use.
Introduction
CLI clients, we all write them, we all want them, but the boilerplate is just horrible. I wanted to get rid of it: Burn it with 🔥.
At my previous dayjob we had something I wrote, or co-wrote, my boss wanted a
sort of chef like API. It became zsknife, but had a huge downside. You needed
to register each command in the main module and I didn’t like that at all. I
forked the concept internally, made it so you didn’t needed to register, but it
lacked discovery.
Unfinished business
When I wanted to write a git helper/client in Perl I took the pattern of my former (or back than current) employer and improved it. The rules where simple:
- A CLI module should be just a
run()statement - A CLI module should be able to describe itself via POD
- A CLI module should be able to trigger a
--helpand--man - A CLI module didn’t need to subscribe itself
- A CLI module should be able to have actions and subactions
The use case for me was to have an API similar to this: Verbs that explain the action you want to execute. Perhaps a noun too, for when it’s about a domain of sorts:
git lab repo create
git lab mr create
git lab mr update
# Or when the tooling itself is the domain
mytool update
mytool release
I never finished the git helper project… but!
YA::CLI
CLI modules on MetaCPAN, I saw a lot! And I wrote another, hence the name, YA::CLI , Yet Another CLI framework.
How does one use it? The meat of a command or subcommand is this:
package MyApp::CLI::MR::Create;
use Moo;
with 'YA::CLI::ActionRole';
sub action { 'mr' };
sub subaction { 'create' };
# Read Getopt::Long for cli_options
sub cli_options { return ('dry-run|n', 'force|f') }
sub run {
...;
}
That’s it. Help, manpages, option parsing (via Getopt::Long ), given to you for free. Add a role and you give your app super powers. The action role handles dispatch, you just write the logic. Testing is a plain object instantiation, no subprocess, no @ARGV gymnastics. The hardest part of writing a new command is deciding what it should do.
An example test for another module of mine:
{
my $log_before = qx{git log --oneline};
write_file('public/index.xml', $initial_rss);
my $cmd = Blog::Release::CLI::RSS->new(config => $config);
$cmd->run();
my $log_after = qx{git log --oneline};
is($log_before, $log_after, 'no git commits made');
}
SOS - POD to the rescue
You can also tell YA::CLI where to get the POD from, you just tell it where
to get it:
sub usage_pod {
return 0; # or undef - pod from the script
return 1; # pod from the file itself
return 'pl'; # similar to 0 or undef
return '/path/to/file'; # this is used as a file for pod
}
You don’t need to call Usage::Pod yourself.
You can add POD to MyApp::CLI and have a help/man page, similar to what
happens when you run git without arguments.
Argument handling
You don’t need @ARGV, you can get the args via $self->_args. All options
that don’t have an attribute (via has) are stashed away in
$self->_cli_args->{'your-name-here'}.
Try it out
Try it yourself:
cpanm YA::CLI
Now you write the script:
#!perl
use strict;
use warnings;
# PODNAME: myapp.pl
# ABSTRACT: yet another app
require MyApp::CLI;
MyApp::CLI->run();
You write some glue code:
package MyApp::CLI;
# ABSTRACT: Command-line interface for myapp
use Moo;
use namespace::autoclean;
extends 'YA::CLI';
# you can leave this out if you don't want it:
sub exclude_search_path {
return ['MyApp::CLI::Role::Base'];
}
1;
And finally your first action handler:
package MyApp::CLI::MyAction;
use Moo;
with 'YA::CLI::ActionRole';
sub action { 'myaction' };
# Read Getopt::Long for cli_options
sub cli_options { return ('dry-run|n', 'force|f') }
sub run {
...;
}
Writing CLI scripts just became easy, no need to worry about Getopt::Long,
Pod::Usage and other boilerplate. Just write a testable module and you are
good to go.
YA::Goodbye