The London Perl and Raku Workshop takes place on 26th Oct 2024. If your company depends on Perl, please consider sponsoring and/or attending.

NAME

App::Task - Nest tasks w/ indented output and pre/post headers

VERSION

This document describes App::Task version 0.02

SYNOPSIS

    use App::Task;

    task "…" => sub {};
    task "…" => ["system","args","here"], {fatal => 1};
    task "…" => "system command here";

Nested

    task "…" => sub {
        task "…" => sub {
            task "…" => sub { … };
        };
        task "…" => sub { … };
        task "…" => sub {
            task "…" => sub {
                task "…" => sub { …  };
            };
        };
    }

DESCRIPTION

This allows us to create scripts that organize tasks together and have their output be organized similarly and with added clarity.

It does this by wrapping each task in a starting/ending output line, indenting output congruent with nested task() depth.

For example, say this:

    system(…);
    foo();
    system(…,…);
    say test_foo() ? "foo is good" : "foo is down";

outputs this:

    Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
    Nunc mi ipsum faucibus vitae aliquet nec ullamcorper sit amet.
    Facilisi morbi tempus iaculis urna id volutpat lacus laoreet.
    Ullamcorper eget nulla facilisi etiam dignissim diam.
    Maecenas volutpat blandit aliquam etiam erat velit scelerisque in dictum.
    foo is good

Nothing wrong with that but it could be easier to process visually, so if we task()’d it up a bit like this:

    task "setup foo" => sub {
        task "configure foo" => "…";
        task "run foo" => \&foo;
    };

    task "finalize foo" => sub {
        task "enable barring" => […,…];
        task "verify foo" => sub {
            my $status = test_foo();
            say $status ? "foo is good" : "foo is down";
            return $status;
        };
    };

Now you get:

    ➜➜➜➜ [1.1] setup foo …
        ➜➜➜➜ [2.1] configure foo …
            Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
         … done (configure foo).

        ➜➜➜➜ [2.2] run foo …
            Nunc mi ipsum faucibus vitae aliquet nec ullamcorper sit amet.
            Facilisi morbi tempus iaculis urna id volutpat lacus laoreet.
         … done (run foo).

     … done (setup foo).

    ➜➜➜➜ [1.2] finalize foo …
        ➜➜➜➜ [2.1] enable barring …
            Ullamcorper eget nulla facilisi etiam dignissim diam.
            Maecenas volutpat blandit aliquam etiam erat velit scelerisque in dictum.
         … done (enable barring).

        ➜➜➜➜ [2.2] verify foo …
            foo is down
         … failed (verify foo).

     … done (finalize foo).

INTERFACE

Each variant has a pre/post heading and indented output.

task NAME => CODEREF

If CODEREF returns true the post heading will be “done”, if it returns false it will be “failed”.

To make CODEREF fatal just throw an exception.

task NAME => CMD_STRING

CMD_STRING is a string suitable for system(CMD_STRING).

If the command exits clean the post heading will be “done”, if it exits unclean it will be “failed”.

To make CMD_STRING exiting unclean be fatal you can set fatal to true in the optional 3rd argument to task:

    task "prep fiddler" => "/usr/bin/fiddler prep"; # will continue on unclean exit
    task "create fiddler" => "/usr/bin/fiddler new", { fatal => 1 }; # will die on unclean exit

task NAME => CMD_ARRAYREF

task NAME => CMD_ARRAYREF is a string suitable for system(@{CMD_ARRAYREF}).

If the command exits clean the post heading will be “done”, if it exits unclean it will be “failed”.

To make CMD_STRING exiting unclean be fatal you can set fatal to true in the optional 3rd argument to task:

    task "prep fiddler" => ["/usr/bin/fiddler", "prep"]; # will continue on unclean exit
    task "create fiddler" => ["/usr/bin/fiddler", "new"], { fatal => 1 }; # will die on unclean exit

$ENV{App_Task_DRYRUN}

If this is true the CMD_ARRAYREF and CMD_STRING version of task() will out put a DEBUG string of the command it would have run.

