+ print {$fd} generate_usage_str('short', $cmd, ' ' x 7, "\n", sub {
+ my ($h) = @_;
+ return sort {
+ if (ref($h->{$a}) eq 'ARRAY' && ref($h->{$b}) eq 'ARRAY') {
+ # $a and $b are both real commands order them by their class
+ return $h->{$a}->[0] cmp $h->{$b}->[0] || $a cmp $b;
+ } elsif (ref($h->{$a}) eq 'ARRAY' xor ref($h->{$b}) eq 'ARRAY') {
+ # real command and subcommand mixed, put sub commands first
+ return ref($h->{$b}) eq 'ARRAY' ? -1 : 1;
+ } else {
+ # both are either from the same class or subcommands
+ return $a cmp $b;
+ }
+ } keys %$h;
+ });
+}
+
+my $print_bash_completion = sub {
+ my ($simple_cmd, $bash_command, $cur, $prev) = @_;
+
+ my $debug = 0;
+
+ return if !(defined($cur) && defined($prev) && defined($bash_command));
+ return if !defined($ENV{COMP_LINE});
+ return if !defined($ENV{COMP_POINT});
+
+ my $cmdline = substr($ENV{COMP_LINE}, 0, $ENV{COMP_POINT});
+ print STDERR "\nCMDLINE: $ENV{COMP_LINE}\n" if $debug;
+
+ my $args = PVE::Tools::split_args($cmdline);
+ shift @$args; # no need for program name
+ my $print_result = sub {
+ foreach my $p (@_) {
+ print "$p\n" if $p =~ m/^\Q$cur\E/;
+ }
+ };
+
+ my ($cmd, $def) = ($simple_cmd, $cmddef);
+ if (!$simple_cmd) {
+ ($cmd, $def, $args, my $expanded) = resolve_cmd($args);
+
+ if (defined($expanded) && $prev ne $expanded) {
+ print "$expanded\n";
+ return;
+ }
+
+ if (ref($def) eq 'HASH') {
+ &$print_result(@{$get_commands->($def)});
+ return;
+ }
+ }
+ return if !$def;
+
+ my $pos = scalar(@$args) - 1;
+ $pos += 1 if $cmdline =~ m/\s+$/;
+ print STDERR "pos: $pos\n" if $debug;
+ return if $pos < 0;
+
+ my $skip_param = {};
+
+ my ($class, $name, $arg_param, $uri_param) = @$def;
+ $arg_param //= [];
+ $uri_param //= {};
+
+ $arg_param = [ $arg_param ] if !ref($arg_param);
+
+ map { $skip_param->{$_} = 1; } @$arg_param;
+ map { $skip_param->{$_} = 1; } keys %$uri_param;
+
+ my $info = $class->map_method_by_name($name);
+
+ my $prop = $info->{parameters}->{properties};
+
+ my $print_parameter_completion = sub {
+ my ($pname) = @_;
+ my $d = $prop->{$pname};
+ if ($d->{completion}) {
+ my $vt = ref($d->{completion});
+ if ($vt eq 'CODE') {
+ my $res = $d->{completion}->($cmd, $pname, $cur, $args);
+ &$print_result(@$res);
+ }
+ } elsif ($d->{type} eq 'boolean') {
+ &$print_result('0', '1');
+ } elsif ($d->{enum}) {
+ &$print_result(@{$d->{enum}});
+ }
+ };
+
+ # positional arguments
+ if ($pos < scalar(@$arg_param)) {
+ my $pname = $arg_param->[$pos];
+ &$print_parameter_completion($pname);
+ return;
+ }
+
+ my @option_list = ();
+ foreach my $key (keys %$prop) {
+ next if $skip_param->{$key};
+ push @option_list, "--$key";
+ }
+
+ if ($cur =~ m/^-/) {
+ &$print_result(@option_list);
+ return;
+ }
+
+ if ($prev =~ m/^--?(.+)$/ && $prop->{$1}) {
+ my $pname = $1;
+ &$print_parameter_completion($pname);
+ return;
+ }
+
+ &$print_result(@option_list);
+};
+
+# prints a formatted table with a title row.
+# $formatopts is an array of hashes, with the following keys:
+# 'key' - key of $data element to print
+# 'title' - column title, defaults to 'key' - won't get cutoff
+# 'cutoff' - maximal (print) length of this column values, if set
+# the last column will never be cutoff
+# 'default' - optional default value for the column
+# formatopts element order defines column order (left to right)
+sub print_text_table {
+ my ($formatopts, $data) = @_;
+
+ my ($formatstring, @keys, @titles, %cutoffs, %defaults);
+ my $last_col = $formatopts->[$#{$formatopts}];
+
+ foreach my $col ( @$formatopts ) {
+ my ($key, $title, $cutoff, $default) = @$col{qw(key title cutoff default)};
+ $title //= $key;
+
+ push @keys, $key;
+ push @titles, $title;
+ $defaults{$key} = $default;
+
+ # calculate maximal print width and cutoff
+ my $titlelen = length($title);
+
+ my $longest = $titlelen;
+ foreach my $entry (@$data) {
+ my $len = length($entry->{$key}) // 0;
+ $longest = $len if $len > $longest;
+ }
+
+ $cutoff = (defined($cutoff) && $cutoff < $longest) ? $cutoff : $longest;
+ $cutoffs{$key} = $cutoff;
+
+ my $printalign = $cutoff > $titlelen ? '-' : '';
+ if ($col == $last_col) {
+ $formatstring .= "%${printalign}${titlelen}s\n";
+ } else {
+ $formatstring .= "%${printalign}${cutoff}s ";
+ }
+ }
+
+ printf $formatstring, @titles;
+
+ foreach my $entry (sort { $a->{$keys[0]} cmp $b->{$keys[0]} } @$data) {
+ printf $formatstring, map { substr(($entry->{$_} // $defaults{$_}), 0 , $cutoffs{$_}) } @keys;