Removed header output relabeling and added column filtering with label handling.
All checks were successful
build / build (push) Successful in 8s

This commit is contained in:
2026-03-07 18:48:29 +01:00
parent f5877d2ccd
commit 281b0c13aa
9 changed files with 160 additions and 80 deletions

View File

@@ -4,19 +4,16 @@
import { parseArgs } from "node:util";
import {
normalizeOutputFormat,
outputFiltered,
parseHeaderSpec,
renderCliOutput,
readCsvFromStdin,
readJsonFromStdin,
renderOutput,
} from "./cli/utils.ts";
type CliValues = {
help?: boolean;
from?: string;
query?: string;
header?: string;
columns?: string;
output?: string;
[key: string]: string | boolean | undefined;
};
@@ -37,11 +34,11 @@ or: sk-tools <command> --help`;
}
function usageTable(): string {
return `Usage: sk-tools table [--from|-F <json|csv|tsv>] [--header|-H <definition|auto|a|original|o>] [global options]
return `Usage: sk-tools table [--from|-F <json|csv|tsv>] [--columns|-C <columns>] [global options]
Options:
--from, -F <json|csv|tsv> Input format on stdin (default: json)
--header, -H <value> Header mode: auto|a (default), original|o, or "col1, col2" or "key1: Label 1, key2: Label 2"`;
--columns, -C <value> Column tokens: col (raw), col: (auto), col:Label (custom), with exact match via = prefix (e.g. =col:)`;
}
function usageCommand(command: string): string {
@@ -90,7 +87,7 @@ async function main(): Promise<void> {
help: { type: "boolean", short: "h" },
from: { type: "string", short: "F" },
query: { type: "string", short: "q" },
header: { type: "string", short: "H" },
columns: { type: "string", short: "C" },
output: { type: "string", short: "o" },
},
strict: true,
@@ -108,12 +105,8 @@ async function main(): Promise<void> {
throw new Error(`Unknown command: ${command}`);
}
const outputFormat = normalizeOutputFormat(typedValues.output);
const result = await runTableCommand(typedValues);
const filtered = outputFiltered(result, typedValues.query);
const headerSpec = parseHeaderSpec(typedValues.header);
renderOutput(outputFormat, headerSpec, filtered);
const output = await runTableCommand(typedValues);
renderCliOutput(output, typedValues.output, typedValues.query, typedValues.columns);
}
main().catch((err: unknown) => {

View File

@@ -5,11 +5,16 @@ import { dsvFormat } from "d3-dsv";
import { toMarkdownTable } from "../markdown.ts";
type HeaderSpec =
| { mode: "auto" }
| { mode: "original" }
| { mode: "list"; labels: string[] }
| { mode: "map"; map: Record<string, string> };
type ColumnToken = {
sourceKey: string;
exactMatch: boolean;
labelMode: "raw" | "auto" | "custom";
customLabel?: string;
};
type ColumnSpec =
| { mode: "default" }
| { mode: "tokens"; tokens: ColumnToken[] };
type OutputFormat = "json" | "table" | "alignedtable" | "prettytable" | "tsv";
@@ -22,41 +27,69 @@ export function outputFiltered(object: unknown, query?: string): unknown {
: object;
}
export function parseHeaderSpec(headerValue?: string): HeaderSpec {
if (!headerValue) {
return { mode: "auto" };
function parseColumnToken(rawToken: string): ColumnToken {
const token = rawToken.trim();
if (!token) {
throw new Error("Invalid --columns token: empty token");
}
const raw = headerValue.trim();
if (raw === "" || raw.toLowerCase() === "auto" || raw.toLowerCase() === "a") {
return { mode: "auto" };
const exactMatch = token.startsWith("=");
const core = exactMatch ? token.slice(1).trim() : token;
if (!core) {
throw new Error(`Invalid --columns token: '${rawToken}'`);
}
if (raw.toLowerCase() === "original" || raw.toLowerCase() === "o") {
return { mode: "original" };
const separatorIndex = core.indexOf(":");
if (separatorIndex < 0) {
return {
sourceKey: core,
exactMatch,
labelMode: "raw",
};
}
const sourceKey = core.slice(0, separatorIndex).trim();
if (!sourceKey) {
throw new Error(`Invalid --columns token: '${rawToken}'`);
}
const rawLabel = core.slice(separatorIndex + 1);
const label = rawLabel.trim();
if (!label) {
return {
sourceKey,
exactMatch,
labelMode: "auto",
};
}
return {
sourceKey,
exactMatch,
labelMode: "custom",
customLabel: label,
};
}
export function parseColumnSpec(columnsValue?: string): ColumnSpec {
if (!columnsValue) {
return { mode: "default" };
}
const raw = columnsValue.trim();
if (raw === "") {
return { mode: "default" };
}
const parts = raw.split(",").map((p) => p.trim()).filter(Boolean);
const isMap = parts.some((p) => p.includes(":"));
if (!isMap) {
return { mode: "list", labels: parts };
if (parts.length === 0) {
return { mode: "default" };
}
const map: Record<string, string> = {};
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 };
return {
mode: "tokens",
tokens: parts.map((part) => parseColumnToken(part)),
};
}
export function normalizeOutputFormat(outputValue?: string): OutputFormat {
@@ -202,7 +235,7 @@ export async function readCsvFromStdin(separator: string): Promise<unknown> {
export function renderOutput(
outputFormat: OutputFormat,
headerSpec: HeaderSpec,
columnSpec: ColumnSpec,
output: unknown,
): void {
if (outputFormat === "tsv") {
@@ -215,12 +248,24 @@ export function renderOutput(
// - alignedtable: aligned columns, no quoting
// - prettytable: aligned columns + quoting selected values
if (outputFormat === "prettytable") {
console.log(toMarkdownTable(output, "prettytable", headerSpec));
console.log(toMarkdownTable(output, "prettytable", columnSpec));
} else if (outputFormat === "alignedtable") {
console.log(toMarkdownTable(output, "alignedtable", headerSpec));
console.log(toMarkdownTable(output, "alignedtable", columnSpec));
} else if (outputFormat === "table") {
console.log(toMarkdownTable(output, "table", headerSpec));
console.log(toMarkdownTable(output, "table", columnSpec));
} else {
console.log(JSON.stringify(output, null, 2));
}
}
export function renderCliOutput(
output: unknown,
outputValue?: string,
query?: string,
columnsValue?: string,
): void {
const outputFormat = normalizeOutputFormat(outputValue);
const filtered = outputFiltered(output, query);
const columnSpec = parseColumnSpec(columnsValue);
renderOutput(outputFormat, columnSpec, filtered);
}

View File

@@ -3,10 +3,11 @@
export { toMarkdownTable } from "./markdown.ts";
export {
outputFiltered,
parseHeaderSpec,
parseColumnSpec,
normalizeOutputFormat,
readJsonFromStdin,
readCsvFromStdin,
renderOutput,
renderCliOutput,
} from "./cli/utils.ts";
export { getConfigDir, getConfig } from "./config/index.ts";

View File

@@ -5,12 +5,16 @@ import { validate as validateUuid } from "uuid";
type Scalar = string | number | boolean | null | undefined;
type ScalarRow = Record<string, Scalar>;
type HeaderSpec =
type ColumnToken = {
sourceKey: string;
exactMatch: boolean;
labelMode: "raw" | "auto" | "custom";
customLabel?: string;
};
type ColumnSpec =
| { mode: "default" }
| { mode: "auto" }
| { mode: "original" }
| { mode: "list"; labels: string[] }
| { mode: "map"; map: Record<string, string> };
| { mode: "tokens"; tokens: ColumnToken[] };
type TableMode = "table" | "alignedtable" | "prettytable";
@@ -103,24 +107,46 @@ function getScalarRowsAndHeaders(value: unknown): { headers: string[]; rows: Sca
return { headers, rows: scalarRows };
}
function resolveColumnKey(headers: string[], token: ColumnToken): string {
if (token.exactMatch) {
return headers.find((header) => header === token.sourceKey) ?? token.sourceKey;
}
const matches = headers.filter(
(header) => header.toLowerCase() === token.sourceKey.toLowerCase(),
);
if (matches.length > 1) {
throw new Error(
`Ambiguous --columns token '${token.sourceKey}' matches multiple columns: ${matches.join(", ")}. Use exact match with '=...'.`,
);
}
return matches[0] ?? token.sourceKey;
}
export function toMarkdownTable(
value: unknown,
mode: TableMode = "table",
headerSpec: HeaderSpec = { mode: "default" },
columnSpec: ColumnSpec = { mode: "default" },
): string {
const prettyMode = mode === "prettytable";
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" && headerSpec.labels[idx]) {
label = headerSpec.labels[idx];
} else if (headerSpec.mode === "map" && headerSpec.map[key]) {
label = headerSpec.map[key];
}
return { key, label };
});
const headerDefinitions = columnSpec.mode === "default"
? headers.map((key) => ({
key,
label: toAutoHeaderLabel(key),
}))
: columnSpec.tokens.map((token) => {
const key = resolveColumnKey(headers, token);
let label = key;
if (token.labelMode === "auto") {
label = toAutoHeaderLabel(key);
} else if (token.labelMode === "custom") {
label = token.customLabel ?? "";
}
return { key, label };
});
if (mode !== "alignedtable" && mode !== "prettytable") {
const headerLine = `| ${headerDefinitions.map((h) => h.label).join(" | ")} |`;