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';
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;
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
--- /dev/null
+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;
+
--- /dev/null
+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;