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