parse the args to mx-run sanely
Jonathan Rockway [Wed, 29 Apr 2009 14:16:15 +0000 (09:16 -0500)]
Makefile.PL
bin/mx-run
lib/MooseX/Runnable.pm
lib/MooseX/Runnable/Util/ArgParser.pm [new file with mode: 0644]
t/arg-parser.t [new file with mode: 0644]

index feca4ff..36c9625 100644 (file)
@@ -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';
 
index 5bcd615..ada3c17 100644 (file)
@@ -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 <mx-run options> Class::Name <options for Class::Name>
+
+usage: mx-run <mx-run options> -- Class::Name <options for Class::Name>
+
 mx-run options:
 
     --help -? -h     Print this message
-    -I<path>         Add <path> to @INC before loading Class::Name
+    -I<path>         Add <path> to @INC before loading modules
     -M<module>       use <module> 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;
index e54bc8d..e374a8b 100644 (file)
@@ -60,12 +60,15 @@ run it, using C<MooseX::Runnable::Run>.
 
 The syntax is:
 
-  mx-run <args for mx-run> Class::Name <args for Class::Name>
+  mx-run <args for mx-run> -- Class::Name <args for 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<MooseX::Runnable::Run>
 
 If you don't want to invoke your app with C<mx-run>, you can write a
diff --git a/lib/MooseX/Runnable/Util/ArgParser.pm b/lib/MooseX/Runnable/Util/ArgParser.pm
new file mode 100644 (file)
index 0000000..613844e
--- /dev/null
@@ -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 (file)
index 0000000..ea19237
--- /dev/null
@@ -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;