NAME
MOP4Import::Base::CLI_JSON - OO-Modulino with NDJSON args/outputs
SYNOPSIS
In MyScript.pm
#!/usr/bin/env perl
package MyScript;
use MOP4Import::Base::CLI_JSON -as_base,
[fields => qw/verbose/, [structs => doc => "usage of this field"]];
unless (caller) {
MY->cli_run(\@ARGV, {h => "help", v => 'verbose', d => 'debug'});
}
sub cmd_foo : Doc("This prints FOO") {
print "FOO\n"
}
sub bar {
(my MY $self) = @_;
([arguments => @ARGV], [structs => $self->{structs}])
}
1;
From shell:
% chmod a+x MyScript.pm
% ./MyScript.pm -h
Usage: MyScript.pm [--opt=value].. <Command> ARGS...
Commands
foo This prints FOO
help
Options from MyScript:
--verbose
--structs usage of this field
Options from MOP4Import::Base::CLI_JSON:
--help show this help message
--quiet to be (somewhat) quiet
--scalar evaluate methods in scalar context
--output choose output serializer (ndjson/json/tsv/dump)
--undef-as serialize undef as this value. used in tsv output
--no-exit-code exit with 0(EXIT_SUCCESS) even when result was falsy/empty
--binary keep STDIN/OUT/ERR binary friendly
%
% ./MyScript.pm foo
FOO
%
% ./MyScript.pm --structs='[1,2,{"x":"y"}]' bar '["baz",{"qux":"quux"}]' '{"other":"arg"}'
["arguments",["baz",{"qux":"quux"}],{"other":"arg"}]
["structs",[1,2,{"x":"y"}]]
DESCRIPTION
MOP4Import::Base::CLI_JSON (or CLI_JSON for short in this document) is my latest boilerplate base class to write Object-Oriented Modulinos (or OO Modulino), a variant of Modulino which I propose in this document. Note: you can write basic OO Modulino without any help of my modules, as I demonstrate soon below.
What is OO Modulino?
A Modulino is a Perl file that can act both as an executable or as a module (See these good articles). With Modulino, you can use some function(s) of the module from CLI without writing scripts or perl -e ...
. Though which functions you can use depend on how the Modulino is written. Generally, to use arbitrary public functions from given Modulino, you may still need to write some scripts.
So, what is OO Modulino? An OO Modulino is a Perl file that can act both as a class module or as a CLI executable (create an instance and invoke a specified method). OO Modulino standardizes the CLI behavior of Modulino about object construction, method invocation, and output handling from CLI so that you can test-drive almost all public features of the class by just giving a method name and parameters without worrying about its details.
So, how do you standardize CLI behavior of OO Modulino? We can mimic CLI behaviors from well-known complex CLI apps. Just recall git
command usage. It has many subcommands like add
, commit
, push
... These can be used as method names. It also takes global options like --git-dir=<path>
, --no-pager
... These can be used as constructor parameters.
For example, suppose you have a module Greetings.pm like followings:
package Greetings;
use strict;
use warnings;
sub new { my $class = shift; bless +{@_}, $class }
sub hello { my $self = shift; join " ", "Hello", $self->{name} }
sub goodnight { my $self = shift; join(" ", "Good night" => $self->{name}, @_)}
1;
To test above from CLI, you may write:
% perl -I. -MGreetings -le 'print Greetings->new(name => "world")->hello'
Minimalistic form of OO Modulino of the above could be written by adding an unless (caller) {...}
block like followings:
#!/usr/bin/env perl
package Greetings_oo_modulino;
use strict;
use warnings;
unless (caller) {
my $self = __PACKAGE__;
my $cmd = shift
or die "Usage: $0 COMMAND ARGS...\n";
print $self->new(name => "world")->$cmd(@ARGV), "\n";
}
sub new { my $class = shift; bless +{@_}, $class }
sub hello { my $self = shift; join " ", "Hello", $self->{name} }
sub goodnight { my $self = shift; join(" ", "Good night" => $self->{name}, @_)}
1;
You can test OO Modulino like following:
% ./Greetings_oo_modulino.pm hello
Extending this to pass constructor options is easily achived by replacing unless (caller)
block and rest like followings:
use fields qw/name/;
sub MY () {__PACKAGE__}
unless (caller) {
my $self = MY->new(name => 'world', MY->_parse_posix_opts(\@ARGV));
my $cmd = shift
or die "Usage: $0 COMMAND ARGS...\n";
print $self->$cmd(@ARGV), "\n";
}
sub _parse_posix_opts {
my ($class, $list) = @_;
my @opts;
while (@$list and $list->[0] =~ /^--(?:(\w+)(?:=(.*))?)?\z/s) {
shift @$list;
last unless defined $1;
push @opts, $1, $2 // 1;
}
@opts;
}
sub new { my MY $self = fields::new(shift); %$self = @_; $self }
sub hello { my MY $self = shift; join " ", "Hello", $self->{name} }
sub goodnight { my MY $self = shift; join " ", "Good night" => $self->{name}, @_}
1;
Now you can test OO Modulino with construction parameters like following:
% ./Greetings_oo_modulino_with_fields.pm --name=Universe hello
Hello Universe
%
With fields, you can reject unknown options at initialization.
% ./Greetings_oo_modulino_with_fields.pm --foo=bar xx
Attempt to access disallowed key 'foo' in a restricted hash at ./Greetings_oo_modulino_with_fields.pm line 29.
About CLI_JSON
Using CLI_JSON as a base class, you can run most methods directly from the shell. It treats subcommand names and options basically like following:
Subcomands are mapped to methods (basically).
(Posix style long) options before the subcommand are used to create instance(via
__PACKAGE__->new(%opts)
).
You can pass complex structures like arrays and hashes as option values and arguments to methods in JSON array/object literal syntax. Results of method invocation are printed with JSON serializer by default. You can override this behavior by implementing official command methods cmd_$COMMAND
.
Note: design goal of this module is *NOT* to provide complete feature-rich human-friendly CLI base class. Instead, it aims to make most methods in developping modules testable/useable via CLI (and perl -d) from very beginning of its development so that we can develop perl modules more rapidly via CLI-tested pieces of codes.
CLASS METHODS
cli_run (\@ARGV, \%option_shortcuts)
__PACKAGE__->cli_run(\@ARGV) unless caller;
This method parses arguments, invokes appropriate command and usually emits its result to STDOUT. Also there is an alias run()
which points to cli_run()
so that you do the same thing like below:
__PACKAGE__->run(\@ARGV) unless caller;
Accepted options are a subset of posix style options (--name
and --name=value
only). --name value
is not allowed, intentionally. If value part of options are recognized as JSON arrays/objects, they are automatically deserialized as perl's arrays/hashes.
If cli_run() gets optional second argument hash, it is used to accept short name for options like following:
__PACKAGE__->cli_run(\@ARGV, {h => 'help', v => 'verbose'}) unless caller;
In above, -h
is recognized samely as --help
and -v
as --verbose
.
Command name interpretation rule
Then first word in @ARGV
is taken and treated as command name. Command name is resolved to a module method in following ways:
cmd_$COMMAND
-
If there is a method named
cmd_$COMMAND
, it is assumed that this method is officially designed for CLI invocation. For official command,run()
just invokes it and do nothing else. Callee method is responsible to handle its output and possibly exit code. $COMMAND
-
If there is a method exactly matches to specified command, it is invoked via cli_invoke($method, @args). It invokes the method in array context (unless
--scalar
option is given). Results are printed bycli_output()
with JSON serializer.If results are empty list, program exit with code 1 (unless
--no-exit-code
option is given). - ??UNKNOWN??
-
If the command name does not meet all above,
cli_unknown_subcommand
hook is called instead.
OPTIONS
These options control run method.
- quiet
-
suppress output of method invocation.
- scalar
-
evaluate methods in scalar context
- output
-
default 'ndjson'. ndjson stands for Newline Delimited JSON. See http://ndjson.org/.
- undef-as
-
default => 'null'
- no-exit-code
-
suppress setting exit-code.
- binary
-
keep STDIN/OUT/ERR binary friendly. (default 0)
METHODS
All public APIs are named starting with cli_...
prefix.
cli_output
cli_invoke
cli_precmd
cli_unknown_subcommand
COMMANDS
help
This provides default implementation of usage output.
SEE ALSO
App::Cmd - if your main goal is writing full-fleged CLI.
LICENSE
Copyright (C) Kobayasi, Hiroaki.
This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself.
AUTHOR
Kobayasi, Hiroaki <buribullet@gmail.com>