App-Easer

 view release on metacpan or  search on metacpan

lib/App/Easer/Tutorial/V2_008.pod  view on Meta::CPAN

                  help   => 'name of key to update',
               },
               {
                  getopt => 'value|v=s',
                  help   => 'value to set for the key',
               }
            ],
            execute => sub ($self) {
               my $name = $self->config('name') // die "no name provided\n";
               my $dbpath = $self->config('db') // die "no db provided\n";
               my $data = load_json($dbpath);
               die "entry for <$name> does not exists\n"
                  unless exists($data->{$name});
               $data->{$name} = $self->config('value') // '';
               save_json($dbpath, $data);
               return 0;
            },
         },
         {
            aliases => [qw< delete >],
            help    => 'delete name or names (based on regex)',
            description => 'Need to provide name or name regular expression',
            options => [
               {
                  getopt => 'name|n=s',
                  help   => 'name of key to delete',
               },
               {
                  getopt => 'name-rx|r=s',
                  help   => 'regular expression for name(s) to delete',
               },
            ],
            execute => sub ($self) {
               my $dbpath = $self->config('db') // die "no db provided\n";
               my $data = load_json($dbpath);
               
               if (defined(my $name = $self->config('name'))) {
                  delete($data->{$name});
               }
               elsif (defined(my $rx = $self->config('name-rx'))) {
                  for my $name (keys($data->%*)) {
                     delete($data->{$name}) if $name =~ m{$rx};
                  }
               }
               else {
                  die "no clue what should be deleted\n";
               }

               save_json($dbpath, $data);
               return 0;
            },
         },
         {
            aliases => [qw< list >],
            help    => 'list names of available name/value pairs',
            description => '',
            options => [ ],
            execute => sub ($self) {
               my $dbpath = $self->config('db') // die "no db provided\n";
               my $data = load_json($dbpath);
               say {*STDOUT} $_ for sort { $a cmp $b } keys($data->%*);
               exit 0;
            },
         },
      ],
   };

   exit(run($app, $0, @ARGV) // 0);

   sub load_json ($path) {
      open my $fh, '<:raw', $path;
      local $/;
      return JSON::PP::decode_json(<$fh>);
   }

   sub save_json ($path, $data) {
      state $encoder = JSON::PP->new->ascii->canonical->pretty;
      open my $fh, '>:raw', $path;
      print {$fh} $encoder->encode($data);
   }


The structure is the following:

   #!/usr/bin/env perl

   # usual preamble with use-s and so on
   use strict;
   use warnings;
   # ...

   # what's needed to use App::Easer::V2
   use App::Easer::V2 qw< run >;
   my $app = {  ... }; # the whole application specification
   exit(run($app, $0, @ARGV) // 0);
   
   # ... other supporting functions etc.

The structure of command/subcommands is provided by means of the
C<children> key at the topmost level. You don't see them but three
commands are always added by default, i.e. C<help> (with an alias
C<usage>), C<commands>, and C<tree>.

=head3 Automatic commands C<help>, C<usage>, C<commands>, and C<tree>

Let's run the program without any option:

   $ ./crud
   A command for CRUD operations over a JSON file
   
   Options:
                db: path to the JSON file for CRUD operations
                    command-line: string, value is required
                                  --db <value>
                                  --json-db <value>
                                  -d <value>
                     environment: CRUD_DB
   
   Sub-commands:
            create: create a name/value pair
          retrieve: get the value associated to a name

lib/App/Easer/Tutorial/V2_008.pod  view on Meta::CPAN

               },
               {
                  getopt => 'value|v=s',
                  help   => 'value to set for the key',
               }
            ],
            execute => sub ($self) {
               my $name = $self->config('name') // die "no name provided\n";
               my $dbpath = $self->config('db') // die "no db provided\n";
               my $data = load_json($dbpath);
               die "entry for <$name> does not exists\n"
                  unless exists($data->{$name});
               $data->{$name} = $self->config('value') // '';
               save_json($dbpath, $data);
               return 0;
            },
         },
         {
            aliases => [qw< delete >],
            help    => 'delete name or names (based on regex)',
            description => 'Need to provide name or name regular expression',
            options => [
               'db',
               {
                  getopt => 'name|n=s',
                  help   => 'name of key to delete',
               },
               {
                  getopt => 'name-rx|r=s',
                  help   => 'regular expression for name(s) to delete',
               },
            ],
            execute => sub ($self) {
               my $dbpath = $self->config('db') // die "no db provided\n";
               my $data = load_json($dbpath);
               
               if (defined(my $name = $self->config('name'))) {
                  delete($data->{$name});
               }
               elsif (defined(my $rx = $self->config('name-rx'))) {
                  for my $name (keys($data->%*)) {
                     delete($data->{$name}) if $name =~ m{$rx};
                  }
               }
               else {
                  die "no clue what should be deleted\n";
               }

               save_json($dbpath, $data);
               return 0;
            },
         },
         {
            aliases => [qw< list >],
            help    => 'list names of available name/value pairs',
            description => '',
            options => [ 'db' ],
            execute => sub ($self) {
               my $dbpath = $self->config('db') // die "no db provided\n";
               my $data = load_json($dbpath);
               say {*STDOUT} $_ for sort { $a cmp $b } keys($data->%*);
               exit 0;
            },
         },
      ],
   };

   sub load_json ($path) {
      open my $fh, '<:raw', $path;
      local $/;
      return JSON::PP::decode_json(<$fh>);
   }

   sub save_json ($path, $data) {
      state $encoder = JSON::PP->new->ascii->canonical->pretty;
      open my $fh, '>:raw', $path;
      print {$fh} $encoder->encode($data);
   }

   exit(run($app, $0, @ARGV) // 0);

=head3 Where's the inheritance?

The are very little changes with respect to the previous iteration, so
let's look at them in more detail:

   my $app = {
      ...
      options => [
         {
            ...
            # Set the option as "inheritable" by children
            transmit    => 1,
         }
      ],
      children => [
         {
            ...
            options => [
               'db',  # <-- inherit option definition from parent
               ...


There are two halves to options definition inheritance: the parent marks
an option as available for inheritance setting a true value for key
C<transmit>, and the child gets it by putting its name in the list of
options (as opposed to a full hash-based definition).


=head3 Why C<transmit>? Because C<+parent>.

You might be wondering why setting options explicitly as C<transmit>
instead of providing them all and let the child command decide. This has
to do with dealing with inheritance of I<many> options all at a time.

If a child's C<options> array has this:


         {
            ...
            options => [

lib/App/Easer/Tutorial/V2_008.pod  view on Meta::CPAN

         ...
      },
      ...

=head2 Pass 3: C<commit> options along the way

Sometimes it can be hard to pre-determine a default value for an option
because its value might depend on multiple other values.

In a single command this is rarely a problem, because the specific
computation for the default value might be done at the beginning of the
C<execute> callback.

What if the value must be set in the root or an intermediate command
instead? As we saw, C<execute> is only called for the I<leaf> command,
not for other ones along the way. This is where C<commit> comes handy,
together with method C<inject_configs>.

Let's take an example program that supports an option C<seed> and a
sub-command to print it (which also inherits the option):


   #!/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 >],
      help    => 'An example',
      sources => 'v2.008',
      config_hash_key => 'v2.008',
      options => [
         {
            getopt => 'seed|randmo-seed=s',
            help   => 'a random seed for no reason!',
            transmit => 1,
         }
      ],
      commit => sub ($self) {
         return if defined($self->config('seed'));
         my $seed = join '-',
            grep { defined }
            @ENV{qw< THIS THAT AND_ALSO_THAT >};
         $seed = rand(1234) unless length($seed // '');
         $self->inject_configs({ seed => $seed });
         warn "WARNING: parent set seed<$seed>\n";
         return;
      },
      children => [
         {
            aliases => [qw< seeker >],
            help    => 'some add-on to look at the seed!',
            description => '',
            options => [ '+parent' ],
            execute => sub ($self) {
               my $seed = $self->config('seed') // '**undef**';
               say "seed is $seed";
               return 0;
            },
         },
      ],
   };

   exit(run($app, $0, @ARGV) // 0);

The new key C<commit> in the parent command sets a callback that is
called immediately after the options gathering process for the specific
level (in this case, the parent command).

If option C<seed> is not set, a custom logic assembles it or falls back
to a random number. This is just a toy example to represent a custom
logic that is difficult to express as a single default value directly
inside the option's definition.

Example runs (assuming the program is called C<seeder>):

   # parents sets the custom default, which is used in the child
   $ seeder seeker
   WARNING: parent set seed<1024.55957658367>
   seed is 1024.55957658367

   # set option in the parent, no custom default is set (no WARNING line)
   $ seeder --seed abc seeker
   seed is abc

   # parent sets custom default, but option is set in the child too and
   # the parent's default is ignored
   $ seeder seeker --seed def
   WARNING: parent set seed<909.487976275958>
   seed is def

=head2 Pass 4: C<final_commit>

The C<commit> mechanism is useful for setting values along the way, but
sometimes we might need to perform some common actions just before a
command is executed (at whatever level).

As an example, suppose that your program uses a custom logger like
L<Log::Log4perl::Tiny> and that you want to provide a command-line
option to set the log level. We might leverage C<commit> to set the log
level after options have been collected, like in the following example:

   #!/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 => [

lib/App/Easer/Tutorial/V2_008.pod  view on Meta::CPAN

   [2024/09/07 16:36:15] [ INFO] this is INFO
   [2024/09/07 16:36:15] [DEBUG] this is DEBUG

=head2 Pass 5: Custom source

As anticipated, sometimes we might want to load additional configuration
options from a I<custom source>. Let's see how to do it.

=head3 Setting a custom callback

Up to now, we used the stock configuration for C<sources> that come with
version C<2.008>, but we can set our own. Consider the following
I<starting> example (it will need some tweaking as we will see):

   # THIS WORKS BUT CAN BE ENHANCED!

   #!/usr/bin/env perl
   use v5.24;
   use warnings;
   use English;
   use experimental qw< signatures >;
   use Mojo::UserAgent;

   use App::Easer::V2 qw< run >;

   my $app = {
      aliases => [qw< MAIN >],
      help    => 'An example',
      sources => 'v2.008',
      config_hash_key => 'v2.008',
      options => [
         {
            getopt => 'foo=s',
            help   => 'an example command-line option',
            transmit => 1,
         },
         {
            getopt => 'bar=s',
            help   => 'another example command-line option, with env',
            environment => 'BAR',
            transmit => 1,
         },
         {
            help        => 'URL for additional configurations',
            environment => 'CONFIG_URL',
            default     => 'https://dummyjson.com/c/12ec-1af6-4270-8dc3',
         },
      ],
      sources => {
         current => [ qw< +CmdLine +Environment +Default +ParentSlices >, \&get_from_url ],
         #next    => [ qw< +CmdLine +Environment +Default +ParentSlices >],
         final   => [],
      },
      children => [
         {
            aliases => [qw< seeker >],
            help    => 'some add-on to look at the seed!',
            description => '',
            options => [ '+parent' ],
            execute => sub ($self) {
               say App::Easer::V2::dd(config => $self->config_hash);
               return 0;
            },
         },
      ],
   };

   exit(run($app, $0, @ARGV) // 0);

   sub get_from_url ($cmd, $opts, $args) {
      my $url = $cmd->config('config_url');
      warn "getting stuff from $url...\n";
      my $ua = Mojo::UserAgent->new;
      return $ua->get($url)->result->json;
   }

This example command loads additional configuration from a URL that
serves a JSON file (I hope the default value continues to work for some
time, but you get the idea anyway). Option C<config_url> holds this URL,
and it can only be set from the environment variable C<CONFIG_URL>
(which, by the way, also gives the option its name as a lowercase
representation) or from the default value.

Key C<sources> is set like this:

   sources => {
      current => [ qw< +CmdLine +Environment +Default +ParentSlices >,
         \&get_from_url ],
   },

The string sources are the default ones: command-line, then environment,
then defaults, then parent. The usual stuff.

The final source, though, is a reference to a sub that, when called,
provides back the desired configuration, getting it dynamically from the
URL:

   sub get_from_url ($cmd, $opts, $args) {
      my $url = $cmd->config('config_url');
      warn "getting stuff from $url...\n";
      my $ua = Mojo::UserAgent->new;
      return $ua->get($url)->result->json;
   }

This I<custom source> must adhere to the above signature, i.e.
receiving:

=over

=item *

a reference to the command object (it's the one invoking the source);

=item *

a reference to an array of options for the source. In this case we just
provided the source as a I<bare> sub reference, so this is an empty
array;

=item *

lib/App/Easer/Tutorial/V2_008.pod  view on Meta::CPAN

      my $ua = Mojo::UserAgent->new;
      return $ua->get($url)->result->json;
   }

There's a cleaner way, as we will see shortly.

=head3 Setting C<sources> for children

To cope with these situation in which I<one is enough>, the C<2.008>
hash-based interface for C<sources> supports setting a C<next> key too,
providing the C<sources> to be set for each child that has not its own
yet. Let's see how to set it:

   #!/usr/bin/env perl
   use v5.24;
   use warnings;
   use English;
   use experimental qw< signatures >;
   use Mojo::UserAgent;

   use App::Easer::V2 qw< run >;

   my $app = {
      aliases => [qw< MAIN >],
      help    => 'An example',
      config_hash_key => 'v2.008',
      options => [
         {
            getopt => 'foo=s',
            help   => 'an example command-line option',
            transmit => 1,
         },
         {
            getopt => 'bar=s',
            help   => 'another example command-line option, with env',
            environment => 'BAR',
            transmit => 1,
         },
         {
            help        => 'URL for additional configurations',
            environment => 'CONFIG_URL',
            default     => 'https://dummyjson.com/c/12ec-1af6-4270-8dc3',
         },
      ],
      sources => {
         current => [ qw< +CmdLine +Environment +Default +ParentSlices >,
            \&get_from_url ],

         #################################################################
         # (default) sources for the children
         next => [ qw< +CmdLine +Environment +Default +ParentSlices >],

      },
      children => [
         {
            aliases => [qw< seeker >],
            help    => 'some add-on to look at the seed!',
            description => '',
            options => [ '+parent' ],
            execute => sub ($self) {
               say App::Easer::V2::dd(config => $self->config_hash);
               return 0;
            },
         },
      ],
   };

   exit(run($app, $0, @ARGV) // 0);

   sub get_from_url ($cmd, $opts, $args) {
      my $url = $cmd->config('config_url');
      warn "getting stuff from $url...\n";
      my $ua = Mojo::UserAgent->new;
      return $ua->get($url)->result->json;
   }

It usage is easy: whatever is put, it's also used as the default
list of C<sources> in the children. In this way we can get rid of the
custom source as soon as we exit from the parent command:

   # Now "getting stuff from https://..." is invoked only once
   $ remote-config-example --bar whatever seeker --foo BARBARBAR
   getting stuff from https://dummyjson.com/c/12ec-1af6-4270-8dc3...
   $VAR1 = {
     'config' => {
        'bar' => 'whatever',
        'baz' => 'galook',
        'config_url' => 'https://dummyjson.com/c/12ec-1af6-4270-8dc3',
        'foo' => 'BARBARBAR'
     }
   };

=head2 Pass 6: C<final> in C<sources>

As we saw before in L<< Pass 4|/Pass 4: C<final_commit> >>, there are
times where we need to go down the line up to the collection of all
options in order to figure out possible additional actions (in that
case, it was setting the right logging level).

What if we need this for custom sources too? In the previous section
example, we astutely get our configuration URL from the environment, but
what if we want to support it as a command-line option and moreover we
want also to propagate (via I<inheritance>) that option in children?

In this case, our previous setup would I<not> work. Whatever we set as
C<current> in the root command will be run when the root command is
analyzed, which happens before the child command.

There are a couple of solutions here. One is to leverage C<final_commit>
as in L<< Pass 4|/Pass 4: C<final_commit> >>, i.e. moving the code for
dynamic loading of the URL inside C<final_commit> and use
C<inject_config> (like in L<< Pass 3|/Pass 3: C<commit> options along
the way >>) to add the newly downloaded configurations.

There is a cleaner approach, though, which consists in using key
C<final> inside the C<sources> hash, like in the following example:

   #!/usr/bin/env perl
   use v5.24;
   use warnings;
   use English;
   use experimental qw< signatures >;
   use Mojo::UserAgent;

   use App::Easer::V2 qw< run >;

   my $app = {
      aliases => [qw< MAIN >],
      help    => 'An example',
      config_hash_key => 'v2.008',
      options => [
         {
            getopt => 'foo=s',
            help   => 'an example command-line option',
            transmit => 1,
         },
         {
            getopt => 'bar=s',
            help   => 'another example command-line option, with env',
            environment => 'BAR',
            transmit => 1,
         },
         {
            ###############################################################
            # Option is promoted to the command-line and made available to
            # children too
            getopt      => 'config_url=s',
            transmit    => 1,
            # try https://dummyjson.com/c/b318-383e-43df-acd6 from cmd line

            help        => 'URL for additional configurations',
            environment => 'CONFIG_URL',
            default     => 'https://dummyjson.com/c/12ec-1af6-4270-8dc3',
         },
      ],
      sources => {

         ##################################################################
         # current is restored to its original, default setting for v2.008
         current => [ qw< +CmdLine +Environment +Default +ParentSlices > ],

         ##################################################################
         # the custom source is moved into final
         final   => [ \&get_from_url ],
      },
      children => [
         {
            aliases => [qw< seeker >],
            help    => 'some add-on to look at the seed!',
            description => '',
            options => [ '+parent' ],
            execute => sub ($self) {
               say App::Easer::V2::dd(config => $self->config_hash);
               return 0;
            },
         },
      ],
   };

   exit(run($app, $0, @ARGV) // 0);

   sub get_from_url ($cmd, $opts, $args) {
      my $url = $cmd->config('config_url');
      warn "getting stuff from $url...\n";
      my $ua = Mojo::UserAgent->new;
      return $ua->get($url)->result->json;
   }

Sample calls;

   # set config_url in the root command
   $ remote-example \
      --config_url https://dummyjson.com/c/b318-383e-43df-acd6 --bar whatever \
      seeker --foo BARBARBAR
   getting stuff from https://dummyjson.com/c/b318-383e-43df-acd6...
   $VAR1 = {
     'config' => {
       'bar' => 'whatever',
       'baz' => 'Galook for the win!',
       'config_url' => 'https://dummyjson.com/c/b318-383e-43df-acd6',
       'foo' => 'BARBARBAR'
     }
   };
   
   # ditto, remove "--bar" from command line. Only the "new" remote JSON
   # config has key "bar" set to the displayed value, so we know it's it
   $ remote-example --config_url https://dummyjson.com/c/b318-383e-43df-acd6 \
      seeker --foo BARBARBAR
   getting stuff from https://dummyjson.com/c/b318-383e-43df-acd6...
   $VAR1 = {
     'config' => {
       'bar' => 'whateeeeever!',
       'baz' => 'Galook for the win!',
       'config_url' => 'https://dummyjson.com/c/b318-383e-43df-acd6',
       'foo' => 'BARBARBAR'
     }
   };
   
   # set config_url in the child command. Again, that value for "bar" comes
   # from the JSON provided on the command line
   $ remote-example \
      seeker \
         --foo BARBARBAR \
         --config_url https://dummyjson.com/c/b318-383e-43df-acd6
   getting stuff from https://dummyjson.com/c/b318-383e-43df-acd6...
   $VAR1 = {
     'config' => {
       'bar' => 'whateeeeever!',
       'baz' => 'Galook for the win!',
       'config_url' => 'https://dummyjson.com/c/b318-383e-43df-acd6',
       'foo' => 'BARBARBAR'
     }
   };


=head2 Pass 7: Getting intermediates to work

So far in these tutorial passes we assumed that root/intermediate
commands are only a way to structure our tree, while the real execution
is performed by the leaves of our commands tree. This is basically why
you get the C<help>/C<usage>/C<commands>/C<tree> sub-commands out of the
box for gree for all non-leaf commands, as well as a default to the
C<usage> sub-command in case no sub-command is provided on the command
line.

Well, you might beg to differ.

The first step is, of course, defining an C<execute> callback for
actually I<doing> something when we determine that the non-leaf command
should be run. And yet this is not enough, as the default is to call
C<usage> in case no sub-command can be found.

=head2 Setting a C<default_child> different fro C<usage>

It turns out this behaviour is not hardcoded, but the effect of the
default on the C<default_child> key in the applications' definition. As
an example:

   #!/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;
            },
         },
      ],

      #####################################################################
      # this sets what's done *by* the root command
      execute => sub ($self) {
         say 'MAIN (root) here!';
         return 0;
      },

      #####################################################################
      # this makes the command itself the default command to call when
      # nothing more is provided on the command line. The default value
      # is 'usage'.
      default_child => '-self',

   };

   exit(run($app, $0, @ARGV) // 0);

Sample calls:

   $ root-exec foo
   foo here!

   $ root-exec bar
   bar here!

   $ root-exec
   MAIN (root) here!


=head3 You might also want to set C<fallback_to>...

While the example in the previous section works, it's still a bit
fragile, because it makes the upper command able to run with regular
options but not with non-option command-line arguments (i.e. those that
end up populating C<residual_args>):

   $ root-exec galook
   cannot find sub-command 'galook'

This happens because L<App::Easer> defaults to looking for a child
command and complains under the assumption that the user I<might> have
mistyped a sub-command's name.

Again, this is not hardcoded but the effect of a configuration option,
namely C<fallback_to>. By setting it to C<-self> you can ask for using
the command itself as the fallback in case no sub-command can be found:

   #!/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;
      },
      default_child => '-self',

      #####################################################################
      # this sets the MAIN command as the default command to run if no
      # child is found when additional residual-args are provided on the
      # command line
      fallback_to   => '-self',
   };

   exit(run($app, $0, @ARGV) // 0);

This works now:

   $ root-exec-with-fallback galook burp
   MAIN (root) here! Also got (galook burp)

It's also possible to set C<fallback_to> to string C<-default> to just
replicate whatever is set for C<default_child> (should you ever change
your mind and want the two mirror each other).

If you need more flexibility, take a look at key C<fallback> in the main
documentation.


=head3 Why was this executed?

L<App::Easer> does its best to figure out which command/sub-command
should be executed. As we saw in the previous sub-sections, it might
have different reasons for running a specific command, be it because
it's the default or a fallback. If you need it, you can call
C<execution_reason> to figure out why, receiving back a string among
C<-leaf>, C<-default>, or C<-fallback>.


=head2 Pass 8: C<aliases> and C<call_name>

Each command in L<App::Easer> supports a C<name> to set the command's
name. Many times, though, it's useful to also support I<aliases> for a
command, e.g. if you want your users to call sub-command C<list> with a
shorter version C<ls>.

Key C<aliases> in the command's specification allows setting these
aliases. As a matter of fact, you might just set it and forget about
C<name>, which will be set to the first alias in case. You decide what's
best for you:

   my $app1 = {
      name => 'foo',
      aliases => [qw< bar baz >],
      ...
   };

   my $same_as_app1 = {
      aliases => [qw< foo bar baz >],
      ...
   };

Sometimes you might want to provide different behaviours for different
aliases, but the underlying implementation is basically the same
(including e.g. command-line options) and you don't want to fire up two
different sub-commands. In this case, you can call method C<call_name>
on the command provided in the callback to figure out how the
sub-command was actually invoked.

Example:

   #!/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 bar >],
            execute => sub ($self) {
               my $name = $self->call_name;
               say "$name here!";
               return 0;
            },
         },
      ],
   };

   exit(run($app, $0, @ARGV) // 0);

Sample calls:

   $ check-name foo
   foo here!

   $ check-name bar
   bar here!

   # let's double check that it does not syphon up everything!
   $ check-name baz
   cannot find sub-command 'baz'

The C<call_name> method also works at the top level, allowing to create
top-level (root) commands that have their own behaviour (see previous
Pass) as well as the possibility to change it depending on how they were
called. The only caveat in this case is that you will get the I<full
path> to the executable (or a link to it), so you might want to account
for it:

   #!/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 bar >],
            execute => sub ($self) {
               my $name = $self->call_name;
               say "$name here!";
               return 0;
            },
         },
      ],
      execute => sub ($self) {
         my $path = $self->call_name;
         my $name = $path =~ s{\A.*/}{}rmxs;
         my @args = $self->residual_args;
         say "$name (root) here! Also got (@args)";
         return 0;
      },
      default_child => '-self',
      fallback_to   => '-self',
   };

   exit(run($app, $0, @ARGV) // 0);


Let's see it in action:

   # let's first create some aliases for our toy example
   $ ln -s example-command main-x 
   $ ln -s example-command main-y

   $ main-x galook
   main-x (root) here! Also got (galook)

   $ main-y burp
   main-y (root) here! Also got (burp)


=head2 Pass 9: More about help

L<App::Easer> comes with a pre-defined help system that makes it
possible to set a short and a long help description, while leaving to
L<App:Easer> the burden to document all options and possibly expose
available sub-commands. There are some defaults and assumptions that you
might want to change, though.

=head3 Invoking the help

The basic assumption in L<App::Easer> is that intermediate commands
(i.e. commands with children) also get three more sub-commands for free,
namely C<usage>, C<help>, C<commands>, and C<tree>. I'd argue that
C<usage> and C<help> are the most interesting of the lot.

This means that your interface might expose a I<slight> inconsistency in
how help is obtained. Assuming our application has the main entry point
with child C<foo>, which in turn has a child C<bar>, we would get the
following out of the box:

   $ main-executable
   # prints out the usage for main-executable

   $ main-executable help
   # prints out the help for main-executable, i.e. the same as usage but
   # with the Description section included

   $ main-executable help foo
   # prints out the help for sub-command foo

   $ main-executable foo help
   # same as above

   $ main-executable foo help bar
   # prints out the help for sub-sub-command bar

You see? There is no C<main-executable foo bar help> like we have for
upper-level commands, because we're assuming that leaf commands should

lib/App/Easer/Tutorial/V2_008.pod  view on Meta::CPAN


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;
      },
      default_child => '-self',

      #####################################################################
      # this sets the MAIN command as the default command to run if no
      # child is found when additional residual-args are provided on the
      # command line
      fallback_to   => '-self',
   };

   exit(run($app, $0, @ARGV) // 0);


The equivalent class-based implementation is based on 4 different parts,
i.e. an entry point program and three classes. They can still be kept
inside the same file, although you might want to split them into each
own file:

   #!/usr/bin/env perl
   use v5.24;
   use warnings;
   use English;
   use experimental qw< signatures >;

   exit(MyApp->new->run($0, @ARGV) // 0);

   # class for the root command
   package MyApp;
   use App::Easer::V2 -command => -spec => {
      aliases => [qw< MAIN >],
      sources => 'v2.008',
      config_hash_key => 'v2.008',
      default_child => '-self',
      fallback_to   => '-self',
   };

   sub execute ($self) {
      my @args = $self->residual_args;
      say "MAIN (root) here! Also got (@args)";
      return 0;
   };


   package MyApp::CmdFoo;
   use App::Easer::V2 -command => -spec => {
      aliases => [qw< foo >],
   };

   sub execute ($self) {
      say 'foo here!';
      return 0;
   };


   package MyApp::CmdBar;
   use App::Easer::V2 -command => -spec => {
      aliases => [qw< bar >],
   };

   sub execute ($self) {
      say 'bar here!';
      return 0;
   };

This setup allows a seamless transition of features from the hash-based
approach to the method-based one. As long as your application traits are
plain data (like C<aliases>, C<help>, etc.) it's possible to treat them
as such and keep them inside the hash provided as argument for C<use>;
everything different can be treated through a method.

As an example, you might want to keep help as POD in the file, and use
some POD-handling code to get it. In this case you might want to move
either C<help> or C<description> (or both) into their own methods.

Sub-command classes must adhere to a naming convention; by default,
their last-part name must start with string C<Cmd> but this can be set
via key C<children_prefixes> (see the main reference documentation for
the details). This allows having command and non-command classes inside
the same directory (i.e. at the same package level) without
intereference.


=head1 AUTHOR

Flavio Poletti <flavio@polettix.it>

=head1 COPYRIGHT AND LICENSE

Copyright 2024 by Flavio Poletti <flavio@polettix.it>

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.



=cut





( run in 0.435 second using v1.01-cache-2.11-cpan-d7a12ab2c7f )