]> git.proxmox.com Git - pve-common.git/blame - src/PVE/CLIHandler.pm
cli: factor out generate usage string
[pve-common.git] / src / PVE / CLIHandler.pm
CommitLineData
e143e9d8
DM
1package PVE::CLIHandler;
2
3use strict;
4use warnings;
5
93ddd7bc 6use PVE::SafeSyslog;
e143e9d8
DM
7use PVE::Exception qw(raise raise_param_exc);
8use PVE::RESTHandler;
881eb755 9use PVE::INotify;
e143e9d8
DM
10
11use base qw(PVE::RESTHandler);
12
13my $cmddef;
14my $exename;
891b798d 15my $cli_handler_class;
e143e9d8 16
d204696c
TL
17my $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
918140af
TL
23my $abort = sub {
24 my ($reason, $cmd) = @_;
25 print_usage_short (\*STDERR, $reason, $cmd);
26 exit (-1);
27};
28
e143e9d8
DM
29my $expand_command_name = sub {
30 my ($def, $cmd) = @_;
31
32 if (!$def->{$cmd}) {
7bac844e
TL
33 my @expanded = grep { /^\Q$cmd\E/ } keys %$def;
34 return $expanded[0] if scalar(@expanded) == 1; # enforce exact match
e143e9d8
DM
35 }
36 return $cmd;
37};
38
edf3d572 39my $complete_command_names = sub {
7bac844e 40 return [ sort keys %$cmddef ];
edf3d572
DM
41};
42
4c802a57
TL
43sub 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
e143e9d8 94__PACKAGE__->register_method ({
3ef20687 95 name => 'help',
e143e9d8
DM
96 path => 'help',
97 method => 'GET',
98 description => "Get help about specified command.",
99 parameters => {
3ef20687 100 additionalProperties => 0,
e143e9d8
DM
101 properties => {
102 cmd => {
103 description => "Command name",
104 type => 'string',
105 optional => 1,
edf3d572 106 completion => $complete_command_names,
e143e9d8
DM
107 },
108 verbose => {
109 description => "Verbose output format.",
110 type => 'boolean',
111 optional => 1,
112 },
113 },
114 },
115 returns => { type => 'null' },
3ef20687 116
e143e9d8
DM
117 code => sub {
118 my ($param) = @_;
119
d204696c 120 $assert_initialized->();
e143e9d8
DM
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();
3ef20687 130 } else {
e143e9d8
DM
131 print_usage_short(\*STDOUT);
132 }
133 return undef;
134 }
135
4c802a57
TL
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+//;
e143e9d8 143
e143e9d8
DM
144 if ($verbose) {
145 print "$str\n";
146 } else {
147 print "USAGE: $str\n";
148 }
149
150 return undef;
151
152 }});
153
0a5a1eee 154sub print_simple_asciidoc_synopsis {
d204696c 155 $assert_initialized->();
fe3f1fde 156
4c802a57
TL
157 my $synopsis = "*${exename}* `help`\n\n";
158 $synopsis .= generate_usage_str('asciidoc');
fe3f1fde
DM
159
160 return $synopsis;
161}
162
0a5a1eee 163sub print_asciidoc_synopsis {
d204696c 164 $assert_initialized->();
fe3f1fde 165
fe3f1fde
DM
166 my $synopsis = "";
167
168 $synopsis .= "*${exename}* `<COMMAND> [ARGS] [OPTIONS]`\n\n";
169
4c802a57 170 $synopsis .= generate_usage_str('asciidoc');
fe3f1fde
DM
171
172 $synopsis .= "\n";
173
174 return $synopsis;
175}
176
e143e9d8 177sub print_usage_verbose {
d204696c 178 $assert_initialized->();
891b798d 179
e143e9d8
DM
180 print "USAGE: $exename <COMMAND> [ARGS] [OPTIONS]\n\n";
181
4c802a57 182 my $str = generate_usage_str('full');
e143e9d8 183
4c802a57 184 print "$str\n";
e143e9d8
DM
185}
186
187sub print_usage_short {
188 my ($fd, $msg) = @_;
189
d204696c 190 $assert_initialized->();
891b798d 191
e143e9d8
DM
192 print $fd "ERROR: $msg\n" if $msg;
193 print $fd "USAGE: $exename <COMMAND> [ARGS] [OPTIONS]\n";
194
4c802a57
TL
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 });
e143e9d8
DM
207}
208
d8053c08 209my $print_bash_completion = sub {
57e67ea3 210 my ($simple_cmd, $bash_command, $cur, $prev) = @_;
d8053c08
DM
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
58d9e664 221 my $args = PVE::Tools::split_args($cmdline);
5fa768fc 222 shift @$args; # no need for program name
d8053c08
DM
223 my $print_result = sub {
224 foreach my $p (@_) {
225 print "$p\n" if $p =~ m/^$cur/;
226 }
227 };
228
5fa768fc
TL
229 my ($cmd, $def) = ($simple_cmd, $cmddef);
230 if (!$simple_cmd) {
231 if (!scalar(@$args)) {
232 &$print_result(keys %$def);
d8053c08
DM
233 return;
234 }
5fa768fc 235 $cmd = $args->[0];
d8053c08 236 }
5fa768fc 237 $def = $def->{$cmd};
d8053c08
DM
238 return if !$def;
239
5fa768fc
TL
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;
d8053c08
DM
244
245 my $skip_param = {};
246
247 my ($class, $name, $arg_param, $uri_param) = @$def;
248 $arg_param //= [];
249 $uri_param //= {};
250
d90a2fd0
DM
251 $arg_param = [ $arg_param ] if !ref($arg_param);
252
d8053c08
DM
253 map { $skip_param->{$_} = 1; } @$arg_param;
254 map { $skip_param->{$_} = 1; } keys %$uri_param;
255
d8053c08
DM
256 my $info = $class->map_method_by_name($name);
257
5fa768fc 258 my $prop = $info->{parameters}->{properties};
d8053c08
DM
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') {
58d9e664 266 my $res = $d->{completion}->($cmd, $pname, $cur, $args);
d8053c08
DM
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
5fa768fc
TL
277 $pos++ if $simple_cmd;
278 if ($pos < scalar(@$arg_param)) {
279 my $pname = $arg_param->[$pos];
d8053c08
DM
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
1f130ba6
DM
304sub verify_api {
305 my ($class) = @_;
306
307 # simply verify all registered methods
308 PVE::RESTHandler::validate_method_schemas();
309}
310
8f3712f8
DM
311my $get_exe_name = sub {
312 my ($class) = @_;
3ef20687 313
8f3712f8
DM
314 my $name = $class;
315 $name =~ s/^.*:://;
316 $name =~ s/_/-/g;
317
318 return $name;
319};
320
c45707a0
DM
321sub generate_bash_completions {
322 my ($class) = @_;
323
324 # generate bash completion config
325
8f3712f8 326 $exename = &$get_exe_name($class);
c45707a0
DM
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
334COMP_WORDBREAKS=\${COMP_WORDBREAKS//:}
335
e4a1d8e2 336complete -o default -C '$exename bashcomplete' $exename
c45707a0
DM
337__EOD__
338}
339
fe3f1fde
DM
340sub generate_asciidoc_synopsys {
341 my ($class) = @_;
0a5a1eee
FG
342 $class->generate_asciidoc_synopsis();
343};
344
345sub generate_asciidoc_synopsis {
346 my ($class) = @_;
fe3f1fde
DM
347
348 $cli_handler_class = $class;
349
350 $exename = &$get_exe_name($class);
351
352 no strict 'refs';
353 my $def = ${"${class}::cmddef"};
57e67ea3 354 $cmddef = $def;
fe3f1fde
DM
355
356 if (ref($def) eq 'ARRAY') {
4c802a57 357 print_simple_asciidoc_synopsis();
fe3f1fde 358 } else {
fe3f1fde
DM
359 $cmddef->{help} = [ __PACKAGE__, 'help', ['cmd'] ];
360
0a5a1eee 361 print_asciidoc_synopsis();
fe3f1fde
DM
362 }
363}
364
7b7f99c9
DM
365# overwrite this if you want to run/setup things early
366sub setup_environment {
367 my ($class) = @_;
368
369 # do nothing by default
370}
371
891b798d 372my $handle_cmd = sub {
57e67ea3 373 my ($args, $pwcallback, $preparefunc, $stringfilemap) = @_;
e143e9d8
DM
374
375 $cmddef->{help} = [ __PACKAGE__, 'help', ['cmd'] ];
376
7b7f99c9 377
57e67ea3 378 my $cmd = shift @$args;
918140af
TL
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') {
e143e9d8
DM
384 PVE::RESTHandler::validate_method_schemas();
385 return;
7b7f99c9
DM
386 }
387
388 $cli_handler_class->setup_environment();
389
390 if ($cmd eq 'bashcomplete') {
57e67ea3 391 &$print_bash_completion(undef, @$args);
d8053c08 392 return;
e143e9d8
DM
393 }
394
8e3e9929
WB
395 &$preparefunc() if $preparefunc;
396
e143e9d8
DM
397 $cmd = &$expand_command_name($cmddef, $cmd);
398
399 my ($class, $name, $arg_param, $uri_param, $outsub) = @{$cmddef->{$cmd} || []};
918140af 400 $abort->("unknown command '$cmd'") if !$class;
e143e9d8
DM
401
402 my $prefix = "$exename $cmd";
408976c6 403 my $res = $class->cli_handler($prefix, $name, \@ARGV, $arg_param, $uri_param, $pwcallback, $stringfilemap);
2026f4b5
DM
404
405 &$outsub($res) if $outsub;
891b798d 406};
2026f4b5 407
891b798d 408my $handle_simple_cmd = sub {
57e67ea3 409 my ($args, $pwcallback, $preparefunc, $stringfilemap) = @_;
2026f4b5 410
57e67ea3 411 my ($class, $name, $arg_param, $uri_param, $outsub) = @{$cmddef};
2026f4b5
DM
412 die "no class specified" if !$class;
413
d8053c08 414 if (scalar(@$args) >= 1) {
2026f4b5
DM
415 if ($args->[0] eq 'help') {
416 my $str = "USAGE: $name help\n";
4c802a57 417 $str .= generate_usage_str('long');
2026f4b5
DM
418 print STDERR "$str\n\n";
419 return;
420 } elsif ($args->[0] eq 'verifyapi') {
421 PVE::RESTHandler::validate_method_schemas();
422 return;
2026f4b5 423 }
e143e9d8 424 }
2026f4b5 425
7b7f99c9
DM
426 $cli_handler_class->setup_environment();
427
428 if (scalar(@$args) >= 1) {
429 if ($args->[0] eq 'bashcomplete') {
430 shift @$args;
57e67ea3 431 &$print_bash_completion($name, @$args);
7b7f99c9
DM
432 return;
433 }
434 }
435
7fe1f565
DM
436 &$preparefunc() if $preparefunc;
437
408976c6 438 my $res = $class->cli_handler($name, $name, \@ARGV, $arg_param, $uri_param, $pwcallback, $stringfilemap);
2026f4b5
DM
439
440 &$outsub($res) if $outsub;
891b798d
DM
441};
442
891b798d
DM
443sub run_cli_handler {
444 my ($class, %params) = @_;
445
446 $cli_handler_class = $class;
447
448 $ENV{'PATH'} = '/sbin:/bin:/usr/sbin:/usr/bin';
449
aa9b9af5 450 foreach my $key (keys %params) {
5ae87c41 451 next if $key eq 'prepare';
1042b82c
DM
452 next if $key eq 'no_init'; # not used anymore
453 next if $key eq 'no_rpcenv'; # not used anymore
aa9b9af5
DM
454 die "unknown parameter '$key'";
455 }
456
5ae87c41 457 my $preparefunc = $params{prepare};
891b798d
DM
458
459 my $pwcallback = $class->can('read_password');
408976c6 460 my $stringfilemap = $class->can('string_param_file_mapping');
891b798d
DM
461
462 $exename = &$get_exe_name($class);
463
464 initlog($exename);
465
891b798d 466 no strict 'refs';
57e67ea3 467 $cmddef = ${"${class}::cmddef"};
891b798d 468
57e67ea3
TL
469 if (ref($cmddef) eq 'ARRAY') {
470 &$handle_simple_cmd(\@ARGV, $pwcallback, $preparefunc, $stringfilemap);
891b798d 471 } else {
57e67ea3 472 &$handle_cmd(\@ARGV, $pwcallback, $preparefunc, $stringfilemap);
891b798d
DM
473 }
474
475 exit 0;
e143e9d8
DM
476}
477
4781;