feat(table): add header spec support and explicit auto mode
This commit is contained in:
55
src/cli.js
55
src/cli.js
@@ -22,7 +22,7 @@ Commands:
|
|||||||
list-apps [--display-name|-n <name>]
|
list-apps [--display-name|-n <name>]
|
||||||
list-app-permissions --app-id|-i <appId> [--resolve|-r] [--short|-s]
|
list-app-permissions --app-id|-i <appId> [--resolve|-r] [--short|-s]
|
||||||
list-app-grants --app-id|-i <appId>
|
list-app-grants --app-id|-i <appId>
|
||||||
table [--pretty|-p] [--quote-guids|-g]
|
table [--pretty|-p] [--quote-guids|-g] [--header|-H <spec|auto|a>]
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
-n, --display-name <name> Filter apps by exact display name
|
-n, --display-name <name> Filter apps by exact display name
|
||||||
@@ -32,6 +32,7 @@ Options:
|
|||||||
-q, --query <jmespath> Filter output JSON using JMESPath
|
-q, --query <jmespath> Filter output JSON using JMESPath
|
||||||
-p, --pretty Use normalized column widths for Markdown table output
|
-p, --pretty Use normalized column widths for Markdown table output
|
||||||
-g, --quote-guids In pretty tables, wrap GUID values in backticks
|
-g, --quote-guids In pretty tables, wrap GUID values in backticks
|
||||||
|
-H, --header <value> Header mode/spec: auto|a OR "col1, col2" OR "key1: Label 1, key2: Label 2"
|
||||||
-o, --output <format> Output format: json|table|prettytable (default: json)
|
-o, --output <format> Output format: json|table|prettytable (default: json)
|
||||||
-h, --help Show this help message`;
|
-h, --help Show this help message`;
|
||||||
}
|
}
|
||||||
@@ -42,6 +43,40 @@ function outputFiltered(object, query) {
|
|||||||
: object;
|
: object;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseHeaderSpec(headerValue) {
|
||||||
|
if (!headerValue) {
|
||||||
|
return { mode: "default" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const raw = headerValue.trim();
|
||||||
|
if (raw === "" || raw.toLowerCase() === "auto" || raw.toLowerCase() === "a") {
|
||||||
|
return { mode: "auto" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts = raw.split(",").map((p) => p.trim()).filter(Boolean);
|
||||||
|
const isMap = parts.some((p) => p.includes(":"));
|
||||||
|
|
||||||
|
if (!isMap) {
|
||||||
|
return { mode: "list", labels: parts };
|
||||||
|
}
|
||||||
|
|
||||||
|
const map = {};
|
||||||
|
for (const part of parts) {
|
||||||
|
const idx = part.indexOf(":");
|
||||||
|
if (idx < 0) {
|
||||||
|
throw new Error(`Invalid --header mapping segment: '${part}'`);
|
||||||
|
}
|
||||||
|
const key = part.slice(0, idx).trim();
|
||||||
|
const label = part.slice(idx + 1).trim();
|
||||||
|
if (!key || !label) {
|
||||||
|
throw new Error(`Invalid --header mapping segment: '${part}'`);
|
||||||
|
}
|
||||||
|
map[key] = label;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { mode: "map", map };
|
||||||
|
}
|
||||||
|
|
||||||
function omitPermissionGuidColumns(value) {
|
function omitPermissionGuidColumns(value) {
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
return value.map((item) => omitPermissionGuidColumns(item));
|
return value.map((item) => omitPermissionGuidColumns(item));
|
||||||
@@ -97,6 +132,7 @@ async function main() {
|
|||||||
query: { type: "string", short: "q" },
|
query: { type: "string", short: "q" },
|
||||||
pretty: { type: "boolean", short: "p" },
|
pretty: { type: "boolean", short: "p" },
|
||||||
"quote-guids": { type: "boolean", short: "g" },
|
"quote-guids": { type: "boolean", short: "g" },
|
||||||
|
header: { type: "string", short: "H" },
|
||||||
output: { type: "string", short: "o" },
|
output: { type: "string", short: "o" },
|
||||||
},
|
},
|
||||||
strict: true,
|
strict: true,
|
||||||
@@ -165,12 +201,23 @@ async function main() {
|
|||||||
const output = command === "list-app-permissions" && values.short
|
const output = command === "list-app-permissions" && values.short
|
||||||
? omitPermissionGuidColumns(filtered)
|
? omitPermissionGuidColumns(filtered)
|
||||||
: filtered;
|
: filtered;
|
||||||
|
const headerSpec = parseHeaderSpec(values.header);
|
||||||
if (command === "table") {
|
if (command === "table") {
|
||||||
console.log(toMarkdownTable(output, Boolean(values.pretty), Boolean(values["quote-guids"])));
|
console.log(toMarkdownTable(
|
||||||
|
output,
|
||||||
|
Boolean(values.pretty),
|
||||||
|
Boolean(values["quote-guids"]),
|
||||||
|
headerSpec,
|
||||||
|
));
|
||||||
} else if (outputFormat === "prettytable") {
|
} else if (outputFormat === "prettytable") {
|
||||||
console.log(toMarkdownTable(output, true, Boolean(values["quote-guids"])));
|
console.log(toMarkdownTable(
|
||||||
|
output,
|
||||||
|
true,
|
||||||
|
Boolean(values["quote-guids"]),
|
||||||
|
headerSpec,
|
||||||
|
));
|
||||||
} else if (outputFormat === "table") {
|
} else if (outputFormat === "table") {
|
||||||
console.log(toMarkdownTable(output));
|
console.log(toMarkdownTable(output, false, false, headerSpec));
|
||||||
} else {
|
} else {
|
||||||
console.log(JSON.stringify(output, null, 2));
|
console.log(JSON.stringify(output, null, 2));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,19 @@ function isGuid(value) {
|
|||||||
&& /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
|
&& /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toAutoHeaderLabel(key) {
|
||||||
|
const withSpaces = String(key)
|
||||||
|
.replace(/[_-]+/g, " ")
|
||||||
|
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
||||||
|
.replace(/\s+/g, " ")
|
||||||
|
.trim();
|
||||||
|
return withSpaces
|
||||||
|
.split(" ")
|
||||||
|
.filter(Boolean)
|
||||||
|
.map((part) => part[0].toUpperCase() + part.slice(1))
|
||||||
|
.join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
function getScalarRowsAndHeaders(value) {
|
function getScalarRowsAndHeaders(value) {
|
||||||
let rows;
|
let rows;
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
@@ -52,35 +65,48 @@ function getScalarRowsAndHeaders(value) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function toMarkdownTable(value, pretty = false, quoteGuids = false) {
|
export function toMarkdownTable(value, pretty = false, quoteGuids = false) {
|
||||||
|
const headerSpec = arguments[3] ?? { mode: "default" };
|
||||||
const { headers, rows } = getScalarRowsAndHeaders(value);
|
const { headers, rows } = getScalarRowsAndHeaders(value);
|
||||||
|
const headerDefinitions = headers.map((key, idx) => {
|
||||||
|
let label = key;
|
||||||
|
if (headerSpec?.mode === "auto") {
|
||||||
|
label = toAutoHeaderLabel(key);
|
||||||
|
} else if (headerSpec?.mode === "list" && Array.isArray(headerSpec.labels) && headerSpec.labels[idx]) {
|
||||||
|
label = headerSpec.labels[idx];
|
||||||
|
} else if (headerSpec?.mode === "map" && headerSpec.map && headerSpec.map[key]) {
|
||||||
|
label = headerSpec.map[key];
|
||||||
|
}
|
||||||
|
return { key, label };
|
||||||
|
});
|
||||||
|
|
||||||
const renderCell = (raw) => {
|
const renderCell = (raw) => {
|
||||||
const text = formatCell(raw);
|
const text = formatCell(raw);
|
||||||
return quoteGuids && isGuid(raw) ? `\`${text}\`` : text;
|
return quoteGuids && isGuid(raw) ? `\`${text}\`` : text;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!pretty) {
|
if (!pretty) {
|
||||||
const headerLine = `| ${headers.join(" | ")} |`;
|
const headerLine = `| ${headerDefinitions.map((h) => h.label).join(" | ")} |`;
|
||||||
const separatorLine = `| ${headers.map(() => "---").join(" | ")} |`;
|
const separatorLine = `| ${headerDefinitions.map(() => "---").join(" | ")} |`;
|
||||||
const rowLines = rows.map((row) =>
|
const rowLines = rows.map((row) =>
|
||||||
`| ${headers.map((key) => formatCell(row[key])).join(" | ")} |`
|
`| ${headerDefinitions.map((h) => formatCell(row[h.key])).join(" | ")} |`
|
||||||
);
|
);
|
||||||
return [headerLine, separatorLine, ...rowLines].join("\n");
|
return [headerLine, separatorLine, ...rowLines].join("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
const widths = headers.map((header, idx) =>
|
const widths = headerDefinitions.map((header, idx) =>
|
||||||
Math.max(
|
Math.max(
|
||||||
header.length,
|
header.label.length,
|
||||||
...rows.map((row) => renderCell(row[headers[idx]]).length),
|
...rows.map((row) => renderCell(row[headerDefinitions[idx].key]).length),
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderRow = (values) =>
|
const renderRow = (values) =>
|
||||||
`| ${values.map((v, idx) => v.padEnd(widths[idx], " ")).join(" | ")} |`;
|
`| ${values.map((v, idx) => v.padEnd(widths[idx], " ")).join(" | ")} |`;
|
||||||
|
|
||||||
const headerLine = renderRow(headers);
|
const headerLine = renderRow(headerDefinitions.map((h) => h.label));
|
||||||
const separatorLine = `|-${widths.map((w) => "-".repeat(w)).join("-|-")}-|`;
|
const separatorLine = `|-${widths.map((w) => "-".repeat(w)).join("-|-")}-|`;
|
||||||
const rowLines = rows.map((row) =>
|
const rowLines = rows.map((row) =>
|
||||||
renderRow(headers.map((header) => renderCell(row[header])))
|
renderRow(headerDefinitions.map((header) => renderCell(row[header.key])))
|
||||||
);
|
);
|
||||||
|
|
||||||
return [headerLine, separatorLine, ...rowLines].join("\n");
|
return [headerLine, separatorLine, ...rowLines].join("\n");
|
||||||
|
|||||||
Reference in New Issue
Block a user