#============================================================================= # # Module: Term::CLI::Command # # Description: Class for (sub-)commands in Term::CLI # # Author: Steven Bakker (SBAKKER), # Created: 30/01/18 # # Copyright (c) 2018 Steven Bakker # # This module is free software; you can redistribute it and/or modify # it under the same terms as Perl itself. See "perldoc perlartistic." # # This software is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. # #============================================================================= package Term::CLI::Command 0.053005 { use 5.014; use strict; use warnings; use List::Util 1.38 qw( first min ); use Getopt::Long 2.42 qw( GetOptionsFromArray ); use Types::Standard 1.000005 qw( ArrayRef CodeRef InstanceOf Maybe Str ); use Term::CLI::L10N; use Moo 1.000001; use namespace::clean 0.25; extends 'Term::CLI::Element'; has options => ( is => 'rw', isa => Maybe[ArrayRef[Str]], predicate => 1 ); with ('Term::CLI::Role::CommandSet'); with ('Term::CLI::Role::ArgumentSet'); with ('Term::CLI::Role::HelpText'); sub option_names { my $self = shift; my $opt_specs = $self->options or return (); my @names; for my $spec (@$opt_specs) { for my $optname (split(qr/\|/, $spec =~ s/^([^!+=:]+).*/$1/r)) { push @names, length($optname) == 1 ? "-$optname" : "--$optname"; } } return @names; } sub complete_line { my ($self, @words) = @_; my $partial = $words[$#words] // ''; if ($self->has_options) { Getopt::Long::Configure(qw(bundling require_order pass_through)); my $opt_specs = $self->options; my %parsed_opts; my $has_terminator; if ($Getopt::Long::VERSION < 2.51) { # Getopt::Long before 2.51 removes '--' from word list; # Try to work around the bug. Can still be fooled by # "--foo --" if "--foo" takes an argument. :-/ $has_terminator = first { $_ eq '--' } @words[0..$#words-1]; eval { GetOptionsFromArray(\@words, \%parsed_opts, @$opt_specs) }; } else { eval { GetOptionsFromArray(\@words, \%parsed_opts, @$opt_specs) }; if (@words > 1 && $words[0] eq '--') { $has_terminator = shift @words; } } if (!$has_terminator && @words <= 1 && $partial =~ /^-/) { # We have to complete a command-line option. return grep { rindex($_, $partial, 0) == 0 } $self->option_names; } } # If the command has arguments, try to skip over them. if ($self->has_arguments) { my @args = $self->arguments; my $n = 0; while (@words > 1) { last if @args == 0; shift @words; $n++; if ($args[0]->max_occur > 0 and $n >= $args[0]->max_occur) { shift @args; $n = 0; } } if (@args) { return $args[0]->complete($words[0]); } } if ($self->has_commands) { if (@words <= 1) { return grep { rindex($_, $partial, 0) == 0 } $self->command_names; } elsif (my $cmd = $self->find_command($words[0])) { return $cmd->complete_line(@words[1..$#words]); } } return (); } sub execute { my ($self, %args) = @_; $args{status} = 0; $args{error} = ''; # Dereference and copy arguments/unparsed/options to prevent # unwanted side-effects. $args{arguments} = [@{$args{arguments}}]; $args{unparsed} = [@{$args{unparsed}}]; $args{options} = {%{$args{options}}}; $args{command_path} = [@{$args{command_path}}]; push @{$args{command_path}}, $self; if ($self->has_options) { my $opt_specs = $self->options; Getopt::Long::Configure(qw(bundling require_order no_pass_through)); my $error = ''; my $ok = do { local( $SIG{__WARN__} ) = sub { chomp($error = join('', @_)) }; GetOptionsFromArray($args{unparsed}, $args{options}, @$opt_specs); }; if (!$ok) { $args{status} = -1; $args{error} = $error; } } if ($args{status} >= 0) { if ($self->has_arguments or !$self->has_commands) { %args = $self->_check_arguments(%args); } } if ($args{status} >= 0 and $self->has_commands) { %args = $self->_execute_command(%args); } return $self->try_callback( %args ); } sub _too_few_args_error { my ($self, $arg_spec) = @_; if ($arg_spec->max_occur == $arg_spec->min_occur) { if ($arg_spec->min_occur == 1) { return loc("missing '[_1]' argument", $arg_spec->name); } else { return loc("need [_1] '[_2]' [numerate,_1,argument]", $arg_spec->min_occur, $arg_spec->name, ); } } elsif ($arg_spec->max_occur - $arg_spec->min_occur == 1) { return loc("need [_1] or [_2] '[_3]' arguments", $arg_spec->min_occur, $arg_spec->max_occur, $arg_spec->name, ); } elsif ($arg_spec->max_occur > 1) { return loc("need between [_1] and [_2] '[_3]' arguments", $arg_spec->min_occur, $arg_spec->max_occur, $arg_spec->name, ); } else { return loc("need at least [_1] '[_2]' [numerate,_1,argument]", $arg_spec->min_occur, $arg_spec->name, ); } } sub _check_arguments { my ($self, %args) = @_; my $unparsed = $args{unparsed}; my @arg_spec = $self->arguments; if (@arg_spec == 0 and @$unparsed > 0) { return (%args, status => -1, error => loc('no arguments allowed'), ); } my $argno = 0; my @parsed_args; for my $arg_spec (@arg_spec) { if (@$unparsed < $arg_spec->min_occur) { return (%args, status => -1, error => $self->_too_few_args_error($arg_spec), ); } my $args_to_check = $arg_spec->max_occur > 0 ? min($arg_spec->max_occur, scalar @$unparsed) : scalar @$unparsed; for my $i (1..$args_to_check) { my $arg = $unparsed->[0]; $argno++; my $arg_value = $arg_spec->validate($arg); if (!defined $arg_value) { return (%args, status => -1, error => "arg#$argno, '$arg': " . $arg_spec->error . " ".loc("for")." '" . $arg_spec->name . "'" ); } push @{$args{arguments}}, $arg_value; shift @$unparsed; } } # At this point, we have processed all our arg_spec. The only way there # are any elements left in @arguments is for the last arg_spec to have # a max_occur that is exceeded. If the command has no sub-commands that # is surely an error. If it does have sub-commands, we'll leave it to # be parsed further. if (@$unparsed > 0 and !$self->has_commands) { my $last_spec = $arg_spec[$#arg_spec]; return (%args, status => -1, error => loc("too many '[_1]' arguments (max. [_2])", $last_spec->name, $last_spec->max_occur, ), ); } return %args; } sub _execute_command { my ($self, %args) = @_; my $unparsed = $args{unparsed}; if (@$unparsed == 0) { if (scalar $self->commands == 1) { my ($cmd) = $self->commands; return (%args, status => -1, error => loc("incomplete command: missing '[_1]'", $cmd->name) ); } return (%args, status => -1, error => loc("missing sub-command")); } my $cmd_name = $unparsed->[0]; my $cmd = $self->find_command($cmd_name); if (!$cmd) { if (scalar $self->commands == 1) { ($cmd) = $self->commands; return (%args, status => -1, error => loc( "expected '[_1]' instead of '[_2]'", $cmd->name, $cmd_name ), ); } return (%args, status => -1, error => loc("unknown sub-command '[_1]'", $cmd_name) ); } shift @$unparsed; return $cmd->execute(%args); } } 1; __END__ =pod =head1 NAME Term::CLI::Command - Class for (sub-)commands in Term::CLI =head1 VERSION version 0.053005 =head1 SYNOPSIS use Term::CLI::Command; use Term::CLI::Argument::Filename; use Data::Dumper; my $copy_cmd = Term::CLI::Command->new( name => 'copy', options => [ 'verbose!' ], arguments => [ Term::CLI::Argument::Filename->new(name => 'src'), Term::CLI::Argument::Filename->new(name => 'dst'), ], callback => sub { my ($self, %args) = @_; print Data::Dumper->Dump([\%args], ['args']); return (%args, status => 0); } ); =head1 DESCRIPTION Class for command elements in L(3p). =head1 CLASS STRUCTURE =head2 Inherits from: L(3p). =head2 Consumes: L(3p), L(3p), L(3p). =head1 CONSTRUCTORS =over =item B ( B =E I ... ) X Create a new C object and return a reference to it. The B attribute is required. Other attributes are: =over =item B =E I Reference to an array containing L(3p) object instances that describe the parameters that the command takes, or C. See also L. =item B =E I Reference to a subroutine that should be called when the command is executed, or C. =item B =E I Reference to an array containing C object instances that describe the sub-commands that the command takes, or C. See also L. =item B =E I Reference to an array containing command options in L(3p) style, or C. =item B =E I Extended description of the command. See also L. =item B =E I Short description of the command. See also L. =item B =E I Static usage summary of the command. See also L. (B You will rarely have to specify this, as it can be determined automatically.) =back =back =head1 INHERITED METHODS This class inherits all the attributes and accessors of L(3p), L(3p), L(3p), and L(3p), most notably: =head2 Accessors =over =item B X See L. =item B X See L. =item B X See L. =item B X See L. Returns a list of C object instances. =item B X See L. Returns a list of C object instances. =item B ( [ I ] ) X See L. =item B ( [ I ] ) X See L. =item B ( [ I ] ) X See L. =item B ( [ I ] ) X See L. =back =head2 Others =over =item B X Return the list of argument names, in the original order. =item B X Return the list of (sub-)command names, sorted alphabetically. =item B ( I ) X Check whether I is a sub-command of this command. If so, return the appropriate C reference; otherwise, return C. =back =head1 METHODS =head2 Accessors =over =item B X Predicate functions that return whether or not the associated attribute has been set. =item B ( [ I ] ) X I with command-line options in L(3p) format. =back =head2 Others =over =item B ( I, I, ... ) X I is a reference to the top-level L instance. The I arguments make up the parameters to this command. Given those, this method attempts to generate possible completions for the last I in the list. The method can complete options, sub-commands, and arguments. Completions of commands and arguments is delegated to the appropriate L and L instances, resp. =item B X Return a list of all command line options for this command. Long options are prefixed with C<-->, and one-letter options are prefixed with C<->. Example: $cmd->options( [ 'verbose|v+', 'debug|d', 'help|h|?' ] ); say join(' ', $cmd->option_names); # output: --debug --help --verbose -? -d -h -v =item B ( I ) This method is called by L. It should not be called directly. It accepts the same list of parameters as the L function (see L), and returns the same structure. The C I should contain the words on the command line that have not been parsed yet. Depending on whether the object has sub-commands or arguments, the rest of the line is parsed (possibly handing off to another sub-command), and the results are passed to the L function. =back =head1 SEE ALSO L(3p), L(3p), L(3p), L(3p), L(3p), L(3p), L(3p). =head1 AUTHOR Steven Bakker Esbakker@cpan.orgE, 2018. =head1 COPYRIGHT AND LICENSE Copyright (c) 2018 Steven Bakker This module is free software; you can redistribute it and/or modify it under the same terms as Perl itself. See "perldoc perlartistic." This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. =begin __PODCOVERAGE =head1 THIS SECTION SHOULD BE HIDDEN This section is meant for methods that should not be considered for coverage. This typically includes things like BUILD and DEMOLISH from Moo/Moose. It is possible to skip these when using the Pod::Coverage class (using C), but this is not an option when running C from the command line. The simplest trick is to add a hidden section with an item list containing these methods. =over =item BUILD =item DEMOLISH =back =end __PODCOVERAGE =cut