#============================================================================= # # Module: Term::CLI # # Description: Class for CLI parsing # # Author: Steven Bakker (SBAKKER), # Created: 31/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 0.053004 { use 5.014; use strict; use warnings; use Text::ParseWords 3.29 qw( parse_line ); use Term::CLI::ReadLine; use FindBin 1.50; use Term::CLI::L10N; # Load all Term::CLI classes so the user doesn't have to. use Term::CLI::Argument::Bool; use Term::CLI::Argument::Enum; use Term::CLI::Argument::Filename; use Term::CLI::Argument::Number; use Term::CLI::Argument::Number::Float; use Term::CLI::Argument::Number::Int; use Term::CLI::Argument::String; use Term::CLI::Command; use Term::CLI::Command::Help; use Types::Standard 1.000005 qw( ArrayRef CodeRef InstanceOf Maybe RegexpRef Str Int ); use Moo 1.000001; use namespace::clean 0.25; extends 'Term::CLI::Base'; with('Term::CLI::Role::CommandSet'); # Provide a default for 'name'. has '+name' => ( default => sub { $FindBin::Script } ); has cleanup => ( is => 'rw', isa => Maybe[CodeRef], predicate => 1 ); has prompt => ( is => 'rw', isa => Str, default => sub { '~> ' } ); has split_function => ( is => 'rw', isa => CodeRef, default => sub { \&_default_split } ); has skip => ( is => 'rw', isa => RegexpRef, ); has history_file => ( is => 'rw', isa => Str ); has history_lines => ( is => 'rw', isa => Int, default => sub { 1000 }, trigger => 1, ); has word_delimiters => ( is => 'rw', isa => Str, default => sub {" \n\t"} ); has quote_characters => ( is => 'rw', isa => Str, default => sub {q("')} ); sub BUILD { my ($self, $args) = @_; my $term = Term::CLI::ReadLine->new($self->name)->term; if (my $sig_list = $args->{ignore_keyboard_signals}) { $term->ignore_keyboard_signals(@$sig_list); } $term->Attribs->{completion_function} = sub { $self->complete_line(@_) }; $term->Attribs->{char_is_quoted_p} = sub { $self->_is_escaped(@_) }; $self->_set_completion_attribs; if (! exists $args->{callback} ) { $self->callback(\&_default_callback); } if (!exists $args->{history_file}) { my $hist_file = $self->name; $hist_file =~ s{^/}{}g; $hist_file =~ s{/$}{}g; $hist_file =~ s{/+}{-}g; $self->history_file("$::ENV{HOME}/.${hist_file}_history"); } # Ensure that the history_lines trigger is called. # If history_lines is given as a parameter, the trigger # *is* called, but it happens *before* the `$term` is # initialised; and if no history_lines is given, the # trigger is not called for the default. $self->history_lines($self->history_lines); } sub DEMOLISH { my ($self) = @_; if ($self->has_cleanup) { $self->cleanup->($self); } } sub _trigger_history_lines { my ($self, $arg) = @_; # Terminal may not be initialiased yet... return if !$self->term; $self->term->StifleHistory($arg); } # %args = $self->_default_callback(%args); # # Default top-level callback if none is given. # Simply check the status and print an error # message if status < 0. sub _default_callback { my ($self, %args) = @_; if ($args{status} < 0) { say STDERR loc("ERROR"), ": ", $args{error}; } return %args; } # ($error, @words) = $self->_default_split($text); # # Default function to split a string into words. # Similar to "shellwords()", except that we use # "parse_line" to support custom delimiters. # # Unfortunately, there's no way to specify custom # quote characters. # sub _default_split { my ($self, $text) = @_; if ($text =~ /\S/) { my $delim = $self->word_delimiters; $text =~ s/^[$delim]+//; my @words = parse_line(qr{[$delim]+}, 0, $text); pop @words if @words and not defined $words[-1]; my $error = @words ? '' : loc('unbalanced quotes in input'); return ($error, @words); } else { return (''); } } # BOOL = CLI->_is_escaped($line, $index); # # The character at $index in $line is a possible word break # character. Check if it is perhaps escaped. # sub _is_escaped { my ($self, $line, $index) = @_; return 0 if !$index or $index < 0; return 0 if substr($line, $index-1, 1) ne '\\'; return !$self->_is_escaped($line, $index-1); } # CLI->_set_completion_attribs(); # # Set some attributes in the Term::ReadLine object related to # custom completion. # sub _set_completion_attribs { my $self = shift; my $term = $self->term; # Default: '" $term->Attribs->{completer_quote_characters} = $self->quote_characters; # Default: \n\t\\"'`@$><=;|&{( and $term->Attribs->{completer_word_break_characters} = $self->word_delimiters; # Default: $term->Attribs->{completion_append_character} = substr($self->word_delimiters, 0, 1); } # CLI->_split_line( $text ); # # Attempt to split $text into words. Use a custom split function if # necessary. # sub _split_line { my ($self, $text) = @_; return $self->split_function->($self, $text); } # Dumb wrapper around "Attrib" that allows mocking the # `completion_quote_character` state. sub _rl_completion_quote_character { my ($self) = @_; my $c = $self->term->Attribs->{completion_quote_character} // ''; return $c =~ s/\000//gr; } # See POD X sub complete_line { my ($self, $text, $line, $start) = @_; $self->_set_completion_attribs; my $quote_char = $self->_rl_completion_quote_character; my @words; if ($start > 0) { if (length $quote_char) { # ReadLine thinks the $text to be completed is quoted. # The quote character will precede the $start of $text. # Make sure we do not include it in the text to break # into words... (my $err, @words) = $self->_split_line(substr($line, 0, $start-1)); } else { (my $err, @words) = $self->_split_line(substr($line, 0, $start)); } } push @words, $text; my @list; if (@words == 1) { @list = grep { rindex($_, $words[0], 0) == 0 } $self->command_names; } elsif (my $cmd = $self->find_command($words[0])) { @list = $cmd->complete_line(@words[1..$#words]); } # Escape spaces in reply if necessary. if (length $quote_char) { return @list; } else { my $delim = $self->word_delimiters; return map { s/([$delim])/\\$1/gr } @list; } } sub readline { my ($self, %args) = @_; my $prompt = $args{prompt} // $self->prompt; my $skip = exists $args{skip} ? $args{skip} : $self->skip; $self->_set_completion_attribs; my $input; while (defined ($input = $self->term->readline($prompt))) { next if defined $skip && $input =~ $skip; last; } return $input; } sub read_history { my $self = shift; my $hist_file = @_ ? shift @_ : $self->history_file; $self->term->ReadHistory($hist_file) or return $self->set_error("$hist_file: $!"); $self->history_file($hist_file); $self->set_error(''); return 1; } sub write_history { my $self = shift; my $hist_file = @_ ? shift @_ : $self->history_file; $self->term->WriteHistory($hist_file) or return $self->set_error("$hist_file: $!"); $self->history_file($hist_file); $self->set_error(''); return 1; } sub execute { my ($self, $cmd) = @_; my ($error, @cmd) = $self->_split_line($cmd); my %args = ( status => 0, error => '', command_line => $cmd, command_path => [$self], unparsed => \@cmd, options => {}, arguments => [], ); return $self->try_callback(%args, status => -1, error => $error) if length $error; if (@cmd == 0) { $args{error} = loc("missing command"); $args{status} = -1; } elsif (my $cmd_ref = $self->find_command($cmd[0])) { %args = $cmd_ref->execute(%args, unparsed => [@cmd[1..$#cmd]] ); } else { $args{error} = $self->error; $args{status} = -1; } return $self->try_callback(%args); } } 1; __END__ =pod =head1 NAME Term::CLI - CLI interpreter based on Term::ReadLine =head1 VERSION version 0.053004 =head1 SYNOPSIS use Term::CLI; use Term::CLI::Command; use Term::CLI::Argument::Filename; use Data::Dumper; my $cli = Term::CLI->new( name => 'myapp', prompt => 'myapp> ', cleanup => sub { my ($cli) = @_; $cli->write_history; or warn "cannot write history: ".$cli->error."\n"; }, callback => sub { my ($self, %args) = @_; print Data::Dumper->Dump([\%args], ['args']); return %args; }, commands => [ 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); } ) ], ); $cli->read_history; # Read history from ~/.myapp_history $cli->write_history; # Write history to ~/.myapp_history $cli->word_delimiters(';,'); # $cli will now recognise things like: 'copy;--verbose;a,b' $cli->word_delimiters(" \t\n"); # $cli will now recognise things like: 'copy --verbose a b' while ( my $input = $cli->readline(skip => qr/^\s*(?:#.*)?$/) ) { $cli->execute($input); } =head1 DESCRIPTION Implement an easy-to-use command line interpreter based on L(3p). Although primarily aimed at use with the L(3p) implementation, it also supports L(3p). First-time users may want to read L(3p) and L(3p) first, and peruse the example scripts in the source distribution's F and F directories. =head1 CLASS STRUCTURE =head2 Inherits from: L(3p). =head2 Consumes: L(3p). =head1 CONSTRUCTORS =over =item B ( B => I ... ) X Create a new C object and return a reference to it. Valid attributes: =over =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 a subroutine that should be called when the object is destroyed (i.e. in L terminology, when C is called). =item B =E I Reference to an array containing L object instances that describe the commands that C recognises, or C. =item B =E I Specify a list of signals for which the keyboard generation should be turned off during a C operation. The list of signals should be a combination of C, C, or C. See also L in L(3p). If this is not specified, C keyboard generation is turned off by default. =item B =E I The application name. This is used for e.g. the history file and default command prompt. If not given, defaults to C<$FindBin::Script> (see L(3p)). =item B =E I Prompt to display when L is called. Defaults to the application name with C> and a space appended. =item B =E I Set the object's L attribute, telling the L method to ignore input lines that match the given I. A common call value is C to skip empty lines, lines with only whitespace, and comments. =item B =E I Specify the file to read/write input history to/from. The default is I + C<_history> in the user's I directory. =item B =E I Maximum number of lines to keep in the input history. Default is 1000. =back =back =head1 INHERITED METHODS This class inherits all the attributes and accessors of L(3p) and L(3p), most notably: =head2 Accessors =over =item B X See L. =item B ( [ I ] ) X See L. =item B X See L. =item B ( [ I ] ) X See L. I with C object instances. =back =head2 Others =over =item B X Predicate function that returns whether or not the C attribute has been set. =item B ( [ I ] ) Gets or sets a reference to a subroutine that should be called when the object is destroyed (i.e. in L terminology, when C is called). The code is called with one parameter: the object to be destroyed. One typical use of C is to ensure that the history gets saved upon exit: my $cli = Term::CLI->new( ... cleanup => sub { my ($cli) = @_; $cli->write_history or warn "cannot write history: ".$cli->error."\n"; } ); =item B ( I ) X See L. =item B ( I ) X See L. =back =head1 METHODS =head2 Accessors =over =item B X The application name. See L. =item B ( [ I ] ) X Get or set the command line prompt to display to the user. =item B X Return a reference to the underlying L object. See L. =item B ( [ I ] ) X Get or set the characters that should considered quote characters for the completion and parsing/execution routines. Default is C<'">, that is a single quote or a double quote. It's possible to change this, but this will interfere with the default splitting function, so if you do want custom quote characters, you should also override the L. =item B ( [ I ] ) Get or set the function that is used to split a (partial) command line into words. The default function uses L. Note that this implies that it can take into account custom delimiters, but I. The I is called as: ( ERROR, [ WORD, ... ] ) = CodeRef->( CLI_OBJ, TEXT ) The function should return a list of at least one element, an I string. Subsequent elements are the words resulting from the split. I string should be empty (not C!) if splitting was successful, otherwise it should contain a relevant error message. =item B ( [ I ] ) Get or set the characters that are considered word delimiters in the completion and parsing/execution routines. Default is C< \t\n>, that is I, I, and I. The first character in the string is also the character that is appended to a completed word at the command line prompt. =back =head2 History Control =over =item B ( [ I ] ) Get or set the maximum number of lines to keep in the history. Default is 1000. =item B ( [ I ] ) Set the default file to read from/write to. =item B ( [ I ] ) Try to read input history from the L. Returns 1 on success. On failure, it will set the L field and return C. If I is given, it will try to read from that file instead. If that is successful, the L attribute will be set to I. =item B ( [ I ] ) Try to write the input history to the L. Returns 1 on success. On failure, it will set the L field and return C. If I is given, it will try to write to that file instead. If that is successful, the L attribute will be set to I. =back =head2 Others =over =item B ( I, I, I ) X Called when the user hits the I key for completion. I is the text to complete, I is the input line so far, I is the position in the line where I starts. The function will split the line in words and delegate the completion to the first L sub-command, see L. =item B ( [ I =E I, ... ] ) X Read a line from the input connected to L, using the L interface. By default, it returns the line read from the input, or an empty value if end of file has been reached (e.g. the user hitting I). The following I are recognised: =over =item B =E I Override the object's L attribute. Skip lines that match the I parameter. A common call is: $text = CLI->readline( skip => qr{^\s+(?:#.*)$} ); This will skip empty lines, lines containing whitespace, and comments. =item B =E I Override the prompt given by the L method. =back Examples: # Just read the next input line. $line = $cli->readline; exit if !defined $line; # Skip empty lines and comments. $line = $cli->readline( skip => qr{^\s*(?:#.*)?$} ); exit if !defined $line; =item B ( I ) X Parse and execute the command line consisting of I (see the return value of L above). The command line is split into words using the L. If that succeeds, then the resulting list of words is parsed and executed, otherwise a parse error is generated (i.e. the object's L function is called with a C of C<-1> and a suitable C field). For specifying a custom word splitting method, see L. Example: while (my $line = $cli->readline(skip => qr/^\s*(?:#.*)?$/)) { $cli->execute($line); } The command line is parsed depth-first, and for every L(3p) encountered, that object's L function is executed (see L). The C function returns the results of the last called callback function. =over =item * Suppose that the C command has a C sub-command that takes an optional C<--verbose> option and a single file argument. =item * Suppose the input is: file show --verbose foo.txt =item * Then the parse tree looks like this: (cli-root) | +--> Command 'file' | +--> Command 'show' | +--> Option '--verbose' | +--> Argument 'foo.txt' =item * Then the callbacks will be called in the following order: =over =item 1. Callback for 'show' =item 2. Callback for 'file' =item 3. Callback for C object. =back The return value from each L (a hash in list form) is fed into the next callback function in the chain. This allows for adding custom data to the return hash that will be fed back up the parse tree (and eventually to the caller). =back =back =head1 SIGNAL HANDLING The C object (through L) will make sure that signals are handled "correctly". This especially means that if a signal is not ignored, the terminal is left in a "sane" state before any signal handler is called or the program exits. See also L. =head1 SEE ALSO L(3p), L(3p), L(3p), L(3p), L(3p), L(3p), L(3p), L(3p), L(3p), L(3p), L(3p), L(3p), L(3p), L(3p), L(3p). Inspiration for the custom completion came from: L. This is an excellent tutorial into the completion mechanics of the C library, and, by extension, 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