Applify: Scripting without boilerplate

20th of July, 2012

Applify is a module which helps you write scripts with less boilerplate code. The scripts written with Applify can also be tested easily.

I started out using plain Getopt::Long, but I thought it was clumsy to combine with my OO code. Then I started using Moose and MooseX::Getopt and life was starting to get good – but not quite: The problem was that it took “forever” to load the application. For a long time I didn’t care much about it (since Moose spared me for so much development time), but after having real users on my scripts I was forced to pay attention: Scripts that used to start up instant took about one second to start.

In addition, the MooseX::Getopt scripts I wrote also had a lot of unwanted boilerplate code which I found cumbersome.

Then I looked around on metacpan and found a number of other modules which tried to make writing scripts easy, but I still thought it was too complex to set up. That was when I wanted to try to do my own and the result is Applify.

Highlighs:

  • Less boilerplate.
  • Loads fast.
  • Can extend other perl classes, based on Moose, Moo or any other framework that you like, using extends 'My::Generic::Class';. Note: –help, –version and –man will still be blasting fast.
  • Auto –help, –man and –version if specified.
  • You can embed other scripts using do(). Example on how to call another script: (do $script)->run;
  • Moo and Applify can be used side-by-side.
  • Works with fatpack.

Here is an example application, using Applify. The application reads the battery stats in Linux and prints it out to screen.

#!/usr/bin/perl
use Applify; # import strict and warnings

# define application options:
# --battery /path/to/battery/dir
# --format [human|perl]
option str => battery => 'Path to battery proc dir', '/proc/acpi/battery/BAT0';
option str => format => 'Output format', 'human';

# --version will print "1.23"
version 1.23;

# just a method to read file contents
sub read_proc_files {
    my $self = shift;
    local @ARGV = map { $self->battery ."/$_" } @_;
    map {
        chomp;
        my($key, $value) = /(\w[^:]+):\s*(\w+)/ or next;
        $key =~ s/\s/_/g;
        $key => $value;
    } <>
}

# another method to prepare human readable data
sub proc_data_to_human {
    my($self, $data) = @_;

    if($data->{'present_rate'} > 0) {
        $data->{'time_left'}
            = $data->{'remaining_capacity'} / $data->{'present_rate'};
        $data->{'percent_left'}
            = $data->{'remaining_capacity'} / $data->{'last_full_capacity'} * 100;
    }
    else {
        @$data{qw/ time_left percent_left /} = (-1, -1);
    }
}

# a method that knows how to print the --format human output
sub human_formatting {
    [ charging_state => 'Battery state' => '%s' ],
    [ present_rate => 'Discharge rate' => '%5i mW' ],
    [ remaining_capacity => 'Remaining power' => '%5i mWh' ],
    [ time_left => 'Remaining time' => '%5.2f h' ],
    [ percent_left => 'Remaining percentage' => '%5.2f %%' ],
}

# app {} creates the main application method which is called
# when the script starts.
# Note: $self is not blessed to "main::", but to a class
# generated by Applify
app {
    my($self, @extra) = @_;
    my %data = $self->read_proc_files(qw/ info state /);

    if($self->format eq 'human') {
        $self->proc_data_to_human(\%data);
        for my $f ($self->human_formatting) {
            printf "%-22s $f->[2]\n", $f->[1], $data{$f->[0]};
        }
    }
    elsif($self->format eq 'perl') {
        require Data::Dumper;
        print Data::Dumper::Dumper(\%data);
    }
    else {
        die "Unknown output format: ", $self->format, "\n";
    }

    return 0; # need to return an exit value, or Applify will barf
};

Here is an example unittest:

use strict;
use warnings;
use Test::More;

my $application = do 'script/battery.pl'
    or BAIL_OUT "Could not read script/battery.pl: $@";
my $script = $application->_script;

isa_ok $script, 'Applify';
can_ok $application, qw/ run human_formatting read_proc_files proc_data_to_human /;

is $script->options->[0]{'name'}, 'battery', '--battery option';
is $script->options->[0]{'default'}, '/proc/acpi/battery/BAT0', '..with default';
is $script->options->[1]{'name'}, 'format', '--format option';
is $script->options->[1]{'default'}, 'human', '..with default';

done_testing;

I’m using Applify for my new scripts. Which module will you be using?