— my toolbox is filled with perl, javascript, css and html

thorsen.pm

Applify: Scripting without boilerplate

Friday, July 20, 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:

    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?

    blog comments powered by Disqus