From 4dcd50052e1653e92d17c0f84c10319b0cafa315 Mon Sep 17 00:00:00 2001 From: Ivan Towlson Date: Fri, 29 Jan 2021 12:48:22 +1300 Subject: [PATCH] Cope with spaces in column header and body --- ts/src/table.ts | 44 ++++++++++++++++++++++++++---- ts/test/table.ts | 71 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+), 6 deletions(-) diff --git a/ts/src/table.ts b/ts/src/table.ts index 8648b11..d31b39d 100644 --- a/ts/src/table.ts +++ b/ts/src/table.ts @@ -58,19 +58,51 @@ export function asTableLines(output: KubectlOutput): Errorable { return { succeeded: false, reason: 'kubectl-error', error: output.stderr }; } +interface TableColumn { + readonly name: string; + readonly startIndex: number; + readonly endIndex?: number; +} + function parseTableLines(table: TableLines, columnSeparator: RegExp): Dictionary[] { if (table.header.length === 0 || table.body.length === 0) { return []; } - const columnHeaders = table.header.toLowerCase().replace(columnSeparator, '|').split('|'); - return table.body.map((line) => parseLine(line, columnHeaders, columnSeparator)); + const columns = parseColumns(table.header, columnSeparator); + return table.body.map((line) => parseLine(line, columns)); } -function parseLine(line: string, columnHeaders: string[], columnSeparator: RegExp) { +function parseLine(line: string, columns: TableColumn[]) { const lineInfoObject = Dictionary.of(); - const bits = line.replace(columnSeparator, '|').split('|'); - bits.forEach((columnValue, index) => { - lineInfoObject[columnHeaders[index].trim()] = columnValue.trim(); + columns.forEach((column) => { + const text = line.substring(column.startIndex, column.endIndex).trim(); + lineInfoObject[column.name] = text; }); return lineInfoObject; } + +function parseColumns(columnHeaders: string, columnSeparator: RegExp): TableColumn[] { + const columnStarts = parseColumnStarts(columnHeaders, columnSeparator); + const columns = Array.of(); + columnStarts.forEach((column, index) => { + const endIndex = (index < columnStarts.length - 1) ? + columnStarts[index + 1].startIndex - 1 : + undefined; + columns.push({ endIndex, ...column }); + }); + return columns; +} + +function parseColumnStarts(columnHeaders: string, columnSeparator: RegExp) { + const columns = Array.of(); + const columnNames = columnHeaders.replace(columnSeparator, '|').split('|'); + let takenTo = 0; + for (const columnName of columnNames) { + const startIndex = columnHeaders.indexOf(columnName, takenTo); + if (startIndex >= 0) { + takenTo = startIndex + columnName.length; + columns.push({ name: columnName.toLowerCase(), startIndex }); + } + } + return columns; +} diff --git a/ts/test/table.ts b/ts/test/table.ts index 1d7a7bc..d65653c 100644 --- a/ts/test/table.ts +++ b/ts/test/table.ts @@ -11,6 +11,27 @@ foo true false barbar false twice `.trim(); +const KUBECTL_SAMPLE_GET_WIDE_RESULT = +` +NAMESPACE NAME FOO BAR RELEASE STATUS SPLINE LEVEL +ns1 foo true false green reticulated +ns2 barbar false twice dark orange none +`.trim(); + +const KUBECTL_SAMPLE_MULTISPACE_RESULT = +` +NAMESPACE NAME FOO BAR RELEASE STATUS MOTTO +ns1 foo true false green let the games begin +ns2 barbar false twice dark orange none +`.trim(); + +const KUBECTL_SAMPLE_SKIPPED_COLUMN_RESULT = +` +NAMESPACE NAME RELEASE STATUS MOTTO +ns1 foo green hello +ns2 barbar dark orange +`.trim(); + describe('asTableLines', () => { it('should report failure if kubectl failed to run', () => { const result = parser.asTableLines(undefined); @@ -84,4 +105,54 @@ describe('parseTabular', () => { assert.equal(objects[1].foo, 'false'); assert.equal(objects[1].bar, 'twice'); }); + it('should parse headers with spaces correctly', () => { + const result = parser.parseTabular({ code: 0, stdout: KUBECTL_SAMPLE_GET_WIDE_RESULT, stderr: '' }); + assert.equal(true, result.succeeded); + const objects = ([]>>result).result; + assert.equal(objects.length, 2); + assert.equal(objects[0].namespace, 'ns1'); + assert.equal(objects[0].name, 'foo'); + assert.equal(objects[0].foo, 'true'); + assert.equal(objects[0].bar, 'false'); + assert.equal(objects[0]['release status'], 'green'); + assert.equal(objects[0]['spline level'], 'reticulated'); + assert.equal(objects[1].namespace, 'ns2'); + assert.equal(objects[1].name, 'barbar'); + assert.equal(objects[1].foo, 'false'); + assert.equal(objects[1].bar, 'twice'); + assert.equal(objects[1]['release status'], 'dark orange'); + assert.equal(objects[1]['spline level'], 'none'); + }); + it('should parse lines with multiple spaces correctly', () => { + const result = parser.parseTabular({ code: 0, stdout: KUBECTL_SAMPLE_MULTISPACE_RESULT, stderr: '' }); + assert.equal(true, result.succeeded); + const objects = ([]>>result).result; + assert.equal(objects.length, 2); + assert.equal(objects[0].namespace, 'ns1'); + assert.equal(objects[0].name, 'foo'); + assert.equal(objects[0].foo, 'true'); + assert.equal(objects[0].bar, 'false'); + assert.equal(objects[0]['release status'], 'green'); + assert.equal(objects[0]['motto'], 'let the games begin'); + assert.equal(objects[1].namespace, 'ns2'); + assert.equal(objects[1].name, 'barbar'); + assert.equal(objects[1].foo, 'false'); + assert.equal(objects[1].bar, 'twice'); + assert.equal(objects[1]['release status'], 'dark orange'); + assert.equal(objects[1]['motto'], 'none'); + }); + it('should parse tables with empty cells', () => { + const result = parser.parseTabular({ code: 0, stdout: KUBECTL_SAMPLE_SKIPPED_COLUMN_RESULT, stderr: '' }); + assert.equal(true, result.succeeded); + const objects = ([]>>result).result; + assert.equal(objects.length, 2); + assert.equal(objects[0].namespace, 'ns1'); + assert.equal(objects[0].name, 'foo'); + assert.equal(objects[0]['release status'], 'green'); + assert.equal(objects[0].motto, 'hello'); + assert.equal(objects[1].namespace, 'ns2'); + assert.equal(objects[1].name, 'barbar'); + assert.equal(objects[1]['release status'], 'dark orange'); + assert.equal(objects[1].motto, ''); + }); });