App-Easer
view release on metacpan or search on metacpan
lib/App/Easer/Tutorial/V2_008.pod view on Meta::CPAN
#!/usr/bin/env perl
use v5.24;
use warnings;
use English;
use experimental qw< signatures >;
use Log::Log4perl::Tiny qw< :easy LOGLEVEL >;
use App::Easer::V2 qw< run >;
my $app = {
aliases => [qw< MAIN >],
help => 'An example',
sources => 'v2.008',
config_hash_key => 'v2.008',
options => [
{
getopt => 'loglevel|l=s',
help => 'logging level for Log::Log4perl::Tiny',
default => 'info',
# TRANSMIT OPTION, BUT IT WILL NOT WORK OUT OF THE BOX!
transmit => 1,
},
],
commit => sub ($self) {
LOGLEVEL(uc($self->config('loglevel')));
return;
},
children => [
{
aliases => [qw< seeker >],
help => 'some add-on to look at the seed!',
description => '',
options => [ '+parent' ],
execute => sub ($self) {
WARN 'this is WARN';
INFO 'this is INFO';
DEBUG 'this is DEBUG';
return 0;
},
},
],
};
exit(run($app, $0, @ARGV) // 0);
Example run after this change:
$ logger-example seeker --loglevel warn
[2024/09/07 16:34:49] [ WARN] this is WARN
[2024/09/07 16:34:49] [ INFO] this is INFO
What's happening?
The C<commit> callback set in the parent is run immediately after
options collections is completed I<in the parent>. At this stage, the
program has not seen the option's value in the child yet. As a result,
it uses whatever it has at that stage, i.e. the default C<info> value.
To address this specific issue, C<final_commit> comes to the rescue. In
the default arrangement, I<all> C<final_commit> callbacks are called in
reverse order from the chosen leaf command up to the command root,
immediately after the leaf command has completed collecting options.
Let's try it out:
# THIS PROGRAM DOES NOT WORK AS INTENDED TOO (YET)
#!/usr/bin/env perl
use v5.24;
use warnings;
use English;
use experimental qw< signatures >;
use Log::Log4perl::Tiny qw< :easy LOGLEVEL >;
use App::Easer::V2 qw< run >;
my $app = {
aliases => [qw< MAIN >],
help => 'An example',
sources => 'v2.008',
config_hash_key => 'v2.008',
options => [
{
getopt => 'loglevel|l=s',
help => 'logging level for Log::Log4perl::Tiny',
default => 'info',
transmit => 1,
},
],
##################################################################
# WE USE final_commit TO SET THE LOGLEVEL
final_commit => sub ($self) {
LOGLEVEL(uc($self->config('loglevel')));
return;
},
children => [
{
aliases => [qw< seeker >],
help => 'some add-on to look at the seed!',
description => '',
options => [ '+parent' ],
execute => sub ($self) {
WARN 'this is WARN';
INFO 'this is INFO';
DEBUG 'this is DEBUG';
return 0;
},
},
],
};
exit(run($app, $0, @ARGV) // 0);
Alas, this does not work yet! Example run after this change:
$ logger-example seeker --loglevel warn
[2024/09/07 16:34:49] [ WARN] this is WARN
[2024/09/07 16:34:49] [ INFO] this is INFO
lib/App/Easer/Tutorial/V2_008.pod view on Meta::CPAN
command, set their C<transmit> to a true value, and inherit it at every
level below. This gives you options C<--help>/C<--usage> consistently at
every level, at the cost of some code repetition at the beginning of
each execution.
If you're interested in help, there's more. You can get the help text by
calling method C<full_help_text> (optionally passing C<usage> to get the
I<shorter> version, i.e. without the I<Description> section).
=head3 Documenting options
L<App::Easer> does its best to generate documentation for options, based
on their definition.
You might want to tweak things, though, and this is where key
C<options_help> comes to the rescue.
If set to a string, it's used as the entire text for the options' help,
no questions asked.
If you find this a bit too I<extreme>, you can set it to a hash
reference supporting two (optional) keys C<preamble> and C<postamble>,
each pointing to a string value. In this case, the options' help is
still generated automatically as in the default case, but the text in
the C<preamble> is pre-pended and the text in the C<postable> is
appended to this auto-generated string. This might e.g. come handy in
case you also want to add documentation related to non-option
command-line arguments, e.g. to indicate that they are files, urls,
whatever.
=head2 Pass 10: Shared Behaviour
As your application grows, you will almost inevitably face the dilemma
of where to put and how to handle I<shared behaviour>, i.e. all those
activities that are common to most if not all sub-commands.
It might be anything, like using a specific logging library, coping with
the need to access a shared model object, etc.
There are a few strategies that you can adopt, discussed below.
=head3 Leverage the root command
Each L<App::Easer> application has one single I<root command> and you
can be sure that there will I<always> be an instance of that command's
class.
One strategy for storing common behaviour, then, would be to put it in
the root command implementation and then use method C<root> to retrieve
the command's object instance and consume the behaviour from there.
The I<downside> of this approach is that you need to implement your main
command as a I<class> instead of a hash definition like every example
seen so far. So while definitely possible, you might not know (yet) how
to do it.
=head3 Use a common base via C<hashy_class>
Although every command/sub-command definition seen in the example so far
has been provided as a simple hash reference full of keys and callbacks,
we've already seen that each command in a command chain is eventually
instantiated as a class object.
Each of these objects are normally instances of class
C<App::Easer::V2::Command>, but they need not be. By setting key
C<hashy_class> in the command's definition to a different class...
that's what will be used eventually.
This means that you might define a class derived from
C<App::Easer::V2::Command> and add the shared behaviour there; each
command will inherit it. This includes the root command too, of course.
=head3 Use the configuration
One of the main features of L<App::Easer> is about managing
configuration options; they can be taken from the outside or generated
dynamically and recorded in multiple ways (think about C<commit> and
C<final_commit> for example).
So one alternative is to encapsulate common behaviours in one or more
object instances, then save them as configuration options that are set
in the leaf configuration using method C<leaf> to retrieve it (e.g. from
the root command) and method C<set_config> to set the object.
=head4 Use shared state
If your application is small(ish) and defined in a single hash as all
eexamples so far, it's still possible to use global variables or lexical
variables accessible from all callbacks, like this:
my $shared_object = My::Class->new(...);
my $app = {
...
execute => sub ($self) {
$shared_object->do_this;
...
};
=head2 Pass 11: Class-based commands
If your application is composed of a few commands, managing it through a
single hash definition is handy and allows you to keep an overall
control.
As the application grows, your definition hash grows too and it can
become too big for easy management. At this point, you might want to
consider splitting the whole application and manage each (sub-)command
on its own.
L<App::Easer> allows transferring application features from the
hash-based declaration to class methods, while not requiring a full
transfer. Let's start from the fictional application in
L<< Pass 7|/Pass 7: Getting intermediates to work >>, containing one
root-level command and two sub-commands:
#!/usr/bin/env perl
use v5.24;
use warnings;
use English;
use experimental qw< signatures >;
use App::Easer::V2 qw< run >;
my $app = {
aliases => [qw< MAIN >],
sources => 'v2.008',
config_hash_key => 'v2.008',
children => [
{
aliases => [qw< foo >],
execute => sub ($self) {
say 'foo here!';
return 0;
},
},
{
aliases => [qw< bar >],
execute => sub ($self) {
say 'bar here!';
return 0;
},
},
],
execute => sub ($self) {
my @args = $self->residual_args;
say "MAIN (root) here! Also got (@args)";
return 0;
( run in 0.801 second using v1.01-cache-2.11-cpan-39bf76dae61 )