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
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.plconvert -- 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