]>
Commit | Line | Data |
---|---|---|
f53ad23a DM |
1 | package PVE::CLIFormatter; |
2 | ||
3 | use strict; | |
4 | use warnings; | |
84142f1d | 5 | use PVE::JSONSchema; |
f53ad23a DM |
6 | use JSON; |
7 | ||
cde31da0 DM |
8 | sub println_max { |
9 | my ($text, $max) = @_; | |
10 | ||
11 | if ($max) { | |
12 | my @lines = split(/\n/, $text); | |
13 | foreach my $line (@lines) { | |
14 | print substr($line, 0, $max) . "\n"; | |
15 | } | |
16 | } else { | |
17 | print $text; | |
18 | } | |
19 | } | |
20 | ||
f53ad23a | 21 | sub data_to_text { |
84142f1d | 22 | my ($data, $propdef) = @_; |
f53ad23a | 23 | |
84142f1d DM |
24 | if (defined($propdef)) { |
25 | if (my $type = $propdef->{type}) { | |
26 | if ($type eq 'boolean') { | |
27 | return $data ? 1 : 0; | |
28 | } | |
29 | } | |
30 | if (!defined($data) && defined($propdef->{default})) { | |
31 | return "($propdef->{default})"; | |
32 | } | |
33 | if (defined(my $renderer = $propdef->{renderer})) { | |
34 | my $code = PVE::JSONSchema::get_renderer($renderer); | |
35 | die "internal error: unknown renderer '$renderer'" if !$code; | |
36 | return $code->($data); | |
37 | } | |
38 | } | |
f53ad23a DM |
39 | return '' if !defined($data); |
40 | ||
41 | if (my $class = ref($data)) { | |
84142f1d | 42 | return to_json($data, { canonical => 1 }); |
f53ad23a DM |
43 | } else { |
44 | return "$data"; | |
45 | } | |
46 | } | |
47 | ||
48 | # prints a formatted table with a title row. | |
49 | # $data - the data to print (array of objects) | |
50 | # $returnprops -json schema property description | |
51 | # $props_to_print - ordered list of properties to print | |
52 | # $sort_key can be used to sort after a column, if it isn't set we sort | |
53 | # after the leftmost column (with no undef value in $data) this can be | |
54 | # turned off by passing 0 as $sort_key | |
793ad69b | 55 | # $border - print with/without table header and asciiart border |
f53ad23a | 56 | sub print_text_table { |
cde31da0 | 57 | my ($data, $returnprops, $props_to_print, $sort_key, $border, $columns) = @_; |
f53ad23a DM |
58 | |
59 | my $autosort = 1; | |
60 | if (defined($sort_key) && $sort_key eq 0) { | |
61 | $autosort = 0; | |
62 | $sort_key = undef; | |
63 | } | |
64 | ||
65 | my $colopts = {}; | |
793ad69b DM |
66 | |
67 | my $borderstring = ''; | |
f53ad23a DM |
68 | my $formatstring = ''; |
69 | ||
70 | my $column_count = scalar(@$props_to_print); | |
71 | ||
72 | for (my $i = 0; $i < $column_count; $i++) { | |
73 | my $prop = $props_to_print->[$i]; | |
74 | my $propinfo = $returnprops->{$prop} // {}; | |
75 | ||
76 | my $title = $propinfo->{title} // $prop; | |
77 | my $cutoff = $propinfo->{print_width} // $propinfo->{maxLength}; | |
78 | ||
79 | # calculate maximal print width and cutoff | |
80 | my $titlelen = length($title); | |
81 | ||
82 | my $longest = $titlelen; | |
83 | my $sortable = $autosort; | |
84 | foreach my $entry (@$data) { | |
84142f1d | 85 | my $len = length(data_to_text($entry->{$prop}, $propinfo)) // 0; |
f53ad23a DM |
86 | $longest = $len if $len > $longest; |
87 | $sortable = 0 if !defined($entry->{$prop}); | |
88 | } | |
89 | $cutoff = $longest if !defined($cutoff) || $cutoff > $longest; | |
90 | $sort_key //= $prop if $sortable; | |
91 | ||
92 | $colopts->{$prop} = { | |
93 | title => $title, | |
94 | default => $propinfo->{default} // '', | |
95 | cutoff => $cutoff, | |
96 | }; | |
97 | ||
793ad69b DM |
98 | if ($border) { |
99 | if ($i == ($column_count - 1)) { | |
100 | $formatstring .= "| %-${cutoff}s |\n"; | |
101 | $borderstring .= "+-" . ('-' x $cutoff) . "-+\n"; | |
102 | } else { | |
103 | $formatstring .= "| %-${cutoff}s "; | |
104 | $borderstring .= "+-" . ('-' x $cutoff) . '-'; | |
105 | } | |
106 | } else { | |
107 | # skip alignment and cutoff on last column | |
108 | $formatstring .= ($i == ($column_count - 1)) ? "%s\n" : "%-${cutoff}s "; | |
109 | } | |
f53ad23a DM |
110 | } |
111 | ||
f53ad23a DM |
112 | if (defined($sort_key)) { |
113 | my $type = $returnprops->{$sort_key}->{type} // 'string'; | |
114 | if ($type eq 'integer' || $type eq 'number') { | |
115 | @$data = sort { $a->{$sort_key} <=> $b->{$sort_key} } @$data; | |
116 | } else { | |
117 | @$data = sort { $a->{$sort_key} cmp $b->{$sort_key} } @$data; | |
118 | } | |
119 | } | |
120 | ||
cde31da0 DM |
121 | println_max($borderstring, $columns) if $border; |
122 | my $text = sprintf $formatstring, map { $colopts->{$_}->{title} } @$props_to_print; | |
123 | println_max($text, $columns); | |
793ad69b | 124 | |
f53ad23a | 125 | foreach my $entry (@$data) { |
cde31da0 DM |
126 | println_max($borderstring, $columns) if $border; |
127 | $text = sprintf $formatstring, map { | |
84142f1d | 128 | substr(data_to_text($entry->{$_}, $returnprops->{$_}) // $colopts->{$_}->{default}, |
f53ad23a DM |
129 | 0, $colopts->{$_}->{cutoff}); |
130 | } @$props_to_print; | |
cde31da0 | 131 | println_max($text, $columns); |
f53ad23a | 132 | } |
cde31da0 | 133 | println_max($borderstring, $columns) if $border; |
f53ad23a DM |
134 | } |
135 | ||
136 | # prints the result of an API GET call returning an array as a table. | |
137 | # takes formatting information from the results property of the call | |
138 | # if $props_to_print is provided, prints only those columns. otherwise | |
139 | # takes all fields of the results property, with a fallback | |
140 | # to all fields occuring in items of $data. | |
141 | sub print_api_list { | |
cde31da0 | 142 | my ($data, $result_schema, $props_to_print, $sort_key, $border, $columns) = @_; |
f53ad23a DM |
143 | |
144 | die "can only print object lists\n" | |
145 | if !($result_schema->{type} eq 'array' && $result_schema->{items}->{type} eq 'object'); | |
146 | ||
147 | my $returnprops = $result_schema->{items}->{properties}; | |
148 | ||
149 | if (!defined($props_to_print)) { | |
150 | $props_to_print = [ sort keys %$returnprops ]; | |
151 | if (!scalar(@$props_to_print)) { | |
152 | my $all_props = {}; | |
153 | foreach my $obj (@{$data}) { | |
154 | foreach my $key (keys %{$obj}) { | |
155 | $all_props->{ $key } = 1; | |
156 | } | |
157 | } | |
158 | $props_to_print = [ sort keys %{$all_props} ]; | |
159 | } | |
160 | die "unable to detect list properties\n" if !scalar(@$props_to_print); | |
161 | } | |
162 | ||
cde31da0 | 163 | print_text_table($data, $returnprops, $props_to_print, $sort_key, $border, $columns); |
f53ad23a DM |
164 | } |
165 | ||
166 | sub print_api_result { | |
cde31da0 | 167 | my ($format, $data, $result_schema, $props_to_print, $sort_key, $columns) = @_; |
f53ad23a DM |
168 | |
169 | return if $result_schema->{type} eq 'null'; | |
170 | ||
171 | if ($format eq 'json') { | |
172 | print to_json($data, {utf8 => 1, allow_nonref => 1, canonical => 1, pretty => 1 }); | |
793ad69b | 173 | } elsif ($format eq 'text' || $format eq 'plain') { |
f53ad23a DM |
174 | my $type = $result_schema->{type}; |
175 | if ($type eq 'object') { | |
176 | $props_to_print = [ sort keys %$data ] if !defined($props_to_print); | |
2a174d2a | 177 | my $kvstore = []; |
f53ad23a | 178 | foreach my $key (@$props_to_print) { |
84142f1d | 179 | push @$kvstore, { key => $key, value => data_to_text($data->{$key}, $result_schema->{properties}->{$key}) }; |
f53ad23a | 180 | } |
2a174d2a | 181 | my $schema = { type => 'array', items => { type => 'object' }}; |
cde31da0 | 182 | print_api_list($kvstore, $schema, ['key', 'value'], 0, $format eq 'text', $columns); |
f53ad23a DM |
183 | } elsif ($type eq 'array') { |
184 | return if !scalar(@$data); | |
185 | my $item_type = $result_schema->{items}->{type}; | |
186 | if ($item_type eq 'object') { | |
cde31da0 | 187 | print_api_list($data, $result_schema, $props_to_print, $sort_key, $format eq 'text', $columns); |
f53ad23a DM |
188 | } else { |
189 | foreach my $entry (@$data) { | |
190 | print data_to_text($entry) . "\n"; | |
191 | } | |
192 | } | |
193 | } else { | |
194 | print "$data\n"; | |
195 | } | |
196 | } else { | |
197 | die "internal error: unknown output format"; # should not happen | |
198 | } | |
199 | } | |
200 | ||
201 | 1; |