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 _ = tslib_1.__importStar(require("lodash"));
|
8 | const util_1 = require("util");
|
9 | const sw = require('string-width');
|
10 | const { orderBy } = require('natural-orderby');
|
11 | class Table {
|
12 | constructor(data, columns, options = {}) {
|
13 | this.data = data;
|
14 |
|
15 | this.columns = Object.keys(columns).map((key) => {
|
16 | const col = columns[key];
|
17 | const extended = col.extended || false;
|
18 | const get = col.get || ((row) => row[key]);
|
19 | const header = typeof col.header === 'string' ? col.header : _.capitalize(key.replace(/\_/g, ' '));
|
20 | const minWidth = Math.max(col.minWidth || 0, sw(header) + 1);
|
21 | return {
|
22 | extended,
|
23 | get,
|
24 | header,
|
25 | key,
|
26 | minWidth,
|
27 | };
|
28 | });
|
29 | // and filter columns
|
30 | if (options.columns) {
|
31 | let filters = options.columns.split(',');
|
32 | this.columns = this.filterColumnsFromHeaders(filters);
|
33 | }
|
34 | else if (!options.extended) {
|
35 |
|
36 | this.columns = this.columns.filter(c => !c.extended);
|
37 | }
|
38 |
|
39 | const { columns: cols, filter, csv, extended, sort, printLine } = options;
|
40 | this.options = {
|
41 | columns: cols,
|
42 | csv,
|
43 | extended,
|
44 | filter,
|
45 | 'no-header': options['no-header'] || false,
|
46 | 'no-truncate': options['no-truncate'] || false,
|
47 | printLine: printLine || ((s) => process.stdout.write(s + '\n')),
|
48 | sort,
|
49 | };
|
50 | }
|
51 | display() {
|
52 |
|
53 | let rows = this.data.map(d => {
|
54 | let row = {};
|
55 | for (let col of this.columns) {
|
56 | let val = col.get(d);
|
57 | if (typeof val !== 'string')
|
58 | val = util_1.inspect(val, { breakLength: Infinity });
|
59 | row[col.key] = val;
|
60 | }
|
61 | return row;
|
62 | });
|
63 |
|
64 | if (this.options.filter) {
|
65 | let [header, regex] = this.options.filter.split('=');
|
66 | const isNot = header[0] === '-';
|
67 | if (isNot)
|
68 | header = header.substr(1);
|
69 | let col = this.findColumnFromHeader(header);
|
70 | if (!col || !regex)
|
71 | throw new Error('Filter flag has an invalid value');
|
72 | rows = rows.filter((d) => {
|
73 | let re = new RegExp(regex);
|
74 | let val = d[col.key];
|
75 | let match = val.match(re);
|
76 | return isNot ? !match : match;
|
77 | });
|
78 | }
|
79 |
|
80 | if (this.options.sort) {
|
81 | let sorters = this.options.sort.split(',');
|
82 | let sortHeaders = sorters.map(k => k[0] === '-' ? k.substr(1) : k);
|
83 | let sortKeys = this.filterColumnsFromHeaders(sortHeaders).map(c => {
|
84 | return ((v) => v[c.key]);
|
85 | });
|
86 | let sortKeysOrder = sorters.map(k => k[0] === '-' ? 'desc' : 'asc');
|
87 | rows = orderBy(rows, sortKeys, sortKeysOrder);
|
88 | }
|
89 | this.data = rows;
|
90 | if (this.options.csv)
|
91 | this.outputCSV();
|
92 | else
|
93 | this.outputTable();
|
94 | }
|
95 | findColumnFromHeader(header) {
|
96 | return this.columns.find(c => c.header.toLowerCase() === header.toLowerCase());
|
97 | }
|
98 | filterColumnsFromHeaders(filters) {
|
99 |
|
100 | filters = [...(new Set(filters))];
|
101 | let cols = [];
|
102 | filters.forEach(f => {
|
103 | let c = this.columns.find(c => c.header.toLowerCase() === f.toLowerCase());
|
104 | if (c)
|
105 | cols.push(c);
|
106 | });
|
107 | return cols;
|
108 | }
|
109 | outputCSV() {
|
110 |
|
111 | const { data, columns, options } = this;
|
112 | options.printLine(columns.map(c => c.header).join(','));
|
113 | data.forEach((d) => {
|
114 | let row = [];
|
115 | columns.forEach(col => row.push(d[col.key] || ''));
|
116 | options.printLine(row.join(','));
|
117 | });
|
118 | }
|
119 | outputTable() {
|
120 |
|
121 | const { data, columns, options } = this;
|
122 |
|
123 |
|
124 |
|
125 | for (let col of columns) {
|
126 |
|
127 |
|
128 | let widthData = data.map((row) => {
|
129 | let d = row[col.key];
|
130 | let manyLines = d.split('\n');
|
131 | if (manyLines.length > 1) {
|
132 | return '*'.repeat(Math.max(...manyLines.map((r) => sw(r))));
|
133 | }
|
134 | return d;
|
135 | });
|
136 | const widths = ['.'.padEnd(col.minWidth - 1), col.header, ...widthData.map((row) => row)].map(r => sw(r));
|
137 | col.maxWidth = Math.max(...widths) + 1;
|
138 | col.width = col.maxWidth;
|
139 | }
|
140 |
|
141 | const maxWidth = screen_1.stdtermwidth;
|
142 |
|
143 | const shouldShorten = () => {
|
144 |
|
145 | if (options['no-truncate'] || !process.stdout.isTTY)
|
146 | return;
|
147 |
|
148 | let dataMaxWidth = _.sumBy(columns, c => c.width);
|
149 | let overWidth = dataMaxWidth - maxWidth;
|
150 | if (overWidth <= 0)
|
151 | return;
|
152 |
|
153 | for (let col of columns) {
|
154 | col.width = col.minWidth;
|
155 | }
|
156 |
|
157 |
|
158 |
|
159 | let dataMinWidth = _.sumBy(columns, c => c.minWidth);
|
160 | if (dataMinWidth >= maxWidth)
|
161 | return;
|
162 |
|
163 | let wiggleRoom = maxWidth - dataMinWidth;
|
164 | let needyCols = _.sortBy(columns.map(c => ({ key: c.key, needs: c.maxWidth - c.width })), c => c.needs);
|
165 | for (let { key, needs } of needyCols) {
|
166 | if (!needs)
|
167 | continue;
|
168 | let col = _.find(columns, c => (key === c.key));
|
169 | if (!col)
|
170 | continue;
|
171 | if (wiggleRoom > needs) {
|
172 | col.width = col.width + needs;
|
173 | wiggleRoom = wiggleRoom - needs;
|
174 | }
|
175 | else if (wiggleRoom) {
|
176 | col.width = col.width + wiggleRoom;
|
177 | wiggleRoom = 0;
|
178 | }
|
179 | }
|
180 | };
|
181 | shouldShorten();
|
182 |
|
183 | if (!options['no-header']) {
|
184 | let headers = '';
|
185 | for (let col of columns) {
|
186 | let header = col.header;
|
187 | headers += header.padEnd(col.width);
|
188 | }
|
189 | options.printLine(chalk_1.default.bold(headers));
|
190 | }
|
191 |
|
192 | for (let row of data) {
|
193 |
|
194 |
|
195 |
|
196 | let numOfLines = 1;
|
197 | for (let col of columns) {
|
198 | const d = row[col.key];
|
199 | let lines = d.split('\n').length;
|
200 | if (lines > numOfLines)
|
201 | numOfLines = lines;
|
202 | }
|
203 | let linesIndexess = [...Array(numOfLines).keys()];
|
204 |
|
205 |
|
206 | linesIndexess.forEach((i) => {
|
207 | let l = '';
|
208 | for (let col of columns) {
|
209 | const width = col.width;
|
210 | let d = row[col.key];
|
211 | d = d.split('\n')[i] || '';
|
212 | const visualWidth = sw(d);
|
213 | const colorWidth = (d.length - visualWidth);
|
214 | let cell = d.padEnd(width + colorWidth);
|
215 | if ((cell.length - colorWidth) > width || visualWidth === width) {
|
216 | cell = cell.slice(0, width - 2) + '… ';
|
217 | }
|
218 | l += cell;
|
219 | }
|
220 | options.printLine(l);
|
221 | });
|
222 | }
|
223 | }
|
224 | }
|
225 | function table(data, columns, options = {}) {
|
226 | new Table(data, columns, options).display();
|
227 | }
|
228 | exports.table = table;
|
229 | (function (table) {
|
230 | table.Flags = {
|
231 | columns: command_1.flags.string({ exclusive: ['extended'], description: 'only show provided columns (comma-separated)' }),
|
232 | sort: command_1.flags.string({ description: 'property to sort by (prepend \'-\' for descending)' }),
|
233 | filter: command_1.flags.string({ description: 'filter property by partial string matching, ex: name=foo' }),
|
234 | csv: command_1.flags.boolean({ exclusive: ['no-truncate', 'no-header'], description: 'output is csv format' }),
|
235 | extended: command_1.flags.boolean({ exclusive: ['columns'], char: 'x', description: 'show extra columns' }),
|
236 | 'no-truncate': command_1.flags.boolean({ exclusive: ['csv'], description: 'do not truncate output to fit screen' }),
|
237 | 'no-header': command_1.flags.boolean({ exclusive: ['csv'], description: 'hide table header from output' }),
|
238 | };
|
239 | function flags(opts) {
|
240 | if (opts) {
|
241 | let f = {};
|
242 | let o = (opts.only && typeof opts.only === 'string' ? [opts.only] : opts.only) || Object.keys(table.Flags);
|
243 | let e = (opts.except && typeof opts.except === 'string' ? [opts.except] : opts.except) || [];
|
244 | o.forEach((key) => {
|
245 | if (e.includes(key))
|
246 | return;
|
247 | f[key] = table.Flags[key];
|
248 | });
|
249 | return f;
|
250 | }
|
251 | return table.Flags;
|
252 | }
|
253 | table.flags = flags;
|
254 | })(table = exports.table || (exports.table = {}));
|