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