1 package PVE
::CLIHandler
;
7 use PVE
::Exception
qw(raise raise_param_exc);
11 use base
qw(PVE::RESTHandler);
15 my $cli_handler_class;
17 my $assert_initialized = sub {
19 die "$caller[0]:$caller[2] - not initialized\n"
20 if !($cmddef && $exename && $cli_handler_class);
24 my ($reason, $cmd) = @_;
25 print_usage_short
(\
*STDERR
, $reason, $cmd);
29 my $expand_command_name = sub {
33 my @expanded = grep { /^\Q$cmd\E/ } keys %$def;
34 return $expanded[0] if scalar(@expanded) == 1; # enforce exact match
39 my $get_commands = sub {
40 my $def = shift // die "no command definition passed!";
41 return [ grep { !(ref($def->{$_}) eq 'HASH' && defined($def->{$_}->{alias
})) } sort keys %$def ];
44 my $complete_command_names = sub { $get_commands->($cmddef) };
46 # traverses the command definition using the $argv array, resolving one level
48 # Returns the matching (sub) command and its definition, and argument array for
49 # this (sub) command and a hash where we marked which (sub) commands got
50 # expanded (e.g. st => status) while traversing
52 my ($argv, $is_alias) = @_;
54 my ($def, $cmd) = ($cmddef, $argv);
56 if (ref($argv) eq 'ARRAY') {
58 my $last_arg_id = scalar(@$argv) - 1;
60 for my $i (0..$last_arg_id) {
61 $cmd = $expand_command_name->($def, $argv->[$i]);
62 $expanded->{$argv->[$i]} = $cmd if $cmd ne $argv->[$i];
63 last if !defined($def->{$cmd});
66 if (ref($def) eq 'ARRAY') {
67 # could expand to a real command, rest of $argv are its arguments
68 my $cmd_args = [ @$argv[$i+1..$last_arg_id] ];
69 return ($cmd, $def, $cmd_args, $expanded);
72 if (defined($def->{alias
})) {
73 die "alias loop detected for '$cmd'" if $is_alias; # avoids cycles
74 # replace aliased (sub)command with the expanded aliased command
75 splice @$argv, $i, 1, split(/ +/, $def->{alias
});
76 return resolve_cmd
($argv, 1);
79 # got either a special command (bashcomplete, verifyapi) or an unknown
80 # cmd, just return first entry as cmd and the rest of $argv as cmd_arg
81 my $cmd_args = [ @$argv[1..$last_arg_id] ];
82 return ($argv->[0], $def, $cmd_args, $expanded);
87 sub generate_usage_str
{
88 my ($format, $cmd, $indent, $separator, $sortfunc) = @_;
90 $assert_initialized->();
91 die 'format required' if !$format;
93 $sortfunc //= sub { sort keys %{$_[0]} };
97 my $can_read_pass = $cli_handler_class->can('read_password');
98 my $can_str_param_fmap = $cli_handler_class->can('string_param_file_mapping');
100 my ($subcmd, $def) = resolve_cmd
($cmd);
104 my ($indent, $separator, $def, $prefix) = @_;
107 if (ref($def) eq 'HASH') {
108 my $oldclass = undef;
109 foreach my $cmd (&$sortfunc($def)) {
111 if (ref($def->{$cmd}) eq 'ARRAY') {
112 my ($class, $name, $arg_param, $fixed_param) = @{$def->{$cmd}};
114 $str .= $separator if $oldclass && $oldclass ne $class;
116 $str .= $class->usage_str($name, "$prefix $cmd", $arg_param,
117 $fixed_param, $format,
118 $can_read_pass, $can_str_param_fmap);
121 } elsif (defined($def->{$cmd}->{alias
}) && ($format eq 'asciidoc')) {
123 $str .= "*$prefix $cmd*\n\nAn alias for '$exename $def->{$cmd}->{alias}'.\n\n";
126 next if $def->{$cmd}->{alias
};
128 my $substr = $generate->($indent, $separator, $def->{$cmd}, "$prefix $cmd");
130 $substr .= $separator if $substr !~ /\Q$separator\E{2}/;
137 my ($class, $name, $arg_param, $fixed_param) = @$def;
138 $abort->("unknown command '$cmd'") if !$class;
141 $str .= $class->usage_str($name, $prefix, $arg_param, $fixed_param, $format,
142 $can_read_pass, $can_str_param_fmap);
147 my $cmdstr = $exename;
148 $cmdstr .= ' ' . join(' ', @$cmd) if defined($cmd);
150 return $generate->($indent, $separator, $def, $cmdstr);
153 __PACKAGE__-
>register_method ({
157 description
=> "Get help about specified command.",
159 additionalProperties
=> 0,
162 description
=> "Command name",
165 completion
=> $complete_command_names,
168 description
=> "Verbose output format.",
174 returns
=> { type
=> 'null' },
179 $assert_initialized->();
181 my $cmd = $param->{cmd
};
183 my $verbose = defined($cmd) && $cmd;
184 $verbose = $param->{verbose
} if defined($param->{verbose
});
188 print_usage_verbose
();
190 print_usage_short
(\
*STDOUT
);
197 $str = generate_usage_str
('full', $cmd, '');
199 $str = generate_usage_str
('short', $cmd, ' ' x
7);
206 print "USAGE: $str\n";
213 sub print_simple_asciidoc_synopsis
{
214 $assert_initialized->();
216 my $synopsis = "*${exename}* `help`\n\n";
217 $synopsis .= generate_usage_str
('asciidoc');
222 sub print_asciidoc_synopsis
{
223 $assert_initialized->();
227 $synopsis .= "*${exename}* `<COMMAND> [ARGS] [OPTIONS]`\n\n";
229 $synopsis .= generate_usage_str
('asciidoc');
236 sub print_usage_verbose
{
237 $assert_initialized->();
239 print "USAGE: $exename <COMMAND> [ARGS] [OPTIONS]\n\n";
241 my $str = generate_usage_str
('full');
246 sub print_usage_short
{
247 my ($fd, $msg, $cmd) = @_;
249 $assert_initialized->();
251 print $fd "ERROR: $msg\n" if $msg;
252 print $fd "USAGE: $exename <COMMAND> [ARGS] [OPTIONS]\n";
254 print {$fd} generate_usage_str
('short', $cmd, ' ' x
7, "\n", sub {
257 if (ref($h->{$a}) eq 'ARRAY' && ref($h->{$b}) eq 'ARRAY') {
258 # $a and $b are both real commands order them by their class
259 return $h->{$a}->[0] cmp $h->{$b}->[0] || $a cmp $b;
260 } elsif (ref($h->{$a}) eq 'ARRAY' xor ref($h->{$b}) eq 'ARRAY') {
261 # real command and subcommand mixed, put sub commands first
262 return ref($h->{$b}) eq 'ARRAY' ?
-1 : 1;
264 # both are either from the same class or subcommands
271 my $print_bash_completion = sub {
272 my ($simple_cmd, $bash_command, $cur, $prev) = @_;
276 return if !(defined($cur) && defined($prev) && defined($bash_command));
277 return if !defined($ENV{COMP_LINE
});
278 return if !defined($ENV{COMP_POINT
});
280 my $cmdline = substr($ENV{COMP_LINE
}, 0, $ENV{COMP_POINT
});
281 print STDERR
"\nCMDLINE: $ENV{COMP_LINE}\n" if $debug;
283 my $args = PVE
::Tools
::split_args
($cmdline);
284 shift @$args; # no need for program name
285 my $print_result = sub {
287 print "$p\n" if $p =~ m/^$cur/;
291 my ($cmd, $def) = ($simple_cmd, $cmddef);
293 ($cmd, $def, $args, my $expaned) = resolve_cmd
($args);
295 if (ref($def) eq 'HASH') {
296 &$print_result(@{$get_commands->($def)});
299 if (my $expanded_cmd = $expaned->{$cur}) {
300 print "$expanded_cmd\n";
306 my $pos = scalar(@$args) - 1;
307 $pos += 1 if $cmdline =~ m/\s+$/;
308 print STDERR
"pos: $pos\n" if $debug;
313 my ($class, $name, $arg_param, $uri_param) = @$def;
317 $arg_param = [ $arg_param ] if !ref($arg_param);
319 map { $skip_param->{$_} = 1; } @$arg_param;
320 map { $skip_param->{$_} = 1; } keys %$uri_param;
322 my $info = $class->map_method_by_name($name);
324 my $prop = $info->{parameters
}->{properties
};
326 my $print_parameter_completion = sub {
328 my $d = $prop->{$pname};
329 if ($d->{completion
}) {
330 my $vt = ref($d->{completion
});
332 my $res = $d->{completion
}->($cmd, $pname, $cur, $args);
333 &$print_result(@$res);
335 } elsif ($d->{type
} eq 'boolean') {
336 &$print_result('0', '1');
337 } elsif ($d->{enum
}) {
338 &$print_result(@{$d->{enum
}});
342 # positional arguments
343 $pos++ if $simple_cmd;
344 if ($pos < scalar(@$arg_param)) {
345 my $pname = $arg_param->[$pos];
346 &$print_parameter_completion($pname);
350 my @option_list = ();
351 foreach my $key (keys %$prop) {
352 next if $skip_param->{$key};
353 push @option_list, "--$key";
357 &$print_result(@option_list);
361 if ($prev =~ m/^--?(.+)$/ && $prop->{$1}) {
363 &$print_parameter_completion($pname);
367 &$print_result(@option_list);
373 # simply verify all registered methods
374 PVE
::RESTHandler
::validate_method_schemas
();
377 my $get_exe_name = sub {
387 sub generate_bash_completions
{
390 # generate bash completion config
392 $exename = &$get_exe_name($class);
395 # $exename bash completion
397 # see http://tiswww.case.edu/php/chet/bash/FAQ
398 # and __ltrim_colon_completions() in /usr/share/bash-completion/bash_completion
399 # this modifies global var, but I found no better way
400 COMP_WORDBREAKS=\${COMP_WORDBREAKS//:}
402 complete -o default -C '$exename bashcomplete' $exename
406 sub generate_asciidoc_synopsys
{
408 $class->generate_asciidoc_synopsis();
411 sub generate_asciidoc_synopsis
{
414 $cli_handler_class = $class;
416 $exename = &$get_exe_name($class);
419 my $def = ${"${class}::cmddef"};
422 if (ref($def) eq 'ARRAY') {
423 print_simple_asciidoc_synopsis
();
425 $cmddef->{help
} = [ __PACKAGE__
, 'help', ['cmd'] ];
427 print_asciidoc_synopsis
();
431 # overwrite this if you want to run/setup things early
432 sub setup_environment
{
435 # do nothing by default
438 my $handle_cmd = sub {
439 my ($args, $pwcallback, $preparefunc, $stringfilemap) = @_;
441 $cmddef->{help
} = [ __PACKAGE__
, 'help', ['cmd'] ];
443 my $cmd_str = join(' ', @$args);
444 my ($cmd, $def, $cmd_args) = resolve_cmd
($args);
446 $abort->("no command specified") if !$cmd;
448 # call verifyapi before setup_environment(), don't execute any real code in
450 if ($cmd eq 'verifyapi') {
451 PVE
::RESTHandler
::validate_method_schemas
();
455 $cli_handler_class->setup_environment();
457 if ($cmd eq 'bashcomplete') {
458 &$print_bash_completion(undef, @$cmd_args);
462 # checked special commands, if def is still a hash we got an incomplete sub command
463 $abort->("incomplete command '$exename $cmd_str'") if ref($def) eq 'HASH';
465 &$preparefunc() if $preparefunc;
467 my ($class, $name, $arg_param, $uri_param, $outsub) = @{$cmddef->{$cmd} || []};
468 $abort->("unknown command '$cmd_str'") if !$class;
470 my $prefix = "$exename $cmd_str";
471 my $res = $class->cli_handler($prefix, $name, $cmd_args, $arg_param, $uri_param, $pwcallback, $stringfilemap);
473 &$outsub($res) if $outsub;
476 my $handle_simple_cmd = sub {
477 my ($args, $pwcallback, $preparefunc, $stringfilemap) = @_;
479 my ($class, $name, $arg_param, $uri_param, $outsub) = @{$cmddef};
480 die "no class specified" if !$class;
482 if (scalar(@$args) >= 1) {
483 if ($args->[0] eq 'help') {
484 my $str = "USAGE: $name help\n";
485 $str .= generate_usage_str
('long');
486 print STDERR
"$str\n\n";
488 } elsif ($args->[0] eq 'verifyapi') {
489 PVE
::RESTHandler
::validate_method_schemas
();
494 $cli_handler_class->setup_environment();
496 if (scalar(@$args) >= 1) {
497 if ($args->[0] eq 'bashcomplete') {
499 &$print_bash_completion($name, @$args);
504 &$preparefunc() if $preparefunc;
506 my $res = $class->cli_handler($name, $name, \
@ARGV, $arg_param, $uri_param, $pwcallback, $stringfilemap);
508 &$outsub($res) if $outsub;
511 sub run_cli_handler
{
512 my ($class, %params) = @_;
514 $cli_handler_class = $class;
516 $ENV{'PATH'} = '/sbin:/bin:/usr/sbin:/usr/bin';
518 foreach my $key (keys %params) {
519 next if $key eq 'prepare';
520 next if $key eq 'no_init'; # not used anymore
521 next if $key eq 'no_rpcenv'; # not used anymore
522 die "unknown parameter '$key'";
525 my $preparefunc = $params{prepare
};
527 my $pwcallback = $class->can('read_password');
528 my $stringfilemap = $class->can('string_param_file_mapping');
530 $exename = &$get_exe_name($class);
535 $cmddef = ${"${class}::cmddef"};
537 if (ref($cmddef) eq 'ARRAY') {
538 &$handle_simple_cmd(\
@ARGV, $pwcallback, $preparefunc, $stringfilemap);
540 &$handle_cmd(\
@ARGV, $pwcallback, $preparefunc, $stringfilemap);