+ 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);
+};
+
+sub data_to_text {
+ my ($data) = @_;
+
+ return '' if !defined($data);
+
+ if (my $class = ref($data)) {
+ return to_json($data, { utf8 => 1, canonical => 1 });
+ } else {
+ return "$data";
+ }
+}
+
+# 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)
+# $sort_key is either a column name, or integer 1 which simply
+# sorts the output according to the leftmost column not containing
+# any undef. if not specified, we do not change order.
+sub print_text_table {
+ my ($formatopts, $data, $sort_key) = @_;
+
+ my ($formatstring, @keys, @titles, %cutoffs, %defaults);
+ my $last_col = $formatopts->[$#{$formatopts}];
+
+ my $autosort = 0;
+ if (defined($sort_key) && ($sort_key eq 1)) {
+ $autosort = 1;
+ $sort_key = undef;
+ }
+
+ foreach my $col ( @$formatopts ) {
+ my ($key, $title, $cutoff) = @$col{qw(key title cutoff)};
+ $title //= $key;
+
+ push @keys, $key;
+ push @titles, $title;
+ $defaults{$key} = $col->{default} // '';
+
+ # calculate maximal print width and cutoff
+ my $titlelen = length($title);
+
+ my $longest = $titlelen;
+ my $sortable = $autosort;
+ foreach my $entry (@$data) {
+ my $len = length(data_to_text($entry->{$key})) // 0;
+ $longest = $len if $len > $longest;
+ $sortable = 0 if !defined($entry->{$key});
+ }
+
+ $sort_key //= $key if $sortable;
+ $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;
+
+ if (defined($sort_key)){
+ @$data = sort { $a->{$sort_key} cmp $b->{$sort_key} } @$data;
+ }
+ foreach my $entry (@$data) {
+ printf $formatstring, map { substr((data_to_text($entry->{$_}) // $defaults{$_}), 0 , $cutoffs{$_}) } @keys;
+ }
+}
+
+# prints the result of an API GET call returning an array as a table.
+# takes formatting information from the results property of the call
+# if $props_to_print is provided, prints only those columns. otherwise
+# takes all fields of the results property, with a fallback
+# to all fields occuring in items of $data.
+sub print_api_list {
+ my ($data, $result_schema, $props_to_print, $sort_key) = @_;
+
+ die "can only print object lists\n"
+ if !($result_schema->{type} eq 'array' && $result_schema->{items}->{type} eq 'object');
+
+ my $returnprops = $result_schema->{items}->{properties};
+
+ if (!defined($props_to_print)) {
+ $props_to_print = [ sort keys %$returnprops ];
+ if (!scalar(@$props_to_print)) {
+ my $all_props = {};
+ foreach my $obj (@{$data}) {
+ foreach my $key (keys %{$obj}) {
+ $all_props->{ $key } = 1;
+ }
+ }
+ $props_to_print = [ sort keys %{$all_props} ];
+ }
+ die "unable to detect list properties\n" if !scalar(@$props_to_print);