instead of 5 slightly different calls to RESTHandler::usage_str this introduces a wrapper function that handles all required cases and is capable of resolving sub-commands. Adds a subroutine to print the short help for a command in case no subcommand was given. Modifies handle_cmd to allow for parsing of subcommands. --- src/PVE/CLIHandler.pm | 219 ++++++++++++++++++++++++++++++++++---------------- 1 file changed, 151 insertions(+), 68 deletions(-)
diff --git a/src/PVE/CLIHandler.pm b/src/PVE/CLIHandler.pm index e61fa6a..7c98287 100644 --- a/src/PVE/CLIHandler.pm +++ b/src/PVE/CLIHandler.pm @@ -2,7 +2,6 @@ package PVE::CLIHandler; use strict; use warnings; -use Data::Dumper; use PVE::SafeSyslog; use PVE::Exception qw(raise raise_param_exc); @@ -48,6 +47,87 @@ my $complete_command_names = sub { return $res; }; +my $generate_usage_str; +$generate_usage_str = sub { + my ($args) = @_; + die "not initialized" if !($cmddef && $exename && $cli_handler_class); + die 'format required' if !$args->{format}; + + # Set the defaults + $args->{sortfunc} //= sub { + my ($hash) = @_; + return sort keys %$hash; + }; + $args->{base} //= $cmddef; + $args->{prefix} //= $exename; + $args->{pwcallback} //= $cli_handler_class->can('read_password'); + $args->{stringfilemap} //= $cli_handler_class->can('string_param_file_mapping'); + $args->{sect_sep} //= ""; + $args->{indent} //= ""; + + my $str = ""; + if ($args->{cmd}) { + raise_param_exc({ cmd => "no such command '$args->{cmd}'"}) if !$args->{base}->{$args->{cmd}}; + if (ref($args->{base}->{$args->{cmd}}) eq 'ARRAY') { + # $args->{cmd} is an array, so it's an actual command + my ($class, $name, $arg_param, $fixed_param) = @{$args->{base}->{$args->{cmd}}}; + $str .= $args->{indent}; + $str .= $class->usage_str($name, "$args->{prefix} $args->{cmd}", $arg_param, + $fixed_param, $args->{format}, $args->{pwcallback}, + $args->{stringfilemap}); + } else { + # $args->{cmd} is not an array, so we recurse + $str .= $generate_usage_str->({ + format => $args->{format}, + sortfunc => $args->{sortfunc}, + base => $args->{base}->{$args->{cmd}}, + prefix => "$args->{prefix} $args->{cmd}", + pwcallback => $args->{pwcallback}, + stringfilemap => $args->{stringfilemap}, + sect_sep => $args->{sect_sep}, + indent => $args->{indent}, + }); + } + } else { + # No single $args->{cmd} given, so we explore all commands + if (ref($args->{base}) eq 'HASH') { + my $oldclass = undef; + foreach my $cmd ($args->{sortfunc}->($args->{base})) { + if (ref($args->{base}->{$cmd}) eq 'ARRAY') { + # $cmd is an array, so it's an actual command + my ($class, $name, $arg_param, $fixed_param) = @{$args->{base}->{$cmd}}; + $str .= $args->{sect_sep} if $oldclass && $oldclass ne $class; + $str .= $args->{indent}; + $str .= $class->usage_str($name, "$args->{prefix} $cmd", $arg_param, + $fixed_param, $args->{format}, $args->{pwcallback}, + $args->{stringfilemap}); + $oldclass = $class; + } else { + # $cmd is not an array, so we recurse + $str .= $generate_usage_str->({ + format => $args->{format}, + sortfunc => $args->{sortfunc}, + base => $args->{base}->{$cmd}, + prefix => "$args->{prefix} $cmd", + pwcallback => $args->{pwcallback}, + stringfilemap => $args->{stringfilemap}, + sect_sep => $args->{sect_sep}, + indent => $args->{indent}, + }); + $str .= $args->{sect_sep}; + } + } + } else { + # Handle simple commands + my ($class, $name, $arg_param, $fixed_param) = @$args->{base}; + $str .= $args->{indent}; + $str .= $class->usage_str($name, $name, $arg_param, $fixed_param, + $args->{format}, $args->{pwcallback}, $args->{stringfilemap}); + } + } + return $str; +}; + __PACKAGE__->register_method ({ name => 'help', path => 'help', @@ -92,16 +172,13 @@ __PACKAGE__->register_method ({ $cmd = &$expand_command_name($cmddef, $cmd); - my ($class, $name, $arg_param, $uri_param) = @{$cmddef->{$cmd} || []}; - - raise_param_exc({ cmd => "no such command '$cmd'"}) if !$class; + my $str = &$generate_usage_str({ + format => $verbose ? 'full' : 'short', + cmd => $cmd, + indent => $verbose ? '' : ' ' x 7, + }); + $str =~ s/^\s+//; - my $pwcallback = $cli_handler_class->can('read_password'); - my $stringfilemap = $cli_handler_class->can('string_param_file_mapping'); - - my $str = $class->usage_str($name, "$exename $cmd", $arg_param, $uri_param, - $verbose ? 'full' : 'short', $pwcallback, - $stringfilemap); if ($verbose) { print "$str\n"; } else { @@ -113,17 +190,10 @@ __PACKAGE__->register_method ({ }}); sub print_simple_asciidoc_synopsis { - my ($class, $name, $arg_param, $uri_param) = @_; - die "not initialized" if !$cli_handler_class; - my $pwcallback = $cli_handler_class->can('read_password'); - my $stringfilemap = $cli_handler_class->can('string_param_file_mapping'); - - my $synopsis = "*${name}* `help`\n\n"; - - $synopsis .= $class->usage_str($name, $name, $arg_param, $uri_param, - 'asciidoc', $pwcallback, $stringfilemap); + my $synopsis = "*${exename}* `help`\n\n"; + $synopsis .= &$generate_usage_str({format => 'asciidoc'}); return $synopsis; } @@ -132,24 +202,11 @@ sub print_asciidoc_synopsis { die "not initialized" if !($cmddef && $exename && $cli_handler_class); - my $pwcallback = $cli_handler_class->can('read_password'); - my $stringfilemap = $cli_handler_class->can('string_param_file_mapping'); - my $synopsis = ""; $synopsis .= "*${exename}* `<COMMAND> [ARGS] [OPTIONS]`\n\n"; - my $oldclass; - foreach my $cmd (sort keys %$cmddef) { - my ($class, $name, $arg_param, $uri_param) = @{$cmddef->{$cmd}}; - my $str = $class->usage_str($name, "$exename $cmd", $arg_param, - $uri_param, 'asciidoc', $pwcallback, - $stringfilemap); - $synopsis .= "\n" if $oldclass && $oldclass ne $class; - - $synopsis .= "$str\n\n"; - $oldclass = $class; - } + $synopsis .= &$generate_usage_str({format => 'asciidoc'}); $synopsis .= "\n"; @@ -160,21 +217,11 @@ sub print_usage_verbose { die "not initialized" if !($cmddef && $exename && $cli_handler_class); - my $pwcallback = $cli_handler_class->can('read_password'); - my $stringfilemap = $cli_handler_class->can('string_param_file_mapping'); - print "USAGE: $exename <COMMAND> [ARGS] [OPTIONS]\n\n"; - foreach my $cmd (sort keys %$cmddef) { - my ($class, $name, $arg_param, $uri_param) = @{$cmddef->{$cmd}}; - my $str = $class->usage_str($name, "$exename $cmd", $arg_param, $uri_param, - 'full', $pwcallback, $stringfilemap); - print "$str\n\n"; - } -} + my $str = &$generate_usage_str({format => 'full', indent => ' ' x 7}); -sub sorted_commands { - return sort { ($cmddef->{$a}->[0] cmp $cmddef->{$b}->[0]) || ($a cmp $b)} keys %$cmddef; + print "$str\n\n"; } sub print_usage_short { @@ -182,22 +229,48 @@ sub print_usage_short { die "not initialized" if !($cmddef && $exename && $cli_handler_class); - my $pwcallback = $cli_handler_class->can('read_password'); - my $stringfilemap = $cli_handler_class->can('string_param_file_mapping'); - print $fd "ERROR: $msg\n" if $msg; print $fd "USAGE: $exename <COMMAND> [ARGS] [OPTIONS]\n"; - my $oldclass; - foreach my $cmd (sorted_commands()) { - my ($class, $name, $arg_param, $uri_param) = @{$cmddef->{$cmd}}; - my $str = $class->usage_str($name, "$exename $cmd", $arg_param, $uri_param, 'short', $pwcallback, $stringfilemap); - print $fd "\n" if $oldclass && $oldclass ne $class; - print $fd " $str"; - $oldclass = $class; - } + print &$generate_usage_str({format => 'short', sect_sep => "\n", sortfunc => + sub { + my ($hash) = @_; + return sort { + if ((ref($hash->{$a}) eq 'ARRAY' && ref($hash->{$b}) eq 'ARRAY') && + ($hash->{$a}->[0] ne $hash->{$b}->[0])) { + return $hash->{$a}->[0] cmp $hash->{$b}->[0]; + } elsif (ref($hash->{$a}) eq 'ARRAY' xor ref($hash->{$b}) eq 'ARRAY') { + return ref($hash->{$b}) eq 'ARRAY' ? -1 : 1; + } else { + return $a cmp $b; + } + } keys %$hash; + }, indent => ' ' x 7}); } +my $print_help_short = sub { + my ($fd, $cmd, $msg) = @_; + + die "not initialized" if !($cmddef); + + print $fd "ERROR: $msg\n" if $msg; + + my $base = $cmddef; + while (scalar(@$cmd) > 1) { + $base = $base->{shift @$cmd}; + } + + my $str = &$generate_usage_str({ + format => 'short', + base => $base, + cmd => $cmd->[0], + indent => ' ' x 7, + }); + $str =~ s/^\s+//; + + print "USAGE: $str\n"; +}; + my $print_bash_completion = sub { my ($cmddef, $simple_cmd, $bash_command, $cur, $prev) = @_; @@ -375,12 +448,11 @@ sub generate_asciidoc_synopsis { no strict 'refs'; my $def = ${"${class}::cmddef"}; + $cmddef = $def; if (ref($def) eq 'ARRAY') { print_simple_asciidoc_synopsis(@$def); } else { - $cmddef = $def; - $cmddef->{help} = [ __PACKAGE__, 'help', ['cmd'] ]; print_asciidoc_synopsis(); @@ -405,33 +477,44 @@ my $handle_cmd = sub { # call verifyapi before setup_environment(), because we do not want to # execute any real code in this case - if (!$cmd) { + if (!defined($cmd->[0])) { print_usage_short (\*STDERR, "no command specified"); exit (-1); - } elsif ($cmd eq 'verifyapi') { + } elsif ($cmd->[0] eq 'verifyapi') { PVE::RESTHandler::validate_method_schemas(); return; } $cli_handler_class->setup_environment(); - if ($cmd eq 'bashcomplete') { + if ($cmd->[0] eq 'bashcomplete') { &$print_bash_completion($cmddef, 0, @$args); return; } &$preparefunc() if $preparefunc; - $cmd = &$expand_command_name($cmddef, $cmd); + unshift @$args, shift @$cmd; + my $base = $def; + while (scalar(@$args) > 0) { + last if (ref($base) eq 'ARRAY'); + push @$cmd, &$expand_command_name($base, shift @$args); + $base = $base->{$cmd->[-1]}; + } + + if (ref($base) eq 'HASH') { + &$print_help_short (\*STDERR, $cmd, "incomplete command '" . join(' ', @$cmd) . "'"); + exit (-1); + } - my ($class, $name, $arg_param, $uri_param, $outsub) = @{$cmddef->{$cmd} || []}; + my ($class, $name, $arg_param, $uri_param, $outsub) = @{$base || []}; if (!$class) { - print_usage_short (\*STDERR, "unknown command '$cmd'"); + print_usage_short (\*STDERR, "unknown command '" . join(' ', @$cmd) . "'"); exit (-1); } - my $prefix = "$exename $cmd"; + my $prefix = "$exename " . join(' ', @$cmd); my $res = $class->cli_handler($prefix, $name, \@ARGV, $arg_param, $uri_param, $pwcallback, $stringfilemap); &$outsub($res) if $outsub; @@ -446,7 +529,7 @@ my $handle_simple_cmd = sub { if (scalar(@$args) >= 1) { if ($args->[0] eq 'help') { my $str = "USAGE: $name help\n"; - $str .= $class->usage_str($name, $name, $arg_param, $uri_param, 'long', $pwcallback, $stringfilemap); + $str .= &$generate_usage_str({format => 'long'}); print STDERR "$str\n\n"; return; } elsif ($args->[0] eq 'verifyapi') { @@ -513,8 +596,8 @@ sub run_cli_handler { &$handle_simple_cmd($def, \@ARGV, $pwcallback, $preparefunc, $stringfilemap); } else { $cmddef = $def; - my $cmd = shift @ARGV; - &$handle_cmd($cmddef, $exename, $cmd, \@ARGV, $pwcallback, $preparefunc, $stringfilemap); + my @cmd = shift @ARGV; + &$handle_cmd($cmddef, $exename, \@cmd, \@ARGV, $pwcallback, $preparefunc, $stringfilemap); } exit 0; -- 2.11.0 _______________________________________________ pve-devel mailing list pve-devel@pve.proxmox.com https://pve.proxmox.com/cgi-bin/mailman/listinfo/pve-devel