mirror of
https://github.com/oblien/openship.git
synced 2026-10-17 05:40:02 +08:00
94 lines
3.2 KiB
JavaScript
94 lines
3.2 KiB
JavaScript
import { readFileSync } from "node:fs";
|
|
import { relative } from "node:path";
|
|
import { docsDirectory, filesUnder, root } from "./docs-surface.mjs";
|
|
|
|
// Tokenize documented shell commands without invoking the shell or command actions.
|
|
function tokens(line) {
|
|
const result = [];
|
|
let token = "",
|
|
quote = "";
|
|
for (let i = 0; i < line.length; i++) {
|
|
const char = line[i];
|
|
if (char === "\\" && quote !== "'" && i + 1 < line.length) {
|
|
token += line[++i];
|
|
continue;
|
|
}
|
|
if (quote) {
|
|
if (char === quote) quote = "";
|
|
else token += char;
|
|
continue;
|
|
}
|
|
if (char === '"' || char === "'") {
|
|
quote = char;
|
|
continue;
|
|
}
|
|
if (char === "#" && !token) break;
|
|
if (/\s/.test(char)) {
|
|
if (token) result.push(token);
|
|
token = "";
|
|
} else token += char;
|
|
}
|
|
if (token) result.push(token);
|
|
return result;
|
|
}
|
|
|
|
function flags(entry) {
|
|
return new Map(
|
|
entry.options.flatMap((option) => {
|
|
const names =
|
|
option.syntax
|
|
.match(/(?:^|[, ])(--?[\w-]+)/g)
|
|
?.map((name) => name.trim().replace(/^,\s*/, "")) ?? [];
|
|
return names.map((name) => [
|
|
name,
|
|
{ value: /[<[]/.test(option.syntax), optional: option.syntax.includes("[") },
|
|
]);
|
|
}),
|
|
);
|
|
}
|
|
|
|
export function checkCliExamples(catalog) {
|
|
const entries = new Map(catalog.map((entry) => [entry.command, entry]));
|
|
const globalFlags = flags(entries.get("openship"));
|
|
const errors = [];
|
|
let count = 0;
|
|
for (const file of filesUnder(docsDirectory, ".mdx")) {
|
|
if (file.includes("/cli/reference/")) continue;
|
|
const source = readFileSync(file, "utf8");
|
|
for (const block of source.matchAll(/^```(?:bash|sh|shell)\b[^\n]*\n([\s\S]*?)^```/gm)) {
|
|
for (const line of block[1].replace(/\\\n\s*/g, " ").split("\n")) {
|
|
const command = line.trim().replace(/^npx\s+/, "");
|
|
if (!command.startsWith("openship ")) continue;
|
|
const parts = tokens(command).slice(1);
|
|
let entry = entries.get("openship");
|
|
count++;
|
|
for (let index = 0; index < parts.length; index++) {
|
|
const part = parts[index];
|
|
if (["--", "|", "||", "&&", ";", ">", ">>"].includes(part)) break;
|
|
if (part.startsWith("-")) {
|
|
const [name] = part.split("=", 1);
|
|
const option = flags(entry).get(name) ?? globalFlags.get(name);
|
|
if (!option)
|
|
errors.push(`${relative(root, file)}: ${entry.command} has no ${name} option`);
|
|
else if (
|
|
option.value &&
|
|
!part.includes("=") &&
|
|
(!option.optional || !parts[index + 1]?.startsWith("-"))
|
|
)
|
|
index++;
|
|
continue;
|
|
}
|
|
const child = entry.children.find((row) =>
|
|
row.syntax.split(" ")[0].split("|").includes(part),
|
|
);
|
|
if (child) entry = entries.get(entry.command + " " + child.syntax.split(/[ |]/)[0]);
|
|
else if (entry.children.length && !entry.arguments.length)
|
|
errors.push(`${relative(root, file)}: ${entry.command} has no ${part} subcommand`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (errors.length) throw new Error([...new Set(errors)].join("\n"));
|
|
return count;
|
|
}
|