#============================================================================= # # Module: Term::CLI::Command::Help # # Description: Class for Term::CLI 'help' command. # # Author: Steven Bakker (SBAKKER), # Created: 18/02/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. # #============================================================================= use 5.014_001; package Term::CLI::Command::Help 0.051007 { use Modern::Perl 1.20140107; use Pod::Text::Termcap 2.08; use List::Util 1.38 qw( first min ); use File::Which 1.09; use Types::Standard 1.000005 qw( ArrayRef Str ); use Getopt::Long 2.42 qw( GetOptionsFromArray ); use Term::CLI::L10N; my @PAGERS = ( [qw( less --no-lessopen --no-init --dumb --quit-at-eof --quit-if-one-screen )], ['more'], ['pg'], ); my @PAGER; if (my $pager = first { defined which($_->[0]) } @PAGERS) { @PAGER = @$pager; } use Moo 1.000001; use namespace::clean 0.25; extends 'Term::CLI::Command'; has 'pager' => ( is => 'rw', isa => ArrayRef[Str], default => sub { [@PAGER] }, ); has '+name' => ( default => sub { 'help' } ); has '+callback' => ( default => sub { sub { my $self = shift; $self->_execute_help(@_); } } ); has '+options' => ( default => sub { [ 'pod|p' ] }, ); has '+description' => ( default => sub { loc(qq{Show help for any given command sequence.\n} .qq{The C<--pod> option (or C<-p>) will cause raw POD\n} .qq{to be shown.}); } ); has '+summary' => ( default => sub { loc('show help') }, ); has '+_arguments' => ( default => sub { [ Term::CLI::Argument::String->new( name => 'cmd', min_occur => 0, max_occur => 0, ) ] }, ); sub _format_pod { my $self = shift; my $text = shift; my $output; my $parser = Pod::Text::Termcap->new( width => $self->term->term_width - 1 ); $parser->output_string(\$output); $parser->parse_string_document($text); return $output; } # ($pod, $text) = $self->_make_command_summary( %args ); sub _make_command_summary { my ($self, %args) = @_; my $cmd_path = $args{cmd_path}; my $commands = $args{commands}; my $pod_prefix = $args{pod_prefix}; my $text = ''; my $full_pod = ''; my $item_length = 0; for my $cmd_ref (@$commands) { for my $usage ($cmd_ref->usage_text(with_options => 'none')) { my $item_text = join(' ', (map { "B<$_>" } @$cmd_path), $usage ); $full_pod .= "=item $item_text\n\n"; my $l = length($item_text =~ s/[BCEIL]<([^>]*)>/$1/gr); $item_length = $l if $l > $item_length; } $full_pod .= $cmd_ref->summary; $full_pod =~ s/\n*$/\n\n/s; } my $max_over_width = int(($self->term->term_width - 4) / 2); my $over_width = min($item_length+4, $max_over_width); $full_pod = $pod_prefix."=over $over_width\n\n$full_pod"; $full_pod .= "=back\n\n"; $text = $self->_format_pod($full_pod); $text =~ s/\n\n+/\n/gs; $text =~ s/^\n+//; return ($full_pod, $text); } sub _get_help { my ($self, %args) = @_; my $text = ''; # Top-level help, i.e. "help" without arguments. # Produce a simple command summary. if (@{$args{arguments}} == 0) { my ($pod, $text) = $self->_make_command_summary( cmd_path => [], pod_prefix => "=head2 ".loc("Commands").":\n\n", commands => [$self->root_node->commands] ); return (%args, pod => $pod, text => $text); } my @cmd_path; my $cur_cmd_ref = $self->root_node; my $cmd_list = $args{arguments}; my @cmd_ref_path; while (@$cmd_list) { my $new_cmd_ref = $cur_cmd_ref->find_command($cmd_list->[0]); if (!$new_cmd_ref) { return (%args, status => -1, error => @cmd_path > 0 ? "@cmd_path: ".$cur_cmd_ref->error : $cur_cmd_ref->error ); } push @cmd_path, shift @{$cmd_list}; push @cmd_ref_path, $new_cmd_ref; $cur_cmd_ref = $new_cmd_ref; } my $last_cmd = pop @cmd_ref_path; my $usage_prefix = join(' ', map { $_->usage_text(with_options => 'none', with_subcommands => 0) } @cmd_ref_path ); $usage_prefix .= ' ' if length $usage_prefix; my $pod .= "=head2 ".loc("Usage").":\n\n"; for my $usage ($last_cmd->usage_text(with_options => 'both')) { $pod .= "$usage_prefix$usage\n\n"; } $pod =~ s/\n*$//s; $pod .= "\n\n"; if (my $description = $cur_cmd_ref->description) { $pod .= "=head2 ".loc("Description").":\n\n"; $pod .= $cur_cmd_ref->description; } elsif (my $summary = $cur_cmd_ref->summary) { $pod .= "=head2 Description:\n\n"; $pod .= $cur_cmd_ref->summary; } $pod =~ s/\n*$/\n\n/s; my $pod2txt = $self->_format_pod($pod); # Only list sub-commands if there are more than one. if (scalar($cur_cmd_ref->commands) > 1) { my ($cmd_pod, $cmd_text) = $self->_make_command_summary( cmd_path => \@cmd_path, pod_prefix => "=head2 ".loc("Sub-Commands").":\n\n", commands => [$cur_cmd_ref->commands], ); $pod .= $cmd_pod; $pod2txt .= $cmd_text; } # Play fast and loose with the POD formatter output. # Remove leading and trailing newlines, reduce line indent. $pod2txt =~ s/^\n+//s; $pod2txt =~ s/\n+$//s; #$pod2txt =~ s/^ //gm; $text .= "$pod2txt\n"; return (%args, pod => $pod, text => $text); } sub complete_line { my ($self, @words) = @_; my $partial = $words[$#words] // ''; # uncoverable branch false if ($self->has_options) { Getopt::Long::Configure(qw(bundling require_order pass_through)); my $opt_specs = $self->options; my %parsed_opts; my $has_terminator = first { $_ eq '--' } @words[0..$#words-1]; eval { GetOptionsFromArray(\@words, \%parsed_opts, @$opt_specs) }; if (!$has_terminator && @words <= 1 && $partial =~ /^-/) { # We have to complete a command-line option. return grep { rindex($_, $partial, 0) == 0 } $self->option_names; } } my $cur_cmd_ref = $self->root_node; while (@words) { my $new_cmd_ref = $cur_cmd_ref->find_command($words[0]); if (!$new_cmd_ref) { last; } shift @words; $cur_cmd_ref = $new_cmd_ref; } if (@words == 0) { return $cur_cmd_ref->name; } elsif ($cur_cmd_ref->has_commands && @words == 1) { return grep { rindex($_, $partial, 0) == 0 } $cur_cmd_ref->command_names; } return (); } # %args = HELP->_execute_help(%args); # # Callback for the builtin "help" command. # sub _execute_help { my ($self, %args) = @_; return %args if $args{status} < 0; %args = $self->_get_help(%args); return %args if $args{status} < 0; if ($args{options}->{pod}) { print "\n$args{pod}"; return %args; } my $pager_fh; my $pager_cmd = $self->pager; if (@$pager_cmd) { no warnings 'exec'; if (!open $pager_fh, "|-", @{$pager_cmd}) { $args{status} = -1; $args{error} = loc("cannot run '[_1]': [_2]", $$pager_cmd[0], $!); return %args; } local( $SIG{PIPE} ) = 'IGNORE'; # Temporarily avoid accidents. print $pager_fh $args{text}; $pager_fh->close; $args{status} = $?; $args{error} = $! if $args{status} != 0; } else { if (!open $pager_fh, '>&', \*STDOUT) { $args{status} = -1; $args{error} = "dup(STDOUT): $!"; return %args; } print $pager_fh $args{text}; if (!$pager_fh->close) { $args{status} = -1; $args{error} = $!; } } return %args; } } 1; __END__ =pod =head1 NAME Term::CLI::Command::Help - A generic 'help' command for Term::CLI =head1 VERSION version 0.051007 =head1 SYNOPSIS use Term::CLI; my $cli = Term::CLI->new( name => 'myapp', prompt => 'myapp> ', commands => [ Term::CLI::Command::Help->new(), Term::CLI::Command->new( name => 'copy', ... ), Term::CLI::Command->new( name => 'move', ... ), ], ); $cli->execute('help'); # -> command summary say "\n----\n"; $cli->execute('help copy'); # -> detailed help on 'copy'. (See L for a working example.) =head1 DESCRIPTION The C class is derived from L(3p) and implements a generic "help" command for L(3p) applications. The C command accepts arguments that it will try to match against the commands of its L(3p) parent. It supports completion, as well as a C<--pod> parameter to dump raw POD text. =head1 CONSTRUCTORS =over =item B X Create a new C object and return a reference to it. The object provides appropriate default values for all attributes, so there is no need to provide any. If you want, you can override the default attributes; in that case, see the L(3p) documentation. Attributes that are "safe" to override are: =over =item B =E I Override the default description for the C command. =item B =E I Override the name for the help command. Default is C. =item B =E I[I] Override the default pager for help display. See L. The value should be a command line split on words, e.g.: OBJ->pager( [ 'cat', '-n', '-e' ] ); If an empty list is provided, no external pager will be used, and output is printed to F directly. See also the L method. =item B =E Override the default summary for the C command. =item B =E Override the automatic usage string for the C command. =back =back =head1 METHODS =over =item B ( [ I[I]> ] ) X Get or set the pager command. If an empty list is provided, no external pager will be used, and output is printed to F directly. Example: $help_cmd->pager([]); # Print directly to STDOUT. $help_cmd->pager([ 'cat', '-n' ]); # Number output lines. =back =head1 OUTPUT FORMATTING Help text is assumed to be in L format, and will be formatted for the terminal using L(3p). =head1 OUTPUT PAGING The C command will try to pipe the formatted output through a suitable pager. At startup, the pager is selected from the following list, in order of preference: L, L, L, F. This can be overridden by supplying a value to the object's L attribute. =head1 EXAMPLE Using the following code: use Term::CLI; my $cli = Term::CLI->new( name => 'myapp', prompt => 'myapp> ', commands => [ Term::CLI::Command::Help->new(), Term::CLI::Command->new( name => 'copy', options => [ 'verbose!' ], summary => 'copy I to I', description => qq{Copy I to I.\n} .qq{Show progress if C<--verbose> is given.}, arguments => [ Term::CLI::Argument::Filename->new(name => 'src'), Term::CLI::Argument::Filename->new(name => 'dst'), ], ), Term::CLI::Command->new( name => 'move', options => [ 'verbose!' ], summary => 'move I to I', description => qq{Move I to I.\n} .qq{Move progress if C<--verbose> is given.}, arguments => [ Term::CLI::Argument::Filename->new(name => 'src'), Term::CLI::Argument::Filename->new(name => 'dst'), ], ) ], ); say "\n----\n"; $cli->execute('help'); # -> command summary say "\n----\n"; $cli->execute('help copy'); # -> detailed help on 'copy'. The output would look something like this: ---- Commands: help [cmd ...] Show help. copy src dst copy src to dst move src dst move src to dst ---- Usage: copy [--verbose] src dst Description: Copy src to dst. Show progress if "--verbose" is given. ---- =head1 SEE ALSO L(1), L(1), L(1), L(1), L(1), 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