From: Jonathan Rockway Date: Wed, 29 Apr 2009 14:16:15 +0000 (-0500) Subject: parse the args to mx-run sanely X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?a=commitdiff_plain;h=2503822b6c8fbb5a1fb1cf9257378f38b6e5cd53;p=gitmo%2FMooseX-Runnable.git parse the args to mx-run sanely --- diff --git a/Makefile.PL b/Makefile.PL index feca4ff..36c9625 100644 --- a/Makefile.PL +++ b/Makefile.PL @@ -8,9 +8,11 @@ requires 'MooseX::Getopt'; # not really requires 'MooseX::Types' => 0.11; requires 'MooseX::Types::Path::Class'; requires 'namespace::autoclean'; +requires 'List::MoreUtils'; build_requires 'Test::More'; build_requires 'ok'; +build_requires 'Test::TableDriven'; install_script 'bin/mx-run'; diff --git a/bin/mx-run b/bin/mx-run index 5bcd615..ada3c17 100644 --- a/bin/mx-run +++ b/bin/mx-run @@ -3,81 +3,64 @@ use strict; use warnings; +use MooseX::Runnable::Util::ArgParser; use MooseX::Runnable::Run; # incidentally, we don't actually use this... exit run(); sub run { - my ($includes, $plugins, $app, $argv) = parse_argv(); + my $args = MooseX::Runnable::Util::ArgParser->new( + argv => \@ARGV, + ); - unshift @INC, $_ for @$includes; - help() unless $app; + help() if $args->is_help; + # set @INC from -I... + unshift @INC, $_->stringify for $args->include_paths; + + # load -M... modules + do { eval "require $_"; die $@ if $@ } + for $args->modules; + + my $app = $args->class_name; local $0 = "mx-run ... $app"; return MooseX::Runnable::Invocation->new( class => $app, - plugins => $plugins || [], - )->run(@$argv); -} - -sub parse_argv { - # we need to parse "incrementally" so we can identify: - # - our args (-Ilib, and --help, -h, and -?) - # - plugins to load (+Plugin) - # - the class name - # - the class' args - # code that's better than this is welcome! - - my (@include, @plugins, $app); - - while( my $arg = shift @ARGV ){ - if ($arg =~ /^-I([^-]+)/){ # XXX: handle -I"quoted string" ? - push @include, $1; - } - elsif ($arg =~ /^-M([^-]+)/){ - my $module = $1; - eval "use $module"; - die $@ if $@; - } - elsif ($arg =~ /^\+\+?([A-Za-z:_]+)$/){ # second + is for +Foo::Bar - push @plugins, $1; - } - elsif ($arg =~ /^--([^-]+)$/){ - help(); - } - else { - if($arg =~ /^([A-Za-z:_]+)$/){ - $app = $arg; - last; - } - else { - help(); - } - } - } - - return \@include, \@plugins, $app, \@ARGV; + plugins => [ keys %{$args->plugins} ], # XXX: fixme + )->run($args->app_args); } sub help { print <<'END'; This is mx-run, a utility for running MooseX::Runnable classes. -usage: mx-run Class::Name + +usage: mx-run -- Class::Name + mx-run options: --help -? -h Print this message - -I Add to @INC before loading Class::Name + -I Add to @INC before loading modules -M use immediately +PluginName Load PluginName (see MooseX::Runnable::Invocation) -Note that as soon as +PluginName is seen, all -[IM] options are -ignored by mx-run, and are instead processed by PluginName. +Note that as soon as +PluginName is seen, all following -[IM] options +are ignored by mx-run, and are instead processed by PluginName. So +put them at the very beginning. + +In the simplest cases, where you use only -I or -M (no plugins), you +may omit the -- before the class name. To get help for Class::Name, run: mx-run Class::Name --help + +Syntax examples: + + mx-run -Ilib Class::Name # Local Class::Name + mx-run -Ilib -MCarp::Always +Debug -- Class::Name # Debuggin + END exit 1; diff --git a/lib/MooseX/Runnable.pm b/lib/MooseX/Runnable.pm index e54bc8d..e374a8b 100644 --- a/lib/MooseX/Runnable.pm +++ b/lib/MooseX/Runnable.pm @@ -60,12 +60,15 @@ run it, using C. The syntax is: - mx-run Class::Name + mx-run -- Class::Name for example: - mx-run -Ilib App::HelloWorld --args --go --here + mx-run -Ilib -- App::HelloWorld --args --go --here +or: + + mx-run -Ilib +Persistent --port 8080 -Persistent -- App::HelloWorld --args --go --here =head2 C If you don't want to invoke your app with C, you can write a diff --git a/lib/MooseX/Runnable/Util/ArgParser.pm b/lib/MooseX/Runnable/Util/ArgParser.pm new file mode 100644 index 0000000..613844e --- /dev/null +++ b/lib/MooseX/Runnable/Util/ArgParser.pm @@ -0,0 +1,239 @@ +package MooseX::Runnable::Util::ArgParser; +use Moose; +use MooseX::Types::Moose qw(HashRef ArrayRef Str Bool); +use MooseX::Types::Path::Class qw(Dir); +use List::MoreUtils qw(first_index); + +use namespace::autoclean -also => ['_look_for_dash_something', '_delete_first']; + +has 'argv' => ( + is => 'ro', + isa => ArrayRef, + required => 1, + auto_deref => 1, +); + +has 'class_name' => ( + is => 'ro', + isa => Str, + lazy_build => 1, +); + +has 'modules' => ( + is => 'ro', + isa => ArrayRef[Str], + lazy_build => 1, + auto_deref => 1, +); + +has 'include_paths' => ( + is => 'ro', + isa => ArrayRef[Dir], + lazy_build => 1, + auto_deref => 1, +); + +has 'plugins' => ( + is => 'ro', + isa => HashRef[ArrayRef[Str]], + lazy_build => 1, +); + +has 'app_args' => ( + is => 'ro', + isa => ArrayRef[Str], + lazy_build => 1, + auto_deref => 1, +); + +has 'is_help' => ( + is => 'ro', + isa => Bool, + lazy_build => 1, +); + + +sub _build_class_name { + my $self = shift; + my @args = $self->argv; + + my $next_is_it = 0; + my $need_dash_dash = 0; + + ARG: + for my $arg (@args) { + if($next_is_it){ + return $arg; + } + + if($arg eq '--'){ + $next_is_it = 1; + next ARG; + } + + next ARG if $arg =~ /^-[A-Za-z]/; + + if($arg =~ /^[+]/){ + $need_dash_dash = 1; + next ARG; + } + + return $arg unless $need_dash_dash; + } + + if($next_is_it){ + confess 'Parse error: expecting ClassName, got EOF'; + } + if($need_dash_dash){ + confess 'Parse error: expecting --, got EOF'; + } + + confess "Parse error: looking for ClassName, but can't find it"; +} + +sub _look_for_dash_something($@) { + my ($something, @args) = @_; + my @result; + + my $rx = qr/^-$something(.*)$/; + ARG: + for my $arg (@args) { + last ARG if $arg eq '--'; + last ARG unless $arg =~ /^-/; + if($arg =~ /$rx/){ + push @result, $1; + } + } + + return @result; +} + +sub _build_modules { + my $self = shift; + my @args = $self->argv; + return [ _look_for_dash_something 'M', @args ]; +} + +sub _build_include_paths { + my $self = shift; + my @args = $self->argv; + return [ map { Path::Class::dir($_) } _look_for_dash_something 'I', @args ]; +} + +sub _build_is_help { + my $self = shift; + my @args = $self->argv; + return + (_look_for_dash_something 'h', @args) || + (_look_for_dash_something '\\?', @args) || + (_look_for_dash_something '-help', @args) ;; +} + +sub _build_plugins { + my $self = shift; + my @args = $self->argv; + $self->class_name; # causes death when plugin syntax is wrong + + my %plugins; + my @accumulator; + my $in_plugin = undef; + + ARG: + for my $arg (@args) { + if(defined $in_plugin){ + if($arg eq '--'){ + $plugins{$in_plugin} = [@accumulator]; + @accumulator = (); + return \%plugins; + } + elsif($arg =~ /^[+](.+)$/){ + $plugins{$in_plugin} = [@accumulator]; + @accumulator = (); + $in_plugin = $1; + next ARG; + } + else { + push @accumulator, $arg; + } + } + else { # once we are $in_plugin, we can never be out again + if($arg eq '--'){ + return {}; + } + elsif($arg =~ /^[+](.+)$/){ + $in_plugin = $1; + next ARG; + } + } + } + + if($in_plugin){ + confess "Parse error: expecting arguments for plugin $in_plugin, but got EOF. ". + "Perhaps you forgot '--' ?"; + } + + return {}; +} + +sub _delete_first($\@) { + my ($to_delete, $list) = @_; + my $idx = first_index { $_ eq $to_delete } @$list; + splice @$list, $idx, 1; + return; +} + +# this is a dumb way to do it, but i forgot about it until just now, +# and don't want to rewrite the whole class ;) ;) +sub _build_app_args { + my $self = shift; + my @args = $self->argv; + + return [] if $self->is_help; # LIES!!11!, but who cares + + # functional programmers may wish to avert their eyes + _delete_first $_, @args for map { "-M$_" } $self->modules; + _delete_first $_, @args for map { "-I$_" } $self->include_paths; + + my %plugins = %{ $self->plugins }; + for my $p (keys %plugins){ + my $vl = scalar @{ $plugins{$p} }; + splice @args, first_index { $_ eq "+$p" } @args, $vl + 1; + } + + # sanity check + if($args[0] eq '--'){ + shift @args; + } + + if($args[0] eq $self->class_name){ + shift @args; + } + else { + confess 'Parse error: Some residual crud was found before the app name: '. + join ', ', @args; + } + + return [@args]; +} + +1; + +__END__ + +=head1 NAME + +MooseX::Runnable::Util::ArgParser - parse @ARGV for mx-run + +=head1 SYNOPSIS + + my $parser = MooseX::Runnable::Util::ArgParser->new( + argv => \@ARGV, + ); + + $parser->class_name; + $parser->modules; + $parser->include_paths; + $parser->plugins; + $parser->is_help; + $parser->app_args; + diff --git a/t/arg-parser.t b/t/arg-parser.t new file mode 100644 index 0000000..ea19237 --- /dev/null +++ b/t/arg-parser.t @@ -0,0 +1,123 @@ +use strict; +use warnings; + +use MooseX::Runnable::Util::ArgParser; + +use Test::TableDriven ( + class_name => { + 'Foo' => 'Foo', + '-Ilib Foo' => 'Foo' , + '-I/foo/bar/lib -Ilib -IFoo module with lots of args' => 'module' , + '-- Foo' => 'Foo', + '-Ilib -- Foo' => 'Foo', + '-Ilib -MFoo::Bar -- Foo::Baz' => 'Foo::Baz', + '-MFoo Bar' => 'Bar', + '+Plugin1 --args --go --here -- Foo' => 'Foo', + '+P --args --arehere +Q --more --args -- Foo' => 'Foo', + '-Ilib +P --args --arehere +Q --more --args -Ilib -- Foo' => 'Foo', + '+P --args -- Foo -- Bar', 'Foo', + }, + + modules => { + 'Foo' => [], + 'Foo -MFoo' => [], + '-MFoo' => ['Foo'], + '-MFoo Foo' => ['Foo'], + '-MFoo Foo' => ['Foo'], + '-MFoo -MFoo Foo' => ['Foo', 'Foo'], + '-MFoo -MBar -MBaz::Quux -Ilib OH::HAI' => ['Foo','Bar','Baz::Quux'], + '+End -MFoo -MBar -- OH::HAI' => [], + '-Ilib +End -MFoo -- OH::HAI' => [], + '-Ilib -MFoo OH::HAI' => ['Foo'], + '-Ilib -MFoo +End -MBar -- OH::HAI' => ['Foo'], + }, + + include_paths => { + 'Foo' => [], + 'Foo -Ilib' => [], + '-Ilib Foo' => ['lib'], + '-MFoo Foo' => [], + '-MFoo -MBar -MBaz::Quux -Ilib OH::HAI' => ['lib'], + '+End -MFoo -MBar -- OH::HAI' => [], + '-Ilib +End -MFoo -- OH::HAI' => ['lib'], + '-Ilib -MFoo OH::HAI' => ['lib'], + '-Ilib -MFoo +End -IBar -- OH::HAI' => ['lib'], + '-Ilib -MFoo -I../../../../lib +End -IBar -- OH::HAI' => + ['lib', '../../../../lib'], + + }, + + plugins => { + 'Foo' => {}, + '-Ilib Foo' => {}, + '-Ilib -MFoo -- Bar' => {}, + '+One --arg +Two --arg2 -- End' => { One => ['--arg'], Two => ['--arg2'] }, + '+Debug +PAR +Foo::Bar -- Baz' => { Debug => [], PAR => [], 'Foo::Bar' => [] }, + }, + + is_help => { + '--help' => 1, + '-h' => 1, + '-?' => 1, + '--?' => 0, + '--h' => 0, + '+Foo --help' => 0, + 'Foo' => 0, + '-Ilib -MFoo --help' => 1, + '-- Foo --help' => 0, + 'Foo --help' => 0, + 'Foo -?' => 0, + 'Foo -h' => 0, + }, + + app_args => { + 'Foo' => [], + '-Ilib Foo' => [], + '-Ilib -MFoo Bar' => [], + 'Foo Bar' => ['Bar'], + 'Foo Bar Baz' => ['Bar', 'Baz'], + '-- Foo Bar Baz' => ['Bar', 'Baz'], + '-Ilib Foo -Ilib' => ['-Ilib'], + '-MFoo Foo -MFoo' => ['-MFoo'], + '-MFoo -MFoo Foo -MFoo' => ['-MFoo'], + '-- Foo --help' => ['--help'], + }, +); + +sub class_name { + my ($argv) = @_; + my $p = MooseX::Runnable::Util::ArgParser->new( argv => [split / /, $argv] ); + return $p->class_name; +} + +sub modules { + my ($argv) = @_; + my $p = MooseX::Runnable::Util::ArgParser->new( argv => [split / /, $argv] ); + return $p->modules; +} + +sub include_paths { + my ($argv) = @_; + my $p = MooseX::Runnable::Util::ArgParser->new( argv => [split / /, $argv] ); + return [ map { $_->stringify } $p->include_paths ]; +} + +sub plugins { + my ($argv) = @_; + my $p = MooseX::Runnable::Util::ArgParser->new( argv => [split / /, $argv] ); + return $p->plugins; +} + +sub is_help { + my ($argv) = @_; + my $p = MooseX::Runnable::Util::ArgParser->new( argv => [split / /, $argv] ); + return $p->is_help ? 1 : 0; +} + +sub app_args { + my ($argv) = @_; + my $p = MooseX::Runnable::Util::ArgParser->new( argv => [split / /, $argv] ); + return $p->app_args; +} + +runtests;