Skip to content

Instantly share code, notes, and snippets.

@ishiduca
Created October 30, 2012 02:30
Show Gist options
  • Save ishiduca/3977964 to your computer and use it in GitHub Desktop.
Save ishiduca/3977964 to your computer and use it in GitHub Desktop.
Smart::Options と Data::Validator でコマンドラインツールを書く(仮)

Smart::Options と Data::Validator で コマンドラインツール

ちょっとしたコマンドラインツールを書くのに、Smart::Options は入力データをパースするのに楽ちんです。 なんですけど、もう少し複雑なことをやらせようとするとデータのバリデーションした方がいいなってなる。なので、Data::Validator を通してバリデーションさせてみる

流れ

  • Smart::Optionsでコマンドラインからの入力データをパース
  • パースしたデータはData::Validatorを通してバリデーションを行う
  • パースしたデータのキー名毎に作業をマッピング && 作業させる

はまった

制限ハッシュで、そのハッシュに任意のキーがあるかチェックするのに defined 使ったら "Attempt to access disallowed key 'キー名' in a restricted hash ..." って怒られて止まっちゃった。 exists 使ったらチェックできた

package Cmd;
use strict;
use warnings;
use Data::Validator;
sub new {
my($class, $argv) = @_;
my $args = delete $argv->{_};
bless {
argv => $argv,
args => $args,
}, $class;
}
sub validate {
my $self = shift;
my $validator = Data::Validator->new( @_ );
$self->{argv} = $validator->validate( %{$self->{argv}} );
$self;
}
sub map {
my $self = shift;
my $cb = pop;
my $argv = $self->{argv};
return if scalar(keys %{$argv}) != scalar @_;
return if scalar(grep{ exists $argv->{$_} }@_) != scalar @_;
# 制限Hashなので defined は使えない
# return if scalar(grep{ defined $argv->{$_} }@_) != scalar @_;
$cb->($argv, $self->{args});
exit 0;
}
1;
package main;
use strict;
use warnings;
use Smart::Options;
use Mouse::Util::TypeConstraints;
subtype 'Cmd::TypeWith'
=> as 'Str'
=> where { $_ eq 'm' or $_ eq 'p' or $_ eq 'r' }
=> message { qq("with" option must be "m" or "p" or "r" ) }
;
my $cmd = Cmd->new( Smart::Options->new
->options(
h => { alias => 'help' },
dir => { default => '.' },
ls => { alias => 'list' },
l => { alias => 'loc' },
p => { alias => 'package_json' },
m => { alias => 'module' },
r => { alias => 'readme' },
lib => { alias => 'library' }
)->boolean(qw{
help ls tree
})->parse
);
$cmd->validate(
dir => { isa => 'Str' },
help => { isa => 'Bool', optional => 1 },
list => { isa => 'Bool', optional => 1 },
tree => { isa => 'Bool', optional => 1 },
loc => { isa => 'Str', optional => 1 },
package_json => { isa => 'Str', optional => 1 },
module => { isa => 'Str', optional => 1 },
readme => { isa => 'Str', optional => 1 },
with => { isa => 'Cmd::TypeWith', optional => 1 },
library => { isa => 'Str', optional => 1 },
);
push @{$cmd->{args}}, delete $cmd->{argv}{dir};
$cmd->map(qw/list/ => sub {
my($argv, $args) = @_;
warn qq("called list);
});
$cmd->map(qw/loc with/ => sub {
my($argv, $args) = @_;
warn $argv->{loc};
warn $argv->{with};
});
$cmd->map(qw/loc library/ => sub {
my($argv, $args) = @_;
warn $argv->{loc};
warn $argv->{library};
});
$cmd->map(sub {
my(undef, $args) = @_;
warn $args->[0];
});
1;
@ishiduca
Copy link
Author

hayajo さんが Smart::Options::WithRule 書いてくれたので書きなおしてみた。

package Cmd;
use strict;
use warnings;

sub new {
    my $class = shift;
    my $argv  = shift;
    my $args  = delete $argv->{_};
    bless {
        opts => $argv,
        args => $args,
    }, $class;
}

sub map {
    my $self = shift;
    my $cb   = pop;
    my $opts = $self->{opts};
    return if scalar(keys %{$opts}) != scalar @_;
    return if scalar(grep{ exists $opts->{$_} }@_) != scalar @_;

    $cb->($opts, $self->{args});
    exit 0;
}
1;

package ObjDocMapper;
use strict;
use warnings;

sub new {
    bless { dir => pop }, shift
}
1;

package main;
use strict;
use warnings;
use Smart::Options::WithRule;
use Mouse::Util::TypeConstraints;

subtype 'Cmd::TypeWith'
  => as      'Str'
  => where   { /^[mpr]$/ }
  => message { qq("with" option must be "m" or "p" or "r") }
;

my $cmd = Cmd->new( Smart::Options::WithRule->new
  ->options(
    dir => { default => '.',
             rule    => { isa => 'Str' }
           },
    l   => { alias => 'loc',
             rule  => { isa => 'Str', optional => 1 }
           },
    lib => { alias => 'library',
             rule  => { isa => 'Str', optional => 1 }
           },
    with => { rule  => { isa => 'Cmd::TypeWith', optional => 1 },
  )
  ->demand('dir')
  ->describe(dir => 'a directory to start scan')
  ->parse
);

my $odm = ObjDocMapper->new( delete $cmd->{opts}{dir} );

$cmd->map(qw/loc with/ => sub {
    my($opts, $args) = @_;
    warn $opts->{loc};
    warn $opts->{with};
});
$cmd->map(qw/loc library/ => sub {
    my($opts, $args) = @_;
    warn $opts->{loc};
    warn $opts->{library};
});
$cmd->map(sub {
    my(undef, $args) = @_;
    my $mod = shift @{$args} or die qq("mod" name not found);
    warn $mod;
});

die qq(show help !);

1;

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment