Experimenting with App::Spec

I recently attended a tech meeting of London Perl Mongers (strongly recommended if you happen to be a Perl developer by the way). Amongst other things, I was introduced to App::Spec. App::Spec is a tool that allows you to specify your program’s commands, parameters, options, the values they can take, etc. with through a yaml file. Not only that, but it can also generate a bash file that provides tab-completion, which is clearly the most important thing in the world.

(If you’re interested in the tiny app I wrote for the blog post, the full code is at https://github.com/errietta/AppSpec-Example)

To get started, you need App::AppSpec (confusingly), which provides the commandline tool, appspec. Once you have it, ‘man appspec’ is a good companion on how to use it, but the most basic thing, creating a skeleton app, can be done with just:

appspec new --class App::Converter --name converter.pl

(Obviously replace your classname and script name).

This will generate everything you need to get started:

$ tree
.
├── bin
│   └── converter.pl
├── lib
│   └── App
│       └── Converter.pm
└── share
    └── converter.pl-spec.yaml


4 directories, 3 files

Let’s create a basic unit converter, that converts between centimetres, metres, and kilometres (Those who prefer imperial measures are free to modify as required :P)

What I want to do in the end is to be able to do

perl -Ilib ./bin/converter.pl convert --from km --to cm 100

To convert 100 km to cm, for example.

After experimenting, I have modified the YAML (share/converter.pl-spec.yaml) file thusly:

name: converter.pl 
appspec: { version: '0.001' }
class: App::Converter
title: 'app title'
description: 'app description'


subcommands:
  convert:
  summary: Convert
  op: convert
  options:
  -
    name: "from"
    type: "string"
    summary: "Unit to convert from (cm, m, km)"
    required: true
    values:
    enum:
      - cm
      - m
      - km 
    completion: true
 -
    name: "to"
    type: "string"
    summary: "Unit to convert to (cm m, km)"
    required: true
    values:
    enum:
      - cm
      - m
      - km
    completion: true
  parameters:
  -
    name: amount
    summary: The amount to convert
    required: true
    type: integer

The ‘subcommand’ is what gives my program the ability to do ./bin/convert.pl convert. Right now, my program does only one thing, but I could add more ‘subcommands’ in the future.

The options and parameters are pretty self-explanatory, other than the ‘values’ part. I couldn’t find out what goes in those values, so after looking at the examples and applying a healthy dose of RTFS it turns out they can be ‘op’ (which will call a function with the same name in your module to retrieve an arrayref of parameters/options), ‘ enum’ (which requires an array of possible values in the yaml file), and ‘mapping’ which takes key/value pairs of options.

In this case, I just used enums, which is the simplest option.

When I run ./bin/convert.pl now, it will automatically require the specific options and values. For example, if I run without an amount, it will exit with the following:

Usage: converter.pl  convert  [options]


Parameters:
amount * The amount to convert


Options:
--from * Unit to convert from (cm, m, km)
--help -h Show command help (flag)
--to * Unit to convert to (cm, m, km)
Error: parameter 'amount': missing
An example of error messages output after running the script with incorrect parameters.
It also provides pretty colours!

 

Now that my spec is ready, it’s time to write my actual program.

Not much to mention here, really. The only things to keep in mind is that it needs to subclass ‘App::Spec::Run::Cmd’. Then, everything that has an ‘op’ in the yaml file (for example  my ‘convert’ operation has an op of ‘convert’) needs to have a subroutine with the same name in the module. Finally, it will be passed ($self, $run), where $run (An App::Spec::Run object) can be used to retrieve ->options and ->parameters amongst other things.

 

package App::Converter;
use strict;
use warnings;
use feature qw/ say /;
use base 'App::Spec::Run::Cmd';

sub convert {
  my ($self, $run) = @_;
  my $options = $run->options;
  my $parameters = $run->parameters;

  my $to = $options->{to};
  my $from = $options->{from};


  my $amount = $parameters->{amount};

  my $multiply = 1;

  # Convert to CM first

  if ($from eq 'm') {
   $multiply = 100;
  } elsif ($from eq 'km') {
   $multiply = 1000000;
  }

  my $cm = $amount * $multiply;

  $multiply = 1;

  if ($to eq 'm') {
   $multiply = 1/100;
  } elsif ($to eq 'km') {
   $multiply = 1/1000000;
  }

  my $answer = $cm * $multiply;

  say $answer;
}

1;

(Kind of a dumb programme, but it’s just a proof of concept).

As for bin/converter.pl, I pretty much left it at the default, just made sure the $specfile was pointing to the right file.

Using my script now yields resutls:

perl -Ilib ./bin/converter.pl  convert --from cm --to m 100
1
perl -Ilib   ./bin/converter.pl  convert --from m --to km 100
0.01
perl -Ilib  ./bin/converter.pl  convert --from km --to cm 100
100000000

And finally, we can get to bash completion! App::AppSpec can be used to generate a completion script:

$ appspec completion share/converter.pl-spec.yaml --bash  >completion.sh # Ignore the errors..

Then you can do :

source completion.sh

And now your ./bin/converter.pl will have auto completion!

$ export PERL5LIB=$PERL5LIB:/home/errietta/App-Converter/lib
$ chmod +x bin/converter.pl

./bin/converter.pl 
convert -- Convert 
help -- Show command help 

./bin/converter.pl convert --
--from -- Unit to convert from (cm, m, km) 
--help -- Show command help 
--to -- Unit to convert to (cm, m, km)

In closing, App::Spec makes it easy to define your script’s parameters and options with a yaml file, plus provides some goodies like auto-completion. It’s certainly something I want to look into more; however be warned that it does have ‘Experimental’ written all over it 😛

 

If you’re interested in the tiny app I wrote for the blog post, the full code is at https://github.com/errietta/AppSpec-Example

    Leave a Reply

    Your email address will not be published. Required fields are marked *

    This site uses Akismet to reduce spam. Learn how your comment data is processed.