Command line tools in style

celogeek

Mar 4, 2012

Slides made for "French Perl Mongers" show

 

Instructions

  • Press space to see the plan and space again to go the slide.
  • Use arrow to move between slide (left, right, down, up).
  • Click on the code area to zoom in, click again to zoom out.

Perl, a good language to create command line tools

In Perl, it's really easy to create a command line tool.

Example 1: old way to do wrapper to mysql tool

Let's see a simple tool that execute stuff with mysql command line.

Display databases


#!/usr/bin/env perl 
use strict;
use warnings;

my @cmd = ('mysql', '-BNe', 'show databases');
open my $f, '-|', @cmd or die $cmd[0].' not found...';
while(my $line = readline $f) {
        print $line;
}
close $f;

We need login/password


#!/usr/bin/env perl 
use strict;
use warnings;

my ($user,$passwd) = split(/,/,$ARGV[0]) if @ARGV > 0;

my @cmd = ('mysql', '-BNe', 'show databases');
push @cmd, "-u'$user'" if defined $user;
push @cmd, "-p'$passwd'" if defined $passwd;

print join(' ', "Launch:", @cmd)."\n";
open my $f, '-|', @cmd or die $cmd[0].' not found...';
while(my $line = readline $f) {
        print $line;
}
close $f;
#launch with mywrapper.pl LOGIN,PASSWD

Add a filter


#!/usr/bin/env perl 
use strict;
use warnings;

my ($user,$passwd) = split(/,/,$ARGV[0]) if @ARGV > 0;
my $filter = $ARGV[1] if @ARGV > 1;

my @cmd = ('mysql', '-BNe', 'show databases');
push @cmd, "-u'$user'" if defined $user;
push @cmd, "-p'$passwd'" if defined $passwd;

print join(' ', "Launch:", @cmd)."\n";
print "Apply filter: $filter\n" if defined $filter;
open my $f, '-|', @cmd or die $cmd[0].' not found...';
while(my $line = readline $f) {
        next if(defined $filter && $line !~ /$filter/);
        print $line;
}
close $f;
#Launch with mywrapper.pl LOGIN,PASSWD FILTER

It's quite easy but hard to maintain when you got ton of options. We need something better, something that handle the options.

Example 2: Getopt::Long::Descriptive

Extended processing of command line options. This module simplify the creation of tools in Perl.

Now use the module in our example


#!/usr/bin/env perl 
use strict;
use warnings;
use Getopt::Long::Descriptive;

my ($opt, $usage) = describe_options(
    "$0 %o",
    ["user|u=s", "user auth"],
    ["passwd|p=s", "passwd auth"],
    ["filter|f=s", "db filter"],
    ["help|h", "this help"],
);
print($usage->text), exit if $opt->help;

my ($user, $passwd, $filter) = ($opt->user, $opt->passwd, $opt->filter);

#same as before

Now we got this :


$ perl mywrapper.pl -h
mywrapperl.pl [-fhpu] [long options...]
	-u --user       user auth
	-p --passwd     passwd auth
	-f --filter     db filter
	-h --help       this help

Test it with args:


$ perl mywrapper.pl -u root -f ^t
Launch: mysql -BNe 'show databases' -u'root'
Apply filter: ^t
test

Pretty better, let's do it with a class.

Example 3: use a class to keep your params.

It is better to use a class to store your params, it's easier to maintain.

The MyWrapper.pm, a package to handle your tool.


package MyWrapper;
use strict;
use warnings;

sub new {
    my ($proto, %params) = @_; my $class = ref $proto || $proto;
    my $self = \%params;
    $self->{mysql} //= 'mysql';

    bless $self, $class;
}

sub mysql {
    my ($self, $mysql) = @_;
    $self->{mysql} = $mysql if @_ > 1;
    return $self->{mysql};
}

sub user {
    my ($self, $user) = @_;
    $self->{user} = $user if @_ > 1;
    return $self->{user};
}

sub passwd {
    my ($self, $passwd) = @_;
    $self->{passwd} = $passwd if @_ > 1;
    return $self->{passwd};
}

sub filter {
    my ($self, $filter) = @_;
    $self->{filter} = $filter if @_ > 1;
    return $self->{filter};
}

sub run {
    my ($self) = @_;

    my $mysql = $self->mysql;
    my $user = $self->user;
    my $passwd = $self->passwd;
    my $filter = $self->filter;

    my @cmd = ('mysql', '-BNe', 'show databases');
    push @cmd, "-u'$user'" if defined $user;
    push @cmd, "-p'$passwd'" if defined $passwd;

    print join(' ', "Launch:", @cmd)."\n";
    print "Apply filter: $filter\n" if defined $filter;
    open my $f, '-|', @cmd or die $cmd[0].' not found...';
    while(my $line = readline $f) {
            next if(defined $filter && $line !~ /$filter/);
            print $line;
    }
    close $f;
}
1;

The tool now looks like :


#!/usr/bin/env perl 
use strict;
use warnings;
use MyWrapper;
use Getopt::Long::Descriptive;

my ($opt, $usage) = describe_options(
    "$0 %o",
    ["user|u=s", "user auth"],
    ["passwd|p=s", "passwd auth"],
    ["filter|f=s", "db filter"],
    ["help|h", "this help"],
);

print($usage->text), exit if $opt->help;

my $wrapper = MyWrapper->new(user => $opt->user, passwd => $opt->passwd, filter => $opt->filter);
$wrapper->run();

Let's run it :


$ perl MyWrapper.pl --user=root --filter=^t
Launch: mysql -BNe 'show databases' -u'root'
Apply filter: ^t
test

Quite better, but a little bit long and complex.

Example 4: use Moo to simplify the package.

Moo is Minimalist Object Orientation.

Our package MyWrapper.pm with Moo:


package MyWrapper;
use strict;
use warnings;
use Moo;

has mysql => ( is => 'rw',
default => sub {
    'mysql'
}
);
has user => ( is => 'rw' );
has passwd => ( is => 'rw' );
has filter => ( is => 'rw' );

#run method remain the same
1;

It's really simple now. But we can do more. Let's see a module that does everything.

Example 5: MooX::Options, options with an object

MooX::Options, add 'option' keyword for your object.

Your MyWrapper.pm is still simple:


package MyWrapper;
use strict;
use warnings;
use Moo;
use MooX::Options;

#note than I don't use option here, it is not available at command line
has mysql => ( is => 'rw',
default => sub {
    'mysql'
}
);
option user => ( is => 'rw', doc => 'user auth', format => 's' );
option passwd => ( is => 'rw', doc => 'passwd auth', format => 's' );
option filter => ( is => 'rw', doc => 'filter table', format => 's' );

#run remain the same

Your MyWrapper.pl is now really simpler:


#!/usr/bin/env perl 
use strict;
use warnings;
use MyWrapper;
my $wrapper = MyWrapper->new_with_options();
$wrapper->run();

You got this, now :


$ perl MyWrapper.pl -h
USAGE: MyWrapper.pl [-h] [long options...]
	--user        user auth
	--passwd      passwd auth
	--filter      filter table
	-h --help     show this help message

Now you know how to do a command line tool in style.

For Perl programmers

Comparison between MooX::Options and MooseX::GetOpt

Why should you prefer MooX::Options over MooseX::GetOpt ?

  • MooX::Options work with Mo/Moo/Mouse/Moose and any other kind of custom Perl Oriented Object you can make.
  • MooX::Options use special 'option' keyword instead of capturing all 'has' method to do a command line. If you have roles and many stuff with attributes, it became a nightmare to remove it from the scope of MooseX::GetOpt.
  • MooX::Options is very customizable and can adapted to any situation. You can change the 'option' keyword for example, or chain to something else than 'has'.

You've got the idea. Ok, it's finished now.

Thank you!

celogeek

Mar 4, 2012