UNPKG

12.3 kBJavaScriptView Raw
1"use strict";
2Object.defineProperty(exports, "__esModule", { value: true });
3const tslib_1 = require("tslib");
4const command_1 = require("@oclif/command");
5const screen_1 = require("@oclif/screen");
6const chalk_1 = tslib_1.__importDefault(require("chalk"));
7const capitalize_1 = tslib_1.__importDefault(require("lodash/capitalize"));
8const sumBy_1 = tslib_1.__importDefault(require("lodash/sumBy"));
9const js_yaml_1 = require("js-yaml");
10const util_1 = require("util");
11const sw = require('string-width');
12const { orderBy } = require('natural-orderby');
13class Table {
14 constructor(data, columns, options = {}) {
15 this.data = data;
16 // assign columns
17 this.columns = Object.keys(columns).map((key) => {
18 const col = columns[key];
19 const extended = col.extended || false;
20 const get = col.get || ((row) => row[key]);
21 const header = typeof col.header === 'string' ? col.header : capitalize_1.default(key.replace(/_/g, ' '));
22 const minWidth = Math.max(col.minWidth || 0, sw(header) + 1);
23 return {
24 extended,
25 get,
26 header,
27 key,
28 minWidth,
29 };
30 });
31 // assign options
32 const { columns: cols, filter, csv, output, extended, sort, printLine } = options;
33 this.options = {
34 columns: cols,
35 output: csv ? 'csv' : output,
36 extended,
37 filter,
38 'no-header': options['no-header'] || false,
39 'no-truncate': options['no-truncate'] || false,
40 printLine: printLine || ((s) => process.stdout.write(s + '\n')),
41 sort,
42 };
43 }
44 display() {
45 // build table rows from input array data
46 let rows = this.data.map(d => {
47 const row = {};
48 for (const col of this.columns) {
49 let val = col.get(d);
50 if (typeof val !== 'string')
51 val = util_1.inspect(val, { breakLength: Infinity });
52 row[col.key] = val;
53 }
54 return row;
55 });
56 // filter rows
57 if (this.options.filter) {
58 /* eslint-disable-next-line prefer-const */
59 let [header, regex] = this.options.filter.split('=');
60 const isNot = header[0] === '-';
61 if (isNot)
62 header = header.substr(1);
63 const col = this.findColumnFromHeader(header);
64 if (!col || !regex)
65 throw new Error('Filter flag has an invalid value');
66 rows = rows.filter((d) => {
67 const re = new RegExp(regex);
68 const val = d[col.key];
69 const match = val.match(re);
70 return isNot ? !match : match;
71 });
72 }
73 // sort rows
74 if (this.options.sort) {
75 const sorters = this.options.sort.split(',');
76 const sortHeaders = sorters.map(k => k[0] === '-' ? k.substr(1) : k);
77 const sortKeys = this.filterColumnsFromHeaders(sortHeaders).map(c => {
78 return ((v) => v[c.key]);
79 });
80 const sortKeysOrder = sorters.map(k => k[0] === '-' ? 'desc' : 'asc');
81 rows = orderBy(rows, sortKeys, sortKeysOrder);
82 }
83 // and filter columns
84 if (this.options.columns) {
85 const filters = this.options.columns.split(',');
86 this.columns = this.filterColumnsFromHeaders(filters);
87 }
88 else if (!this.options.extended) {
89 // show extented columns/properties
90 this.columns = this.columns.filter(c => !c.extended);
91 }
92 this.data = rows;
93 switch (this.options.output) {
94 case 'csv':
95 this.outputCSV();
96 break;
97 case 'json':
98 this.outputJSON();
99 break;
100 case 'yaml':
101 this.outputYAML();
102 break;
103 default:
104 this.outputTable();
105 }
106 }
107 findColumnFromHeader(header) {
108 return this.columns.find(c => c.header.toLowerCase() === header.toLowerCase());
109 }
110 filterColumnsFromHeaders(filters) {
111 // unique
112 filters = [...(new Set(filters))];
113 const cols = [];
114 filters.forEach(f => {
115 const c = this.columns.find(c => c.header.toLowerCase() === f.toLowerCase());
116 if (c)
117 cols.push(c);
118 });
119 return cols;
120 }
121 getCSVRow(d) {
122 const values = this.columns.map(col => d[col.key] || '');
123 const needToBeEscapedForCsv = (e) => {
124 // CSV entries containing line breaks, comma or double quotes
125 // as specified in https://tools.ietf.org/html/rfc4180#section-2
126 return e.includes('"') || e.includes('\n') || e.includes('\r\n') || e.includes('\r') || e.includes(',');
127 };
128 const lineToBeEscaped = values.find(needToBeEscapedForCsv);
129 return values.map(e => lineToBeEscaped ? `"${e.replace('"', '""')}"` : e);
130 }
131 resolveColumnsToObjectArray() {
132 // tslint:disable-next-line:no-this-assignment
133 const { data, columns } = this;
134 return data.map((d) => {
135 return columns.reduce((obj, col) => {
136 return Object.assign(Object.assign({}, obj), { [col.key]: d[col.key] || '' });
137 }, {});
138 });
139 }
140 outputJSON() {
141 this.options.printLine(JSON.stringify(this.resolveColumnsToObjectArray(), undefined, 2));
142 }
143 outputYAML() {
144 this.options.printLine(js_yaml_1.safeDump(this.resolveColumnsToObjectArray()));
145 }
146 outputCSV() {
147 // tslint:disable-next-line:no-this-assignment
148 const { data, columns, options } = this;
149 if (!options['no-header']) {
150 options.printLine(columns.map(c => c.header).join(','));
151 }
152 data.forEach((d) => {
153 const row = this.getCSVRow(d);
154 options.printLine(row.join(','));
155 });
156 }
157 outputTable() {
158 // tslint:disable-next-line:no-this-assignment
159 const { data, columns, options } = this;
160 // column truncation
161 //
162 // find max width for each column
163 for (const col of columns) {
164 // convert multi-line cell to single longest line
165 // for width calculations
166 const widthData = data.map((row) => {
167 const d = row[col.key];
168 const manyLines = d.split('\n');
169 if (manyLines.length > 1) {
170 return '*'.repeat(Math.max(...manyLines.map((r) => sw(r))));
171 }
172 return d;
173 });
174 const widths = ['.'.padEnd(col.minWidth - 1), col.header, ...widthData.map((row) => row)].map(r => sw(r));
175 col.maxWidth = Math.max(...widths) + 1;
176 col.width = col.maxWidth;
177 }
178 // terminal width
179 const maxWidth = screen_1.stdtermwidth;
180 // truncation logic
181 const shouldShorten = () => {
182 // don't shorten if full mode
183 if (options['no-truncate'] || (!process.stdout.isTTY && !process.env.CLI_UX_SKIP_TTY_CHECK))
184 return;
185 // don't shorten if there is enough screen width
186 const dataMaxWidth = sumBy_1.default(columns, c => c.width);
187 const overWidth = dataMaxWidth - maxWidth;
188 if (overWidth <= 0)
189 return;
190 // not enough room, short all columns to minWidth
191 for (const col of columns) {
192 col.width = col.minWidth;
193 }
194 // if sum(minWidth's) is greater than term width
195 // nothing can be done so
196 // display all as minWidth
197 const dataMinWidth = sumBy_1.default(columns, c => c.minWidth);
198 if (dataMinWidth >= maxWidth)
199 return;
200 // some wiggle room left, add it back to "needy" columns
201 let wiggleRoom = maxWidth - dataMinWidth;
202 const needyCols = columns.map(c => ({ key: c.key, needs: c.maxWidth - c.width })).sort((a, b) => a.needs - b.needs);
203 for (const { key, needs } of needyCols) {
204 if (!needs)
205 continue;
206 const col = columns.find(c => key === c.key);
207 if (!col)
208 continue;
209 if (wiggleRoom > needs) {
210 col.width = col.width + needs;
211 wiggleRoom -= needs;
212 }
213 else if (wiggleRoom) {
214 col.width = col.width + wiggleRoom;
215 wiggleRoom = 0;
216 }
217 }
218 };
219 shouldShorten();
220 // print headers
221 if (!options['no-header']) {
222 let headers = '';
223 for (const col of columns) {
224 const header = col.header;
225 headers += header.padEnd(col.width);
226 }
227 options.printLine(chalk_1.default.bold(headers));
228 }
229 // print rows
230 for (const row of data) {
231 // find max number of lines
232 // for all cells in a row
233 // with multi-line strings
234 let numOfLines = 1;
235 for (const col of columns) {
236 const d = row[col.key];
237 const lines = d.split('\n').length;
238 if (lines > numOfLines)
239 numOfLines = lines;
240 }
241 const linesIndexess = [...new Array(numOfLines).keys()];
242 // print row
243 // including multi-lines
244 linesIndexess.forEach((i) => {
245 let l = '';
246 for (const col of columns) {
247 const width = col.width;
248 let d = row[col.key];
249 d = d.split('\n')[i] || '';
250 const visualWidth = sw(d);
251 const colorWidth = (d.length - visualWidth);
252 let cell = d.padEnd(width + colorWidth);
253 if ((cell.length - colorWidth) > width || visualWidth === width) {
254 cell = cell.slice(0, width - 2) + '… ';
255 }
256 l += cell;
257 }
258 options.printLine(l);
259 });
260 }
261 }
262}
263function table(data, columns, options = {}) {
264 new Table(data, columns, options).display();
265}
266exports.table = table;
267(function (table) {
268 table.Flags = {
269 columns: command_1.flags.string({ exclusive: ['extended'], description: 'only show provided columns (comma-separated)' }),
270 sort: command_1.flags.string({ description: 'property to sort by (prepend \'-\' for descending)' }),
271 filter: command_1.flags.string({ description: 'filter property by partial string matching, ex: name=foo' }),
272 csv: command_1.flags.boolean({ exclusive: ['no-truncate'], description: 'output is csv format [alias: --output=csv]' }),
273 output: command_1.flags.string({
274 exclusive: ['no-truncate', 'csv'],
275 description: 'output in a more machine friendly format',
276 options: ['csv', 'json', 'yaml'],
277 }),
278 extended: command_1.flags.boolean({ exclusive: ['columns'], char: 'x', description: 'show extra columns' }),
279 'no-truncate': command_1.flags.boolean({ exclusive: ['csv'], description: 'do not truncate output to fit screen' }),
280 'no-header': command_1.flags.boolean({ exclusive: ['csv'], description: 'hide table header from output' }),
281 };
282 // eslint-disable-next-line no-inner-declarations
283 function flags(opts) {
284 if (opts) {
285 const f = {};
286 const o = (opts.only && typeof opts.only === 'string' ? [opts.only] : opts.only) || Object.keys(table.Flags);
287 const e = (opts.except && typeof opts.except === 'string' ? [opts.except] : opts.except) || [];
288 o.forEach((key) => {
289 if (e.includes(key))
290 return;
291 f[key] = table.Flags[key];
292 });
293 return f;
294 }
295 return table.Flags;
296 }
297 table.flags = flags;
298})(table = exports.table || (exports.table = {}));