93e2cad6585e2540a0713557e2ef40b3ddaafc21
[pve-common.git] / src / PVE / CLIFormatter.pm
1 package PVE::CLIFormatter;
2
3 use strict;
4 use warnings;
5 use PVE::JSONSchema;
6 use JSON;
7 use utf8;
8 use Encode;
9
10 sub println_max {
11     my ($text, $max) = @_;
12
13     if ($max) {
14         my @lines = split(/\n/, $text);
15         foreach my $line (@lines) {
16             print encode('UTF-8', substr($line, 0, $max) . "\n");
17         }
18     } else {
19         print encode('UTF-8', $text);
20     }
21 }
22
23 sub data_to_text {
24     my ($data, $propdef) = @_;
25
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     }
41     return '' if !defined($data);
42
43     if (my $class = ref($data)) {
44         return to_json($data, { canonical => 1 });
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
54 # $options
55 # - sort_key: can be used to sort after a column, if it isn't set we sort
56 #   after the leftmost column (with no undef value in $data) this can be
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
62 sub print_text_table {
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};
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 = {};
77
78     my $borderstring_m = '';
79     my $borderstring_b = '';
80     my $borderstring_t = '';
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) {
98             my $len = length(data_to_text($entry->{$prop}, $propinfo)) // 0;
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
111         if ($border) {
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                 }
142             } else {
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             }
153         } else {
154             # skip alignment and cutoff on last column
155             $formatstring .= ($i == ($column_count - 1)) ? "%s\n" : "%-${cutoff}s ";
156         }
157     }
158
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
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;
172     my $text = sprintf $formatstring, map { $colopts->{$_}->{title} } @$props_to_print;
173     println_max($text, $columns);
174
175     foreach my $entry (@$data) {
176         println_max($borderstring_m, $columns) if $border;
177         $text = sprintf $formatstring, map {
178             substr(data_to_text($entry->{$_}, $returnprops->{$_}) // $colopts->{$_}->{default},
179                    0, $colopts->{$_}->{cutoff});
180         } @$props_to_print;
181         println_max($text, $columns);
182     }
183     println_max($borderstring_b, $columns) if $border;
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 {
192     my ($data, $result_schema, $props_to_print, $options) = @_;
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
213     print_text_table($data, $returnprops, $props_to_print, $options);
214 }
215
216 sub print_api_result {
217     my ($format, $data, $result_schema, $props_to_print, $options) = @_;
218
219     $options //= {};
220     $options = { %$options }; # copy
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 });
226     } elsif ($format eq 'text' || $format eq 'plain') {
227         my $type = $result_schema->{type};
228         if ($type eq 'object') {
229             $props_to_print = [ sort keys %$data ] if !defined($props_to_print);
230             my $kvstore = [];
231             foreach my $key (@$props_to_print) {
232                 push @$kvstore, { key => $key, value => data_to_text($data->{$key}, $result_schema->{properties}->{$key}) };
233             }
234             my $schema = { type => 'array', items => { type => 'object' }};
235             $options->{border} = $format eq 'text';
236             print_api_list($kvstore, $schema, ['key', 'value'], $options);
237         } elsif ($type eq 'array') {
238             return if !scalar(@$data);
239             my $item_type = $result_schema->{items}->{type};
240             if ($item_type eq 'object') {
241                 $options->{border} = $format eq 'text';
242                 print_api_list($data, $result_schema, $props_to_print, $options);
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;