1 | "use strict";
|
2 | Object.defineProperty(exports, "__esModule", { value: true });
|
3 | const tslib_1 = require("tslib");
|
4 | const command_1 = require("@oclif/command");
|
5 | const screen_1 = require("@oclif/screen");
|
6 | const chalk_1 = tslib_1.__importDefault(require("chalk"));
|
7 | const capitalize_1 = tslib_1.__importDefault(require("lodash/capitalize"));
|
8 | const sumBy_1 = tslib_1.__importDefault(require("lodash/sumBy"));
|
9 | const js_yaml_1 = require("js-yaml");
|
10 | const util_1 = require("util");
|
11 | const sw = require('string-width');
|
12 | const { orderBy } = require('natural-orderby');
|
13 | class Table {
|
14 | constructor(data, columns, options = {}) {
|
15 | this.data = data;
|
16 |
|
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 |
|
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 |
|
57 | if (this.options.filter) {
|
58 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
125 |
|
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 |
|
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 |
|
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 |
|
159 | const { data, columns, options } = this;
|
160 |
|
161 |
|
162 |
|
163 | for (const col of columns) {
|
164 |
|
165 |
|
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 |
|
179 | const maxWidth = screen_1.stdtermwidth;
|
180 |
|
181 | const shouldShorten = () => {
|
182 |
|
183 | if (options['no-truncate'] || (!process.stdout.isTTY && !process.env.CLI_UX_SKIP_TTY_CHECK))
|
184 | return;
|
185 |
|
186 | const dataMaxWidth = sumBy_1.default(columns, c => c.width);
|
187 | const overWidth = dataMaxWidth - maxWidth;
|
188 | if (overWidth <= 0)
|
189 | return;
|
190 |
|
191 | for (const col of columns) {
|
192 | col.width = col.minWidth;
|
193 | }
|
194 |
|
195 |
|
196 |
|
197 | const dataMinWidth = sumBy_1.default(columns, c => c.minWidth);
|
198 | if (dataMinWidth >= maxWidth)
|
199 | return;
|
200 |
|
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 |
|
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 |
|
230 | for (const row of data) {
|
231 |
|
232 |
|
233 |
|
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 |
|
243 |
|
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 | }
|
263 | function table(data, columns, options = {}) {
|
264 | new Table(data, columns, options).display();
|
265 | }
|
266 | exports.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 |
|
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 = {}));
|