]> git.proxmox.com Git - pve-common.git/blob - src/PVE/CLIHandler.pm
cli: factor out generate usage string
[pve-common.git] / src / PVE / CLIHandler.pm
1 package PVE::CLIHandler;
2
3 use strict;
4 use warnings;
5
6 use PVE::SafeSyslog;
7 use PVE::Exception qw(raise raise_param_exc);
8 use PVE::RESTHandler;
9 use PVE::INotify;
10
11 use base qw(PVE::RESTHandler);
12
13 my $cmddef;
14 my $exename;
15 my $cli_handler_class;
16
17 my $assert_initialized = sub {
18 my @caller = caller;
19 die "$caller[0]:$caller[2] - not initialized\n"
20 if !($cmddef && $exename && $cli_handler_class);
21 };
22
23 my $abort = sub {
24 my ($reason, $cmd) = @_;
25 print_usage_short (\*STDERR, $reason, $cmd);
26 exit (-1);
27 };
28
29 my $expand_command_name = sub {
30 my ($def, $cmd) = @_;
31
32 if (!$def->{$cmd}) {
33 my @expanded = grep { /^\Q$cmd\E/ } keys %$def;
34 return $expanded[0] if scalar(@expanded) == 1; # enforce exact match
35 }
36 return $cmd;
37 };
38
39 my $complete_command_names = sub {
40 return [ sort keys %$cmddef ];
41 };
42
43 sub generate_usage_str {
44 my ($format, $cmd, $indent, $separator, $sortfunc) = @_;
45
46 $assert_initialized->();
47 die 'format required' if !$format;
48
49 $sortfunc //= sub { sort keys %{$_[0]} };
50 $separator //= '';
51 $indent //= '';
52
53 my $can_read_pass = $cli_handler_class->can('read_password');
54 my $can_str_param_fmap = $cli_handler_class->can('string_param_file_mapping');
55
56 my $def = $cmddef;
57 $def = $def->{$cmd} if $cmd && ref($def) eq 'HASH' && $def->{$cmd};
58
59 my $generate;
60 $generate = sub {
61 my ($indent, $separator, $def, $prefix) = @_;
62
63 my $str = '';
64 if (ref($def) eq 'HASH') {
65 my $oldclass = undef;
66 foreach my $cmd (&$sortfunc($def)) {
67
68 if (ref($def->{$cmd}) eq 'ARRAY') {
69 my ($class, $name, $arg_param, $fixed_param) = @{$def->{$cmd}};
70
71 $str .= $separator if $oldclass && $oldclass ne $class;
72 $str .= $indent;
73 $str .= $class->usage_str($name, "$prefix $cmd", $arg_param,
74 $fixed_param, $format,
75 $can_read_pass, $can_str_param_fmap);
76 $oldclass = $class;
77 }
78
79 }
80 } else {
81 my ($class, $name, $arg_param, $fixed_param) = @$def;
82 $abort->("unknown command '$cmd'") if !$class;
83
84 $str .= $indent;
85 $str .= $class->usage_str($name, $prefix, $arg_param, $fixed_param, $format,
86 $can_read_pass, $can_str_param_fmap);
87 }
88 return $str;
89 };
90
91 return $generate->($indent, $separator, $def, $exename);
92 }
93
94 __PACKAGE__->register_method ({
95 name => 'help',
96 path => 'help',
97 method => 'GET',
98 description => "Get help about specified command.",
99 parameters => {
100 additionalProperties => 0,
101 properties => {
102 cmd => {
103 description => "Command name",
104 type => 'string',
105 optional => 1,
106 completion => $complete_command_names,
107 },
108 verbose => {
109 description => "Verbose output format.",
110 type => 'boolean',
111 optional => 1,
112 },
113 },
114 },
115 returns => { type => 'null' },
116
117 code => sub {
118 my ($param) = @_;
119
120 $assert_initialized->();
121
122 my $cmd = $param->{cmd};
123
124 my $verbose = defined($cmd) && $cmd;
125 $verbose = $param->{verbose} if defined($param->{verbose});
126
127 if (!$cmd) {
128 if ($verbose) {
129 print_usage_verbose();
130 } else {
131 print_usage_short(\*STDOUT);
132 }
133 return undef;
134 }
135
136 my $str;
137 if ($verbose) {
138 $str = generate_usage_str('full', $cmd, '');
139 } else {
140 $str = generate_usage_str('short', $cmd, ' ' x 7);
141 }
142 $str =~ s/^\s+//;
143
144 if ($verbose) {
145 print "$str\n";
146 } else {
147 print "USAGE: $str\n";
148 }
149
150 return undef;
151
152 }});
153
154 sub print_simple_asciidoc_synopsis {
155 $assert_initialized->();
156
157 my $synopsis = "*${exename}* `help`\n\n";
158 $synopsis .= generate_usage_str('asciidoc');
159
160 return $synopsis;
161 }
162
163 sub print_asciidoc_synopsis {
164 $assert_initialized->();
165
166 my $synopsis = "";
167
168 $synopsis .= "*${exename}* `<COMMAND> [ARGS] [OPTIONS]`\n\n";
169
170 $synopsis .= generate_usage_str('asciidoc');
171
172 $synopsis .= "\n";
173
174 return $synopsis;
175 }
176
177 sub print_usage_verbose {
178 $assert_initialized->();
179
180 print "USAGE: $exename <COMMAND> [ARGS] [OPTIONS]\n\n";
181
182 my $str = generate_usage_str('full');
183
184 print "$str\n";
185 }
186
187 sub print_usage_short {
188 my ($fd, $msg) = @_;
189
190 $assert_initialized->();
191
192 print $fd "ERROR: $msg\n" if $msg;
193 print $fd "USAGE: $exename <COMMAND> [ARGS] [OPTIONS]\n";
194
195 print {$fd} generate_usage_str('short', undef, ' ' x 7, "\n", sub {
196 my ($h) = @_;
197 return sort {
198 if (ref($h->{$a}) eq 'ARRAY' && ref($h->{$b}) eq 'ARRAY') {
199 # $a and $b are both real commands order them by their class
200 return $h->{$a}->[0] cmp $h->{$b}->[0] || $a cmp $b;
201 } else {
202 # both are from the same class
203 return $a cmp $b;
204 }
205 } keys %$h;
206 });
207 }
208
209 my $print_bash_completion = sub {
210 my ($simple_cmd, $bash_command, $cur, $prev) = @_;
211
212 my $debug = 0;
213
214 return if !(defined($cur) && defined($prev) && defined($bash_command));
215 return if !defined($ENV{COMP_LINE});
216 return if !defined($ENV{COMP_POINT});
217
218 my $cmdline = substr($ENV{COMP_LINE}, 0, $ENV{COMP_POINT});
219 print STDERR "\nCMDLINE: $ENV{COMP_LINE}\n" if $debug;
220
221 my $args = PVE::Tools::split_args($cmdline);
222 shift @$args; # no need for program name
223 my $print_result = sub {
224 foreach my $p (@_) {
225 print "$p\n" if $p =~ m/^$cur/;
226 }
227 };
228
229 my ($cmd, $def) = ($simple_cmd, $cmddef);
230 if (!$simple_cmd) {
231 if (!scalar(@$args)) {
232 &$print_result(keys %$def);
233 return;
234 }
235 $cmd = $args->[0];
236 }
237 $def = $def->{$cmd};
238 return if !$def;
239
240 my $pos = scalar(@$args) - 1;
241 $pos += 1 if $cmdline =~ m/\s+$/;
242 print STDERR "pos: $pos\n" if $debug;
243 return if $pos < 0;
244
245 my $skip_param = {};
246
247 my ($class, $name, $arg_param, $uri_param) = @$def;
248 $arg_param //= [];
249 $uri_param //= {};
250
251 $arg_param = [ $arg_param ] if !ref($arg_param);
252
253 map { $skip_param->{$_} = 1; } @$arg_param;
254 map { $skip_param->{$_} = 1; } keys %$uri_param;
255
256 my $info = $class->map_method_by_name($name);
257
258 my $prop = $info->{parameters}->{properties};
259
260 my $print_parameter_completion = sub {
261 my ($pname) = @_;
262 my $d = $prop->{$pname};
263 if ($d->{completion}) {
264 my $vt = ref($d->{completion});
265 if ($vt eq 'CODE') {
266 my $res = $d->{completion}->($cmd, $pname, $cur, $args);
267 &$print_result(@$res);
268 }
269 } elsif ($d->{type} eq 'boolean') {
270 &$print_result('0', '1');
271 } elsif ($d->{enum}) {
272 &$print_result(@{$d->{enum}});
273 }
274 };
275
276 # positional arguments
277 $pos++ if $simple_cmd;
278 if ($pos < scalar(@$arg_param)) {
279 my $pname = $arg_param->[$pos];
280 &$print_parameter_completion($pname);
281 return;
282 }
283
284 my @option_list = ();
285 foreach my $key (keys %$prop) {
286 next if $skip_param->{$key};
287 push @option_list, "--$key";
288 }
289
290 if ($cur =~ m/^-/) {
291 &$print_result(@option_list);
292 return;
293 }
294
295 if ($prev =~ m/^--?(.+)$/ && $prop->{$1}) {
296 my $pname = $1;
297 &$print_parameter_completion($pname);
298 return;
299 }
300
301 &$print_result(@option_list);
302 };
303
304 sub verify_api {
305 my ($class) = @_;
306
307 # simply verify all registered methods
308 PVE::RESTHandler::validate_method_schemas();
309 }
310
311 my $get_exe_name = sub {
312 my ($class) = @_;
313
314 my $name = $class;
315 $name =~ s/^.*:://;
316 $name =~ s/_/-/g;
317
318 return $name;
319 };
320
321 sub generate_bash_completions {
322 my ($class) = @_;
323
324 # generate bash completion config
325
326 $exename = &$get_exe_name($class);
327
328 print <<__EOD__;
329 # $exename bash completion
330
331 # see http://tiswww.case.edu/php/chet/bash/FAQ
332 # and __ltrim_colon_completions() in /usr/share/bash-completion/bash_completion
333 # this modifies global var, but I found no better way
334 COMP_WORDBREAKS=\${COMP_WORDBREAKS//:}
335
336 complete -o default -C '$exename bashcomplete' $exename
337 __EOD__
338 }
339
340 sub generate_asciidoc_synopsys {
341 my ($class) = @_;
342 $class->generate_asciidoc_synopsis();
343 };
344
345 sub generate_asciidoc_synopsis {
346 my ($class) = @_;
347
348 $cli_handler_class = $class;
349
350 $exename = &$get_exe_name($class);
351
352 no strict 'refs';
353 my $def = ${"${class}::cmddef"};
354 $cmddef = $def;
355
356 if (ref($def) eq 'ARRAY') {
357 print_simple_asciidoc_synopsis();
358 } else {
359 $cmddef->{help} = [ __PACKAGE__, 'help', ['cmd'] ];
360
361 print_asciidoc_synopsis();
362 }
363 }
364
365 # overwrite this if you want to run/setup things early
366 sub setup_environment {
367 my ($class) = @_;
368
369 # do nothing by default
370 }
371
372 my $handle_cmd = sub {
373 my ($args, $pwcallback, $preparefunc, $stringfilemap) = @_;
374
375 $cmddef->{help} = [ __PACKAGE__, 'help', ['cmd'] ];
376
377
378 my $cmd = shift @$args;
379 $abort->("no command specified") if !$cmd;
380
381 # call verifyapi before setup_environment(), don't execute any real code in
382 # this case
383 if ($cmd eq 'verifyapi') {
384 PVE::RESTHandler::validate_method_schemas();
385 return;
386 }
387
388 $cli_handler_class->setup_environment();
389
390 if ($cmd eq 'bashcomplete') {
391 &$print_bash_completion(undef, @$args);
392 return;
393 }
394
395 &$preparefunc() if $preparefunc;
396
397 $cmd = &$expand_command_name($cmddef, $cmd);
398
399 my ($class, $name, $arg_param, $uri_param, $outsub) = @{$cmddef->{$cmd} || []};
400 $abort->("unknown command '$cmd'") if !$class;
401
402 my $prefix = "$exename $cmd";
403 my $res = $class->cli_handler($prefix, $name, \@ARGV, $arg_param, $uri_param, $pwcallback, $stringfilemap);
404
405 &$outsub($res) if $outsub;
406 };
407
408 my $handle_simple_cmd = sub {
409 my ($args, $pwcallback, $preparefunc, $stringfilemap) = @_;
410
411 my ($class, $name, $arg_param, $uri_param, $outsub) = @{$cmddef};
412 die "no class specified" if !$class;
413
414 if (scalar(@$args) >= 1) {
415 if ($args->[0] eq 'help') {
416 my $str = "USAGE: $name help\n";
417 $str .= generate_usage_str('long');
418 print STDERR "$str\n\n";
419 return;
420 } elsif ($args->[0] eq 'verifyapi') {
421 PVE::RESTHandler::validate_method_schemas();
422 return;
423 }
424 }
425
426 $cli_handler_class->setup_environment();
427
428 if (scalar(@$args) >= 1) {
429 if ($args->[0] eq 'bashcomplete') {
430 shift @$args;
431 &$print_bash_completion($name, @$args);
432 return;
433 }
434 }
435
436 &$preparefunc() if $preparefunc;
437
438 my $res = $class->cli_handler($name, $name, \@ARGV, $arg_param, $uri_param, $pwcallback, $stringfilemap);
439
440 &$outsub($res) if $outsub;
441 };
442
443 sub run_cli_handler {
444 my ($class, %params) = @_;
445
446 $cli_handler_class = $class;
447
448 $ENV{'PATH'} = '/sbin:/bin:/usr/sbin:/usr/bin';
449
450 foreach my $key (keys %params) {
451 next if $key eq 'prepare';
452 next if $key eq 'no_init'; # not used anymore
453 next if $key eq 'no_rpcenv'; # not used anymore
454 die "unknown parameter '$key'";
455 }
456
457 my $preparefunc = $params{prepare};
458
459 my $pwcallback = $class->can('read_password');
460 my $stringfilemap = $class->can('string_param_file_mapping');
461
462 $exename = &$get_exe_name($class);
463
464 initlog($exename);
465
466 no strict 'refs';
467 $cmddef = ${"${class}::cmddef"};
468
469 if (ref($cmddef) eq 'ARRAY') {
470 &$handle_simple_cmd(\@ARGV, $pwcallback, $preparefunc, $stringfilemap);
471 } else {
472 &$handle_cmd(\@ARGV, $pwcallback, $preparefunc, $stringfilemap);
473 }
474
475 exit 0;
476 }
477
478 1;