@veda-hq/pariksha 0.5.13 → 0.5.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE +6 -0
- package/README.md +280 -5
- package/dist/absolute-paths.js +59 -0
- package/dist/aggregate-gate-needs.js +153 -0
- package/dist/cli.js +130 -0
- package/dist/doc-command-rules.js +179 -0
- package/dist/doc-command-targets.js +122 -0
- package/dist/doc-commands.js +187 -0
- package/dist/doc-files.js +74 -0
- package/dist/doc-flag-spec.js +87 -0
- package/dist/doc-identifiers.js +348 -0
- package/dist/doc-links.js +183 -0
- package/dist/doc-paths.js +267 -0
- package/dist/doc-references-config.js +172 -0
- package/dist/doc-references-gate.js +338 -0
- package/dist/doc-references.js +535 -0
- package/dist/doc-scripts.js +224 -0
- package/dist/non-synced-resources.js +164 -0
- package/dist/pnpm-settings.js +350 -0
- package/dist/semver-range.js +320 -0
- package/dist/shell-text.js +193 -0
- package/package.json +5 -3
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
// The commands a Markdown document shows (DOC-DRIFT-02): each simple command
|
|
2
|
+
// in a shell fence or an inline code span, read into the words bash would
|
|
3
|
+
// hand the program. doc-command-rules.ts judges them.
|
|
4
|
+
import { posix } from 'node:path';
|
|
5
|
+
import { codeSpans, isShellFence, scanMarkdown } from "./doc-references.js";
|
|
6
|
+
import { maskQuoted, stripComment } from "./shell-text.js";
|
|
7
|
+
/** A command word: what makes a code span or a bare fence line a command. */
|
|
8
|
+
const COMMAND_HEAD = /^(?:cd\s+\S+\s*&&\s*)?(?:[A-Z_][A-Z0-9_]*=\S*\s+)*(?:pnpm|npx|node|tsx|python3?|bash|sh)\s/;
|
|
9
|
+
/**
|
|
10
|
+
* In a fence declared as shell, a script path may itself be the command
|
|
11
|
+
* (`./scripts/x.ts --flag`). Elsewhere a lone path is a mention.
|
|
12
|
+
*/
|
|
13
|
+
const SCRIPT_HEAD = /^(?:[A-Z_][A-Z0-9_]*=\S*\s+)*\.{0,2}\/?[\w./-]+\.(?:ts|mjs|js|sh|py)(?:[\s;&|]|$)/;
|
|
14
|
+
/** Fence info strings that declare a block as shell (a bare fence only may be). */
|
|
15
|
+
const DECLARED_SHELL = new Set(['bash', 'sh', 'shell', 'zsh', 'console', 'terminal']);
|
|
16
|
+
/** The value of digits a pattern already matched in base `radix`. */
|
|
17
|
+
const digitsValue = (digits, radix) => {
|
|
18
|
+
let value = 0;
|
|
19
|
+
for (const digit of digits.toLowerCase())
|
|
20
|
+
value = value * radix + '0123456789abcdef'.indexOf(digit);
|
|
21
|
+
return value;
|
|
22
|
+
};
|
|
23
|
+
/** The named escapes of ANSI-C quoting. */
|
|
24
|
+
const ANSI_C_NAMED = { a: '\u0007', b: '\b', e: '\u001b', E: '\u001b', f: '\f', n: '\n', r: '\r', t: '\t', v: '\v', '\\': '\\', "'": "'", '"': '"', '?': '?' };
|
|
25
|
+
/**
|
|
26
|
+
* The value of a `$'…'` body, as bash decodes it: the named escapes, octal
|
|
27
|
+
* `\N` to `\NNN` (low eight bits), `\xH`/`\xHH`, `\uHHHH`, `\UHHHHHHHH` and
|
|
28
|
+
* `\cX` (X's low five bits); an unknown escape keeps its backslash, and the
|
|
29
|
+
* value ends at its first NUL.
|
|
30
|
+
*/
|
|
31
|
+
const ansiC = (body) => String(body
|
|
32
|
+
.replace(/\\(?:([0-7]{1,3})|x([\da-fA-F]{1,2})|u([\da-fA-F]{1,4})|U([\da-fA-F]{1,8})|c([^])|[^])/g, (escape, octal, x, u, bigU, control) => {
|
|
33
|
+
if (octal !== undefined)
|
|
34
|
+
return String.fromCharCode(digitsValue(octal, 8) & 0xff);
|
|
35
|
+
const hex = x ?? u ?? bigU;
|
|
36
|
+
if (hex !== undefined) {
|
|
37
|
+
const code = digitsValue(hex, 16);
|
|
38
|
+
// Past Unicode bash writes no character; keep the escape.
|
|
39
|
+
return code > 0x10ffff ? escape : String.fromCodePoint(code);
|
|
40
|
+
}
|
|
41
|
+
if (control !== undefined)
|
|
42
|
+
return String.fromCharCode(control.charCodeAt(0) & 0x1f);
|
|
43
|
+
return ANSI_C_NAMED[escape.slice(1)] ?? escape;
|
|
44
|
+
})
|
|
45
|
+
.split('\0')[0]);
|
|
46
|
+
/**
|
|
47
|
+
* Shell words of one simple command, as bash hands them to the program. A
|
|
48
|
+
* word runs to unquoted whitespace and joins its parts (`--mode="a b"` is one
|
|
49
|
+
* word): single quotes keep every character, double quotes every character
|
|
50
|
+
* but a backslash before `$`, `` ` ``, `"` or `\`, and outside quotes a
|
|
51
|
+
* backslash keeps the character after it. `$'…'` is ANSI-C quoting, its
|
|
52
|
+
* escapes decoded, and `$"…"` a double-quoted string. A quote that never
|
|
53
|
+
* closes stays a character (bash refuses the line). Expansions, operators and
|
|
54
|
+
* line continuations are the callers' or out of reach.
|
|
55
|
+
*/
|
|
56
|
+
export const shellWords = (text) => Array.from(text.matchAll(/(?:\$'(?:[^'\\]|\\[^])*'|'[^']*'|\$?"(?:[^"\\]|\\[^])*"|\\[^]|[^\s'"\\]|['"\\])+/g), ([word]) => Array.from(word.matchAll(/\$'((?:[^'\\]|\\[^])*)'|'([^']*)'|\$?"((?:[^"\\]|\\[^])*)"|\\([^])|([^])/g), (part) => (part[1] === undefined ? undefined : ansiC(part[1])) ?? part[2] ?? part[3]?.replace(/\\([$`"\\])/g, '$1') ?? part[4] ?? String(part[5])).join(''));
|
|
57
|
+
/** Whether code ends in a backslash that escapes what follows: an odd run of them. */
|
|
58
|
+
const continues = (code) => /(?:^|[^\\])(?:\\\\)*\\$/.test(code);
|
|
59
|
+
const withoutCr = (line) => (line.endsWith('\r') ? line.slice(0, -1) : line);
|
|
60
|
+
/** `code` without its outer blanks, but one a backslash escapes (`a\ `). */
|
|
61
|
+
const trimCode = (code) => {
|
|
62
|
+
const trimmed = code.trim();
|
|
63
|
+
if (!continues(trimmed))
|
|
64
|
+
return trimmed;
|
|
65
|
+
const end = code.length - code.trimStart().length + trimmed.length;
|
|
66
|
+
return `${trimmed}${code.charAt(end)}`;
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* The simple commands of one shell line, as written: split at `&&`, `||`, `;`
|
|
70
|
+
* and `|` outside quotes and escapes (found in the quote-masked line, which
|
|
71
|
+
* keeps every index).
|
|
72
|
+
*/
|
|
73
|
+
export const simpleCommands = (line) => {
|
|
74
|
+
const masked = maskQuoted(line);
|
|
75
|
+
const commands = [];
|
|
76
|
+
let start = 0;
|
|
77
|
+
for (const match of masked.matchAll(/&&|\|\||;|\|/g)) {
|
|
78
|
+
commands.push(trimCode(line.slice(start, match.index)));
|
|
79
|
+
start = match.index + match[0].length;
|
|
80
|
+
}
|
|
81
|
+
commands.push(trimCode(line.slice(start)));
|
|
82
|
+
return commands;
|
|
83
|
+
};
|
|
84
|
+
/** Words after any leading `NAME=value` environment assignments. */
|
|
85
|
+
export const withoutAssignments = (words) => {
|
|
86
|
+
const first = words.findIndex((word) => !/^[A-Z_][A-Z0-9_]*=/.test(word));
|
|
87
|
+
return first === -1 ? [] : words.slice(first);
|
|
88
|
+
};
|
|
89
|
+
/** The directory a `cd` command's words name: after bash's own options (`-L`, `-P`, `-e`, `-@`) and a `--`. `cd -` is no option. */
|
|
90
|
+
const cdOperand = (words) => {
|
|
91
|
+
let at = 1;
|
|
92
|
+
while (/^-[LPe@]+$/.test(String(words[at])))
|
|
93
|
+
at += 1;
|
|
94
|
+
if (words[at] === '--')
|
|
95
|
+
at += 1;
|
|
96
|
+
return words[at];
|
|
97
|
+
};
|
|
98
|
+
/** Options of npx (npm exec) whose value is the next word, `-c`/`--call` among them. */
|
|
99
|
+
export const NPX_VALUE_OPTIONS = new Set(['--package', '-p', '--cache', '--userconfig', '--shell', '--call', '-c', '--workspace', '-w']);
|
|
100
|
+
/**
|
|
101
|
+
* The shell command an `npx -c '…'` runs, if `words` are one: the value of
|
|
102
|
+
* `-c`/`--call` after npx's other options. npm refuses a call with words
|
|
103
|
+
* after it, so `npx -c tsx x.ts` runs nothing.
|
|
104
|
+
*/
|
|
105
|
+
export const npxCall = (words) => {
|
|
106
|
+
if (words[0] !== 'npx')
|
|
107
|
+
return undefined;
|
|
108
|
+
let at = 1;
|
|
109
|
+
for (let word = words[at]; word?.startsWith('-') === true; word = words[at]) {
|
|
110
|
+
const [name, inline] = word.split(/=(.*)/s);
|
|
111
|
+
if (name === '--call' || name === '-c') {
|
|
112
|
+
const call = inline ?? words[at + 1];
|
|
113
|
+
const next = at + (inline === undefined ? 2 : 1);
|
|
114
|
+
// npm reads an empty call as none: its words run.
|
|
115
|
+
return call === '' || next < words.length ? undefined : call;
|
|
116
|
+
}
|
|
117
|
+
at += NPX_VALUE_OPTIONS.has(word) ? 2 : 1;
|
|
118
|
+
}
|
|
119
|
+
return undefined;
|
|
120
|
+
};
|
|
121
|
+
/** Every simple command shown in a Markdown document. */
|
|
122
|
+
export const documentedCommands = (file, content, exemptMarker) => {
|
|
123
|
+
const commands = [];
|
|
124
|
+
/** Reads `text` from directory `from`, recording its commands when `shown`; returns the directory its `cd`s leave the shell in. */
|
|
125
|
+
const read = (line, text, fenced, from, shown) => {
|
|
126
|
+
let cwd = from;
|
|
127
|
+
for (const part of simpleCommands(text)) {
|
|
128
|
+
const words = withoutAssignments(shellWords(part));
|
|
129
|
+
const dir = words[0] === 'cd' ? cdOperand(words) : undefined;
|
|
130
|
+
if (dir !== undefined) {
|
|
131
|
+
// An absolute operand replaces the directory.
|
|
132
|
+
cwd = posix.normalize(posix.isAbsolute(dir) ? dir : posix.join(cwd, dir)).replace(/^\.$/, '');
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
// `npx -c '…'` runs its call's commands, each one judged; npm runs the
|
|
136
|
+
// call in a shell of its own, so a `cd` in it moves only the call's later commands.
|
|
137
|
+
const call = npxCall(words);
|
|
138
|
+
if (call !== undefined) {
|
|
139
|
+
read(line, call, fenced, cwd, shown);
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (shown && words.length > 0)
|
|
143
|
+
commands.push({ file, line, words, text: part, cwd, fenced });
|
|
144
|
+
}
|
|
145
|
+
return cwd;
|
|
146
|
+
};
|
|
147
|
+
const lines = scanMarkdown(content, exemptMarker);
|
|
148
|
+
// A fence's lines are one shell session: a `cd` on its own line moves the
|
|
149
|
+
// lines after it. The next fence starts from the root again.
|
|
150
|
+
let fenceCwd = '';
|
|
151
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
152
|
+
const line = lines[index];
|
|
153
|
+
if (line === undefined)
|
|
154
|
+
break;
|
|
155
|
+
if (line.fence === null) {
|
|
156
|
+
if (line.exempt || line.frontmatter)
|
|
157
|
+
continue;
|
|
158
|
+
// An inline span is a command of its own, run from the root.
|
|
159
|
+
for (const span of codeSpans(line.text)) {
|
|
160
|
+
if (COMMAND_HEAD.test(`${span.content} `))
|
|
161
|
+
read(line.number, span.content, false, '', true);
|
|
162
|
+
}
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (isShellFence(line.fence)) {
|
|
166
|
+
// A `#` outside quotes starts a comment, and a backslash inside one
|
|
167
|
+
// continues nothing, so the comment goes before a trailing backslash is
|
|
168
|
+
// read. That backslash joins the next line as bash joins it, and the
|
|
169
|
+
// joined text is read for a comment again. A CRLF line's CR is its line break.
|
|
170
|
+
let text = stripComment(withoutCr(line.text).replace(/^\s*\$\s+/, ''));
|
|
171
|
+
const start = line.number;
|
|
172
|
+
// Only a line of the same fence continues it.
|
|
173
|
+
for (let next = lines[index + 1]; continues(text) && next?.block === line.block; next = lines[index + 1]) {
|
|
174
|
+
index += 1;
|
|
175
|
+
text = stripComment(`${text.slice(0, -1)}${withoutCr(next.text)}`);
|
|
176
|
+
}
|
|
177
|
+
text = trimCode(text);
|
|
178
|
+
// Every line's `cd` counts, an exempt line's included (the shell still
|
|
179
|
+
// runs it); only a line that runs something, unexempt, is judged.
|
|
180
|
+
fenceCwd = read(start, text, true, fenceCwd, !line.exempt && (COMMAND_HEAD.test(`${text} `) || (DECLARED_SHELL.has(line.fence) && SCRIPT_HEAD.test(text))));
|
|
181
|
+
}
|
|
182
|
+
// The fence's last line: the next fence starts from the root.
|
|
183
|
+
if (lines[index + 1]?.block !== line.block)
|
|
184
|
+
fenceCwd = '';
|
|
185
|
+
}
|
|
186
|
+
return commands;
|
|
187
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// The documents absolute-paths and non-synced-resources read, and the config
|
|
2
|
+
// both take. A product's hook hands them its own list (`--files-from`); with
|
|
3
|
+
// none, they read every file git tracks.
|
|
4
|
+
import { readFileSync, statSync } from 'node:fs';
|
|
5
|
+
import { resolve } from 'node:path';
|
|
6
|
+
import { entries } from "./coverage-by-type.js";
|
|
7
|
+
import { gitAt, must } from "./git.js";
|
|
8
|
+
/** Markdown and MDX. */
|
|
9
|
+
export const DEFAULT_INCLUDE = '\\.(md|mdx)$';
|
|
10
|
+
const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
11
|
+
/**
|
|
12
|
+
* `{ "include"?: "<regex>", "guidance"?: ["<line>", …] }`; absent keys take
|
|
13
|
+
* the defaults. Throws on anything else, so a mistyped key never passes as
|
|
14
|
+
* the default.
|
|
15
|
+
*/
|
|
16
|
+
export const parseDocConfig = (text, defaultGuidance) => {
|
|
17
|
+
if (text === undefined)
|
|
18
|
+
return { include: new RegExp(DEFAULT_INCLUDE), guidance: defaultGuidance };
|
|
19
|
+
let raw;
|
|
20
|
+
try {
|
|
21
|
+
raw = JSON.parse(text);
|
|
22
|
+
}
|
|
23
|
+
catch (e) {
|
|
24
|
+
throw new Error(`not JSON: ${e.message}`);
|
|
25
|
+
}
|
|
26
|
+
if (!isRecord(raw))
|
|
27
|
+
throw new Error('the config must be a JSON object');
|
|
28
|
+
const unknown = Object.keys(raw).find((k) => k !== 'include' && k !== 'guidance');
|
|
29
|
+
if (unknown !== undefined)
|
|
30
|
+
throw new Error(`unknown key "${unknown}"`);
|
|
31
|
+
// Absent takes the default; a present `null` is refused like any other wrong value.
|
|
32
|
+
const include = 'include' in raw ? raw['include'] : DEFAULT_INCLUDE;
|
|
33
|
+
if (typeof include !== 'string' || include === '')
|
|
34
|
+
throw new Error('"include" must be a non-empty regex string');
|
|
35
|
+
let pattern;
|
|
36
|
+
try {
|
|
37
|
+
pattern = new RegExp(include);
|
|
38
|
+
}
|
|
39
|
+
catch (e) {
|
|
40
|
+
throw new Error(`"include" is not a regex: ${e.message}`);
|
|
41
|
+
}
|
|
42
|
+
const guidance = 'guidance' in raw ? raw['guidance'] : defaultGuidance;
|
|
43
|
+
if (!Array.isArray(guidance) || guidance.some((line) => typeof line !== 'string'))
|
|
44
|
+
throw new Error('"guidance" must be a list of lines');
|
|
45
|
+
return { include: pattern, guidance: guidance };
|
|
46
|
+
};
|
|
47
|
+
/** Whether `path` (under `base` when relative) names a regular file, a symlink to one included. */
|
|
48
|
+
export const isFileAt = (base, path) => {
|
|
49
|
+
try {
|
|
50
|
+
return statSync(resolve(base, path)).isFile();
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* The documents to check, as listed: each entry of `listed` that names a
|
|
58
|
+
* regular file under `base` and matches `include`, in order; a named path
|
|
59
|
+
* that is not a file is dropped. With no list, every file git tracks under
|
|
60
|
+
* `base`, as git names it.
|
|
61
|
+
*/
|
|
62
|
+
export const selectDocs = (base, listed, include) => {
|
|
63
|
+
const paths = listed === undefined ? must(gitAt(base), ['ls-files', '-z']).split('\0').filter(Boolean) : entries(listed);
|
|
64
|
+
return paths.filter((path) => isFileAt(base, path) && include.test(path));
|
|
65
|
+
};
|
|
66
|
+
/** A document's lines as a shell `read` loop sees them: split on newlines, a last line without one included. */
|
|
67
|
+
export const linesOf = (text) => {
|
|
68
|
+
const lines = text.split('\n');
|
|
69
|
+
if (lines.at(-1) === '')
|
|
70
|
+
lines.pop();
|
|
71
|
+
return lines;
|
|
72
|
+
};
|
|
73
|
+
/** A listed document's text. */
|
|
74
|
+
export const readDoc = (base, path) => readFileSync(resolve(base, path), 'utf8');
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// What a script's own source declares about its flags (DOC-DRIFT-02): whether
|
|
2
|
+
// it reads its own argv, which flags it knows, the values an enum-valued flag
|
|
3
|
+
// takes, its required flags and its first positional's choices.
|
|
4
|
+
// doc-command-rules.ts holds a documented command to them.
|
|
5
|
+
/** argparse actions that take no value: the flag alone is the input. */
|
|
6
|
+
const FLAG_ONLY_ACTIONS = ['store_true', 'store_false', 'store_const', 'append_const', 'count', 'help', 'version'];
|
|
7
|
+
/** The keyword arguments in an `add_argument` call's text, each value unquoted. A string is read whole, so `help="nargs=0"` is one argument. */
|
|
8
|
+
const keywordArguments = (text) => {
|
|
9
|
+
const out = new Map();
|
|
10
|
+
for (const match of text.matchAll(/"[^"]*"|'[^']*'|(\w+)\s*=\s*("[^"]*"|'[^']*'|[^,"']*)/g)) {
|
|
11
|
+
if (match[1] !== undefined)
|
|
12
|
+
out.set(match[1], String(match[2]).trim().replace(/^["']|["']$/g, ''));
|
|
13
|
+
}
|
|
14
|
+
return out;
|
|
15
|
+
};
|
|
16
|
+
/** Words an argparse option with these keyword arguments takes. */
|
|
17
|
+
const argparseValueCount = (kwargs) => {
|
|
18
|
+
const action = kwargs.get('action');
|
|
19
|
+
if (action !== undefined && (FLAG_ONLY_ACTIONS.includes(action) || action.endsWith('BooleanOptionalAction')))
|
|
20
|
+
return 0;
|
|
21
|
+
const nargs = kwargs.get('nargs');
|
|
22
|
+
if (nargs === undefined || nargs === '?')
|
|
23
|
+
return 1;
|
|
24
|
+
return /^\d+$/.test(nargs) ? Number(nargs) : Infinity;
|
|
25
|
+
};
|
|
26
|
+
const quotedValues = (text) => new Set([...text.matchAll(/["']([^"']+)["']/g)].map((m) => String(m[1])));
|
|
27
|
+
/** What a script's own source declares about its flags. */
|
|
28
|
+
export const flagSpec = (source, path) => {
|
|
29
|
+
const shell = /\.(sh|bash)$/.test(path) || !/\.\w+$/.test(path);
|
|
30
|
+
const python = path.endsWith('.py');
|
|
31
|
+
// A script that hands its arguments on cannot be judged from its own text.
|
|
32
|
+
// Nor can a shell script that sources a file: the file runs with the
|
|
33
|
+
// script's own arguments, so its flags may be read there.
|
|
34
|
+
const forwards = shell
|
|
35
|
+
? /"\$@"|\$\*|"\$\{@\}"|\$@|^\s*(?:source|\.)\s/m.test(source)
|
|
36
|
+
: python
|
|
37
|
+
? /parse_known_args|sys\.argv\[1:\]\s*\)/.test(source) && !/add_argument/.test(source)
|
|
38
|
+
: /strict:\s*false/.test(source);
|
|
39
|
+
const values = (flag) => {
|
|
40
|
+
const usage = new RegExp(`${flag}[ =]<([\\w.:-]+(?:\\|[\\w.:-]+)+)>`).exec(source);
|
|
41
|
+
if (usage !== null)
|
|
42
|
+
return new Set(String(usage[1]).split('|'));
|
|
43
|
+
const braces = new RegExp(`${flag}[ =]\\{([\\w.:-]+(?:,[\\w.:-]+)+)\\}`).exec(source);
|
|
44
|
+
if (braces !== null)
|
|
45
|
+
return new Set(String(braces[1]).split(','));
|
|
46
|
+
const choices = new RegExp(`add_argument\\(\\s*["']${flag}["'][^)]*?choices\\s*=\\s*[[(]([^\\])]*)[\\])]`).exec(source);
|
|
47
|
+
return choices === null ? undefined : quotedValues(String(choices[1]));
|
|
48
|
+
};
|
|
49
|
+
// With subparsers, an argument a subcommand's parser adds (`x =
|
|
50
|
+
// subs.add_parser(…)`, or chained on one) is that subcommand's: not
|
|
51
|
+
// required of every invocation, and not the first word.
|
|
52
|
+
const subcommands = /add_subparsers/.test(source);
|
|
53
|
+
const local = new Set([...source.matchAll(/(\w+)\s*=\s*[\w.]+\.add_parser\(/g)].map((m) => String(m[1])));
|
|
54
|
+
const calls = python
|
|
55
|
+
? [...source.matchAll(/(?:(\w+)|\))\.add_argument\(\s*["']([\w-]+)["']((?:\s*,\s*["'][\w-]+["'])*)([^)]*)/g)]
|
|
56
|
+
.filter((match) => !subcommands || (match[1] !== undefined && !local.has(match[1])))
|
|
57
|
+
.map((match) => ({
|
|
58
|
+
name: String(match[2]),
|
|
59
|
+
// Every option string: `add_argument("-o", "--out")` is both.
|
|
60
|
+
aliases: [String(match[2]), ...Array.from(String(match[3]).matchAll(/["']([\w-]+)["']/g), (m) => String(m[1]))],
|
|
61
|
+
rest: String(match[4]),
|
|
62
|
+
}))
|
|
63
|
+
: [];
|
|
64
|
+
const required = calls.filter((call) => call.name.startsWith('--') && /required\s*=\s*True/.test(call.rest)).map((call) => call.name);
|
|
65
|
+
// The first positional's own choices. A call's text ends at its first `)`, so a tuple of choices runs to the end of it.
|
|
66
|
+
const first = calls.find((call) => !call.name.startsWith('-'));
|
|
67
|
+
const choices = first === undefined ? null : /choices\s*=\s*[[(]([^\])]*)/.exec(first.rest);
|
|
68
|
+
return {
|
|
69
|
+
closed: !forwards,
|
|
70
|
+
known: (flag) => {
|
|
71
|
+
if (flag === '--help' || flag === '-h')
|
|
72
|
+
return true;
|
|
73
|
+
if (new RegExp(`${flag}(?![\\w-])`).test(source))
|
|
74
|
+
return true;
|
|
75
|
+
// node:util parseArgs declares `name: { type: … }` without dashes.
|
|
76
|
+
const key = flag.replace(/^--/, '');
|
|
77
|
+
return new RegExp(`(?:^|[\\s{,])['"]?${key}['"]?\\s*:\\s*\\{[^}]*\\btype\\s*:`).test(source);
|
|
78
|
+
},
|
|
79
|
+
values,
|
|
80
|
+
required,
|
|
81
|
+
valueCount: (flag) => {
|
|
82
|
+
const call = calls.find((each) => each.aliases.includes(flag));
|
|
83
|
+
return call === undefined ? 0 : argparseValueCount(keywordArguments(call.rest));
|
|
84
|
+
},
|
|
85
|
+
positional: choices === null ? undefined : quotedValues(String(choices[1])),
|
|
86
|
+
};
|
|
87
|
+
};
|