App-Easer

 view release on metacpan or  search on metacpan

docs/docs/10-tutorial-base.md  view on Meta::CPAN

---
title: 'Tutorial: a to-do application'
layout: default
---

# Tutorial: a to-do application

In this tutorial we will take a look at an example application, i.e. a
small to-do application to manage tasks from the command line. This is
the same application that is available inside the `eg` sub-directory of
the [App::Easer][] package.

## Boilerplate and high level structure

Let's start with setting up the skeleton for our application:

```perl
#!/usr/bin/env perl
use v5.24;
use warnings;
use experimental 'signatures';
no warnings 'experimental::signatures';
use App::Easer 'run';
 
my $application = {
   factory       => {prefixes => {'#' => 'TuDu#'}},
   configuration => {
      'auto-leaves'    => 1,
      'help-on-stderr' => 1,
   },
   commands => {
      MAIN   => { ... },
      dump   => { ... },
      list   => { ... },
      show   => { ... },
      cat    => { ... },
      add    => { ... },
      edit   => { ... },
      done   => { ... },
      wait   => { ... },
      resume => { ... },
      remove => { ... },
   },
};
exit run($application, [@ARGV]);

package TuDu;
use Path::Tiny 'path';
use POSIX 'strftime';
...
```

The `factory` top level configuration lays the ground for putting the
implementation inside a separate package `TuDu`. That particular prefix
setting is used as follows:

- a name like `#foobar` will be turned into `TuDu#foobar`;
- this will be resolved into function `TuDu::foobar` (i.e. function
  `foobar` inside package `TuDu`.

As a result, we have a shortcut to point towards functions inside the
`TuDu` package for our implementations.

We are setting a couple of high-level configurations:

- `auto-leaves`: every command without explicit children will be treated
  as a leaf command, so it will not get a `help` and a `commands`
  sub-commands;
- `help-on-stderr`: help messages (from `help` and `commands`) will be
  printed on standard error instead of standard output. This makes it
  more difficult to pipe them through a pager (like `more` or `less`),
  but avoids that the help messages might be accidentally considered
  part of the "real" output of the command.

The rest of the `$application` hash reference is initialized with a
skeleton of all the sub-commands that we aim to support. The structure
is pretty flat - all "real" sub-commands are in fact children to the
`MAIN` entry point.

## Setting the MAIN entry point

Let's flesh out the `MAIN` entry point. This will collect the *global*
configuration options (e.g. where the configuration file is placed,
where to place the tasks, etc.) as well as doing some housekeeping to
ease the work of the "real" commands:

```perl
# inside hash at $application->{commands}:
MAIN => {
   help        => 'to-do application',
   description => 'A simple to-do application',

   children => [qw< list show cat add edit done wait resume remove >],

   sources        => '+SourcesWithFiles',
   'config-files' => ["$ENV{HOME}/.tudu.conf", '/etc/tudu.conf'],
   options     => [
      {
         help        => 'path to the configuration file',
         getopt      => 'config|c=s',
         environment => 'TUDU_CONFIG',
      },
      {
         help        => 'base directory where tasks are kept',
         getopt      => 'basedir|dir|d=s',
         environment => 'TUDU_BASEDIR',
         default     => "$ENV{HOME}/.tudu",
      },
      {
         help   => 'max number of attempts to find non-colliding id',
         getopt => 'attempts|max-attempts|M=i',
         default => 9,
      },
   ],

   commit         => '#ensure_basedir',
},
```

The `help` and `descriptions` are useful ways to provide clues to the
users about how to use this command. Of the two, `help` is used where a
concise description is needed, while `description` is used in the help
page regarding the command itself, so it's generally more verbose.

As anticipated, `children` points to all sub-commands, because this
specific application has a flat hierarchy. For fancier hierarchies,
[Defining commands hierarchy][] is the tutorial to look for.

The next section deals with managing input options. The `sources`

docs/docs/10-tutorial-base.md  view on Meta::CPAN

         default     => 'vi',
      }
   ],
   execute => '#add',
},
```

Keys `help`, `description`, `supports`, and `execute` are exacty as
before.

Options are no surprise too: we already saw them in detail for the
`MAIN` entry point command. The difference here is that, by default,
options are taken from the command line, then the environment, then the
parent command, then the defaults; there is no loading of additional
options from files. This is also what the user expects, anyway.

Other sub-commands `list` and `edit` share the same structure.


## The dump outlier

The example `tudu` application also contains an *outlier* sub-command
`dump`, which is normally excluded from the children list (we would have
to set it explicitly in `MAIN`'s `children` in case).

```perl
# inside hash at $application->{commands}:
dump => { # this child is normally excluded!
   help => 'dump configuration',
   execute => sub ($m, $c, $a) {
      require Data::Dumper;
      warn Data::Dumper::Dumper({config => $c, args => $a});
      return 0;
   },
},
```

In this case we don't need to hand the execution over to `TuDu`, but can
provide it right off the bat with a `sub` reference. This gives us an
idea of how flexible we can be with the *executables*, ranging from
in-site implementation, to reference to other subs, up to putting stuff
in different packages and, possibly, different module files.

## Getting all pieces together

The whole program for our toy `tudu` application is the following,
including all the implementation functions placed in the `TuDu` package:

```
#!/usr/bin/env perl
use v5.24;
use warnings;
use experimental 'signatures';
no warnings 'experimental::signatures';
use App::Easer 'run';

my $application = {
   factory       => {prefixes => {'#' => 'TuDu#'}},
   configuration => {
      'auto-leaves'    => 1,
      'help-on-stderr' => 1,
   },
   commands => {
      MAIN => {
         help        => 'to-do application',
         description => 'A simple to-do application',
         options     => [
            {
               help        => 'path to the configuration file',
               getopt      => 'config|c=s',
               environment => 'TUDU_CONFIG',
            },
            {
               help        => 'base directory where tasks are kept',
               getopt      => 'basedir|dir|d=s',
               environment => 'TUDU_BASEDIR',
               default     => "$ENV{HOME}/.tudu",
            },
            {
               help   => 'max number of attempts to find non-colliding id',
               getopt => 'attempts|max-attempts|M=i',
               default => 9,
            },
         ],
         sources        => '+SourcesWithFiles',
         'config-files' => ["$ENV{HOME}/.tudu.conf", '/etc/tudu.conf'],
         commit         => '#ensure_basedir',
         children => [qw< list show cat add edit done wait resume remove >],
      },
      dump => { # this child is normally excluded!
         help => 'dump configuration',
         execute => sub ($m, $c, $a) {
            require Data::Dumper;
            warn Data::Dumper::Dumper({config => $c, args => $a});
            return 0;
         },
      },
      list => {
         help        => 'list tasks',
         description => 'Get full or partial list of tasks',
         supports    => [qw< list ls >],
         options     => [
            {
               help => 'include all tasks (including done) '
                 . '(exclusion is not honored)',
               getopt => 'all|A!',
            },
            {
               help => 'include(/exclude) all active tasks '
                 . '(ongoing and waiting)',
               getopt => 'active|a!',
            },
            {
               help   => 'include(/exclude) done tasks',
               getopt => 'done|d!',
            },
            {
               help   => 'include(/exclude) ongoing tasks',
               getopt => 'ongoing|o!',
            },
            {



( run in 0.905 second using v1.01-cache-2.11-cpan-39bf76dae61 )