You can then check it in your application’s code to do things differently in a dry run mode. An example is found in the "App::Task::tie_task()" example function below.

App::Task::tie_task()

Not exported or exportable.

Take no arguments. Returns the tied() objects for STDOUT, STDERR.

    App::Task::tie_task();
    my ($o, $e) = App::Task::tie_task();

Dies if you call it and STDOUT or STDERR are already tied.

Some modules don’t play well with tied STDOUT/STDERR. To get them to work you need to do some wrapping to essentially:

redefine the thing in question to do this pseudo code logic:

    if (SDTDOUT/STDERR are tied) {
        untie STDOUT/STDERR
        do the original thing
        reset STDOUT/STDERR by calling C<App::Task::tie_task()>
    }
    else {
        do the original thing
    }

For example, Git::Repository is an excellent tool. However if you do this:

    my $git = get_git_obj($CWD);
    task "doing some git stuff" => sub {
        $git->run(checkout => "-b", $branchname);
        my $user = $git->run( "config", "--get", "user.name" );
        …
    };

A few things go wonky, the most obvious is that the config call will output the result, unindented, to the screen instead of $user being populated with it.

To make it play nice we change our get_git_obj() function to look like this (including the Git::Repository::Plugin::Dirty plugin).

    my $git;

    sub get_git_obj {
        my ( $work_tree, $verbose ) = @_;

        if ( !$git ) {
            require Git::Repository;
            Git::Repository->import('Dirty');

            my $real_run   = \&Git::Repository::run;
            my $real_dirty = \&Git::Repository::is_dirty;
            no warnings "redefine";
            *Git::Repository::run = sub {
                if ( !$ENV{App_Task_DRYRUN} ) {
                    if ( tied *STDOUT || tied *STDERR ) {
                        untie *STDOUT;
                        untie *STDERR;
                        if ( defined wantarray ) {
                            my ( @rv, $rv );
                            if   (wantarray) { @rv = $real_run->(@_) }
                            else             { $rv = $real_run->(@_) }
                            App::Task::tie_task();
                            return wantarray ? @rv : $rv;
                        }
                        else {
                            $real_run->(@_);
                        }
                        App::Task::tie_task();
                        return;
                    }
                    else {
                        goto &$real_run;
                    }
                }

                shift;
                print "(DRYRUN) >_ git " . join " ", map {
                    if (m/ /) { s/'/\\'/g; $_ = "'$_'" }
                    $_
                } @_;
                print "\n";
            };
            *Git::Repository::is_dirty = sub {
                return if $ENV{App_Task_DRYRUN};
                goto &$real_dirty;
            };
        }

        if ( !$git->{$work_tree} ) {
            $git->{$work_tree} = Git::Repository->new( { fatal => ["!0"], quiet => ( $verbose ? 0 : 1 ), work_tree => $work_tree } );
        }

        return $git->{$work_tree};
    }

DIAGNOSTICS

Throws no warnings or errors of its own.

CONFIGURATION AND ENVIRONMENT

App::Task requires no configuration files or environment variables.

DEPENDENCIES

Text::OutputFilter, Tie::Handle::Base, IPC::Open3::Utils, IO::Interactive::Tiny

INCOMPATIBILITIES AND LIMITATIONS

The indentation is not carried across forks (patches to not use Tie welcome!). That means, for example, if you call system() directly inside a task it will not be indented.

BUGS AND FEATURES

Please report any bugs or feature requests (and a pull request for bonus points) through the issue tracker at https://github.com/drmuey/p5-App-Task/issues.

AUTHOR

Daniel Muey <http://drmuey.com/cpan_contact.pl>

LICENCE AND COPYRIGHT

Copyright (c) 2018, Daniel Muey <http://drmuey.com/cpan_contact.pl>. All rights reserved.

This module is free software; you can redistribute it and/or modify it under the same terms as Perl itself. See perlartistic.

DISCLAIMER OF WARRANTY

BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE SOFTWARE "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE IS WITH YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR, OR CORRECTION.

IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE SOFTWARE AS PERMITTED BY THE ABOVE LICENCE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE SOFTWARE (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.