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. To install it: cpanm YA::CLI.
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