@amritk/nish-x86_64-linux 0.24.0 → 0.26.0
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/bin/nish +0 -0
- package/docs/AI.md +45 -4
- package/package.json +1 -1
- package/runtime/browser.mjs +4 -3
- package/runtime/core/host.d.ts +12 -0
- package/runtime/core/host.mjs +74 -2
- package/runtime/nish.d.ts +148 -0
- package/runtime/nish.h +59 -0
- package/runtime/nish.mjs +25 -0
- package/runtime/node.mjs +136 -6
- package/runtime/runtime-net.c +44 -3
- package/runtime/runtime-os.c +351 -18
- package/scripts/build.sh +13 -3
- package/std/README.md +27 -10
- package/std/cli-args.ts +1216 -0
- package/std/cli-flags.ts +585 -0
- package/std/cli.ts +308 -19
- package/std/compress/checksum.ts +132 -20
- package/std/compress/deflate.ts +638 -248
- package/std/compress/gzip.ts +20 -6
- package/std/crypto/LICENSE-fiat-crypto-p384 +21 -0
- package/std/crypto/p256.ts +63 -1
- package/std/crypto/p384.ts +6044 -0
- package/std/crypto/pki-roots.ts +322 -0
- package/std/crypto/pki.ts +1207 -0
- package/std/crypto/rsa.ts +663 -0
- package/std/crypto/x509.ts +1054 -58
- package/std/net/dns-config.ts +734 -0
- package/std/net/dns-message.ts +517 -0
- package/std/net/dns-resolver.ts +1015 -0
- package/std/net/dns.ts +478 -0
- package/std/net/http1-client.ts +1227 -0
- package/std/net/http1-server.ts +7 -2
- package/std/net/http1.ts +9 -9
- package/std/net/postgres.ts +259 -26
- package/std/net/tls/client.ts +1094 -0
- package/std/net/tls/codec.ts +585 -3
- package/std/net/tls/record-client.ts +1192 -0
- package/std/net/tls/verify.ts +221 -0
- package/std/net/tls-tcp.ts +642 -11
- package/std/net/tls.ts +2 -2
- package/std/net/url.ts +559 -4
- package/std/yaml-write.ts +415 -0
package/std/cli-flags.ts
ADDED
|
@@ -0,0 +1,585 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `std/cli-flags` — a command line's flags, declared once as data and read
|
|
3
|
+
* back by name, with the same declaration writing the `--help` rows.
|
|
4
|
+
*
|
|
5
|
+
* import { FlagTable } from "nish/cli-flags";
|
|
6
|
+
*
|
|
7
|
+
* const flags = new FlagTable("git-lite");
|
|
8
|
+
* flags.command("remote add", "<name> <url>", "add a remote");
|
|
9
|
+
* flags.string("output", "<file>", "where to write");
|
|
10
|
+
* flags.number("jobs", "<n>", "parallel jobs");
|
|
11
|
+
* flags.boolean("verbose", "say more");
|
|
12
|
+
* flags.repeatable("include", "<dir>", "search here too");
|
|
13
|
+
* const got = flags.parseFrom(process.argv, 1);
|
|
14
|
+
* if (!got.ok) { got.exitUsage(); }
|
|
15
|
+
* got.command; // "remote add", or "" for none
|
|
16
|
+
* got.str("output"); got.num("jobs"); got.bool("verbose"); got.all("include");
|
|
17
|
+
*
|
|
18
|
+
* **No function values.** A flag is a name, a kind, a placeholder and a line
|
|
19
|
+
* of help, and the program asks the parse for a value by name afterwards, so
|
|
20
|
+
* the table is plain data the help menu can read too (`writeHelp`).
|
|
21
|
+
*
|
|
22
|
+
* **The grammar** is the GNU long-option one without the short options:
|
|
23
|
+
* `--name value` and `--name=value` for a string, number or repeatable flag,
|
|
24
|
+
* `--name` and `--no-name` for a boolean, `--` ending the flags, and a lone
|
|
25
|
+
* `-` an ordinary word. Words that are not flags may come before, between or
|
|
26
|
+
* after them; the leading ones choose the command, the longest declared path
|
|
27
|
+
* they spell, and the rest are positionals. A flag belongs to the whole table,
|
|
28
|
+
* not to one command: a program that wants `--force` only under `remote add`
|
|
29
|
+
* checks `command` itself.
|
|
30
|
+
*
|
|
31
|
+
* **A usage error is an answer, not a panic.** `parse` stops at the first one
|
|
32
|
+
* and the result carries it as data (`code`, `message`, `argument`), so a
|
|
33
|
+
* program can test it or word it its own way; `exitUsage` is the usual
|
|
34
|
+
* ending, the message on stderr and exit status 2, the convention `nish`
|
|
35
|
+
* itself keeps. Asking for a flag the table does not declare, or as the wrong
|
|
36
|
+
* kind, is a bug in the program rather than in its command line, and panics.
|
|
37
|
+
*
|
|
38
|
+
* **What it reaches.** Nothing but `exitUsage`, which reaches `exit`.
|
|
39
|
+
*/
|
|
40
|
+
import { HelpMenu } from "nish/cli"
|
|
41
|
+
import { splitWhitespace } from "nish/text"
|
|
42
|
+
|
|
43
|
+
/** A flag that takes one value, read with `str`. */
|
|
44
|
+
export const FLAG_STRING: i32 = 0
|
|
45
|
+
|
|
46
|
+
/** A flag whose one value is a number, read with `num`. */
|
|
47
|
+
export const FLAG_NUMBER: i32 = 1
|
|
48
|
+
|
|
49
|
+
/** A flag that takes no value: `--name` is true and `--no-name` false, read with `bool`. */
|
|
50
|
+
export const FLAG_BOOLEAN: i32 = 2
|
|
51
|
+
|
|
52
|
+
/** A flag that takes a value each time it is given, every one kept in order, read with `all`. */
|
|
53
|
+
export const FLAG_REPEATABLE: i32 = 3
|
|
54
|
+
|
|
55
|
+
/** `ParsedFlags.code` for a parse with no usage error. */
|
|
56
|
+
export const FLAG_OK: i32 = 0
|
|
57
|
+
|
|
58
|
+
/** A `--name` the table does not declare, or a short option (`-v`), which the grammar does not have. */
|
|
59
|
+
export const FLAG_ERR_UNKNOWN: i32 = 1
|
|
60
|
+
|
|
61
|
+
/** A flag that takes a value, last on the line with no `=`. */
|
|
62
|
+
export const FLAG_ERR_MISSING_VALUE: i32 = 2
|
|
63
|
+
|
|
64
|
+
/** A number flag's value that is not one finite number. */
|
|
65
|
+
export const FLAG_ERR_NOT_A_NUMBER: i32 = 3
|
|
66
|
+
|
|
67
|
+
/** A flag that is not repeatable, given a second time (for a boolean, in either spelling). */
|
|
68
|
+
export const FLAG_ERR_REPEATED: i32 = 4
|
|
69
|
+
|
|
70
|
+
/** `--name=value` (or `--no-name=value`) on a boolean. */
|
|
71
|
+
export const FLAG_ERR_BOOLEAN_VALUE: i32 = 5
|
|
72
|
+
|
|
73
|
+
/** Leading words that spell no declared command, when the table declares any. */
|
|
74
|
+
export const FLAG_ERR_UNKNOWN_COMMAND: i32 = 6
|
|
75
|
+
|
|
76
|
+
/** One declared flag: its name without the `--`, its `FLAG_*` kind and its help. */
|
|
77
|
+
export class FlagSpec {
|
|
78
|
+
name: string
|
|
79
|
+
kind: i32
|
|
80
|
+
meta: string
|
|
81
|
+
description: string
|
|
82
|
+
|
|
83
|
+
constructor(name: string, kind: i32, meta: string, description: string) {
|
|
84
|
+
this.name = name
|
|
85
|
+
this.kind = kind
|
|
86
|
+
this.meta = meta
|
|
87
|
+
this.description = description
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* One declared command: its `path` as the words a person types, those words
|
|
93
|
+
* split once so that matching does not split them per parse, and its help.
|
|
94
|
+
*/
|
|
95
|
+
export class CommandSpec {
|
|
96
|
+
path: string
|
|
97
|
+
words: string[]
|
|
98
|
+
shape: string
|
|
99
|
+
description: string
|
|
100
|
+
|
|
101
|
+
constructor(path: string, words: string[], shape: string, description: string) {
|
|
102
|
+
this.path = path
|
|
103
|
+
this.words = words
|
|
104
|
+
this.shape = shape
|
|
105
|
+
this.description = description
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Whether `path` and `words` agree on their first `count` words; both have at least that many. */
|
|
110
|
+
const agrees = (path: string[], words: string[], count: i32): boolean => {
|
|
111
|
+
for (let i: i32 = 0; i < count && i < toI32(path.length) && i < toI32(words.length); i++) {
|
|
112
|
+
if (path[i] !== words[i]) {
|
|
113
|
+
return false
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return true
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Whether `text` is a decimal number in the one syntax every target reads
|
|
121
|
+
* alike: an optional sign, digits with an optional `.` fraction (one digit at
|
|
122
|
+
* least), then an optional `e` exponent with its own sign, ASCII only.
|
|
123
|
+
* `Number` alone reads more, and not the same more natively as under Node
|
|
124
|
+
* (`0x1A`, `0b11`, padding, `""` as zero), so a number flag asks this first.
|
|
125
|
+
*/
|
|
126
|
+
const isDecimal = (text: string): boolean => {
|
|
127
|
+
// 0 before anything, 1 in the digits, 2 in the fraction, 3 just after
|
|
128
|
+
// the `e`, 4 in the exponent.
|
|
129
|
+
let state: i32 = 0
|
|
130
|
+
let mantissa: i32 = 0
|
|
131
|
+
let exponent: i32 = 0
|
|
132
|
+
for (let i: i32 = 0; i < toI32(text.length); i++) {
|
|
133
|
+
const c = toI32(text.charCodeAt(i))
|
|
134
|
+
if (c >= 48 && c <= 57) {
|
|
135
|
+
if (state >= 3) {
|
|
136
|
+
exponent = exponent + 1
|
|
137
|
+
state = 4
|
|
138
|
+
} else {
|
|
139
|
+
mantissa = mantissa + 1
|
|
140
|
+
state = state === 0 ? toI32(1) : state
|
|
141
|
+
}
|
|
142
|
+
} else if ((c === 43 || c === 45) && (state === 0 || state === 3)) {
|
|
143
|
+
state = state + 1
|
|
144
|
+
} else if (c === 46 && state <= 1) {
|
|
145
|
+
state = 2
|
|
146
|
+
} else if ((c === 101 || c === 69) && (state === 1 || state === 2) && mantissa > 0) {
|
|
147
|
+
state = 3
|
|
148
|
+
} else {
|
|
149
|
+
return false
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return mantissa > 0 && (state < 3 || exponent > 0)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Whether `name` can follow `--` and be matched: not empty, no leading `-`, and no `=`, space or control byte. */
|
|
156
|
+
const isFlagName = (name: string): boolean => {
|
|
157
|
+
if (name.length === 0 || name.startsWith("-")) {
|
|
158
|
+
return false
|
|
159
|
+
}
|
|
160
|
+
for (let i: i32 = 0; i < toI32(name.length); i++) {
|
|
161
|
+
const c = toI32(name.charCodeAt(i))
|
|
162
|
+
if (c <= 32 || c === 61) {
|
|
163
|
+
return false
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return true
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* The flags and commands of one program, declared in the order its help
|
|
171
|
+
* lists them. Each declaring method panics on a name no command line could
|
|
172
|
+
* match (empty, a leading `-`, an `=` or whitespace in a flag name), one
|
|
173
|
+
* declared twice, or a `no-x` beside a boolean `x`, because the table is the
|
|
174
|
+
* program's own text and a mistake there is a bug.
|
|
175
|
+
*/
|
|
176
|
+
export class FlagTable {
|
|
177
|
+
program: string
|
|
178
|
+
flags: FlagSpec[]
|
|
179
|
+
commands: CommandSpec[]
|
|
180
|
+
|
|
181
|
+
constructor(program: string) {
|
|
182
|
+
this.program = program
|
|
183
|
+
this.flags = []
|
|
184
|
+
this.commands = []
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** The index of the flag `name` (without `--`), or `-1`. */
|
|
188
|
+
indexOf(name: string): i32 {
|
|
189
|
+
for (let i: i32 = 0; i < toI32(this.flags.length); i++) {
|
|
190
|
+
if (this.flags[i].name === name) {
|
|
191
|
+
return i
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return -1
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** A flag of `kind`, after the checks every declaring method shares. */
|
|
198
|
+
declare(name: string, kind: i32, meta: string, description: string): void {
|
|
199
|
+
if (!isFlagName(name)) {
|
|
200
|
+
panic(
|
|
201
|
+
`nish/cli-flags: a flag name is not empty and has no leading '-', '=' or whitespace, got "${name}"`
|
|
202
|
+
)
|
|
203
|
+
}
|
|
204
|
+
if (this.indexOf(name) >= 0) {
|
|
205
|
+
panic(`nish/cli-flags: --${name} is declared twice`)
|
|
206
|
+
}
|
|
207
|
+
// `--no-x` cannot be both a flag of its own and the clearing of a boolean `--x`.
|
|
208
|
+
const clears = kind === FLAG_BOOLEAN ? this.indexOf(`no-${name}`) : toI32(-1)
|
|
209
|
+
const cleared = name.startsWith("no-") ? this.indexOf(name.substring(3)) : toI32(-1)
|
|
210
|
+
if (clears >= 0 || (cleared >= 0 && this.flags[cleared].kind === FLAG_BOOLEAN)) {
|
|
211
|
+
panic(
|
|
212
|
+
`nish/cli-flags: --${kind === FLAG_BOOLEAN ? `no-${name}` : name} would both be a flag and clear a boolean`
|
|
213
|
+
)
|
|
214
|
+
}
|
|
215
|
+
this.flags.push(new FlagSpec(name, kind, meta, description))
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* A command: `path` is one or more words (`"remote add"`), `shape` what
|
|
220
|
+
* follows them in the help (`"<name> <url>"`, may be empty). Declaring any
|
|
221
|
+
* command makes leading words that spell none a usage error.
|
|
222
|
+
*/
|
|
223
|
+
command(path: string, shape: string, description: string): void {
|
|
224
|
+
const words = splitWhitespace(path)
|
|
225
|
+
const joined = words.join(" ")
|
|
226
|
+
if (words.length === 0) {
|
|
227
|
+
panic("nish/cli-flags: a command path has at least one word")
|
|
228
|
+
}
|
|
229
|
+
for (const w of words) {
|
|
230
|
+
if (w.startsWith("-")) {
|
|
231
|
+
panic(`nish/cli-flags: a command word has no leading '-', got "${w}" in "${joined}"`)
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
for (const c of this.commands) {
|
|
235
|
+
if (c.path === joined) {
|
|
236
|
+
panic(`nish/cli-flags: the command "${joined}" is declared twice`)
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
this.commands.push(new CommandSpec(joined, words, shape, description))
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** A string flag: `--name value` or `--name=value`, once. */
|
|
243
|
+
string(name: string, meta: string, description: string): void {
|
|
244
|
+
this.declare(name, FLAG_STRING, meta, description)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* A number flag, read as an `f64`: a finite decimal (`-2.5`, `1e3`, `.5`),
|
|
249
|
+
* the same on every target. Hex, binary, `Infinity`, padding and `""` are
|
|
250
|
+
* usage errors.
|
|
251
|
+
*/
|
|
252
|
+
number(name: string, meta: string, description: string): void {
|
|
253
|
+
this.declare(name, FLAG_NUMBER, meta, description)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** A boolean flag: `--name` sets it, `--no-name` clears it, once. */
|
|
257
|
+
boolean(name: string, description: string): void {
|
|
258
|
+
this.declare(name, FLAG_BOOLEAN, "", description)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** A flag given any number of times, each with a value. */
|
|
262
|
+
repeatable(name: string, meta: string, description: string): void {
|
|
263
|
+
this.declare(name, FLAG_REPEATABLE, meta, description)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* The index of the command `words` spell the longest declared path of, or
|
|
268
|
+
* `-1`. A path spelled by a prefix of `words` counts; the words after it
|
|
269
|
+
* are the positionals.
|
|
270
|
+
*/
|
|
271
|
+
matchCommand(words: string[]): i32 {
|
|
272
|
+
let best: i32 = -1
|
|
273
|
+
let bestLength: i32 = 0
|
|
274
|
+
for (let i: i32 = 0; i < toI32(this.commands.length); i++) {
|
|
275
|
+
const path = this.commands[i].words
|
|
276
|
+
const length = toI32(path.length)
|
|
277
|
+
if (length <= toI32(words.length) && length > bestLength && agrees(path, words, length)) {
|
|
278
|
+
best = i
|
|
279
|
+
bestLength = length
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return best
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* The words an unknown command is reported as: the leading words up to and
|
|
287
|
+
* including the first that no declared path continues with, so
|
|
288
|
+
* `remote rename x` against `remote add` is `"remote rename"`.
|
|
289
|
+
*/
|
|
290
|
+
unknownCommand(words: string[]): string {
|
|
291
|
+
let known: i32 = 0
|
|
292
|
+
for (const c of this.commands) {
|
|
293
|
+
let k: i32 = 0
|
|
294
|
+
while (k < toI32(c.words.length) && k < toI32(words.length) && agrees(c.words, words, k + 1)) {
|
|
295
|
+
k = k + 1
|
|
296
|
+
}
|
|
297
|
+
if (k > known) {
|
|
298
|
+
known = k
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
const shown: string[] = []
|
|
302
|
+
for (let i: i32 = 0; i < toI32(words.length) && i <= known; i++) {
|
|
303
|
+
shown.push(words[i])
|
|
304
|
+
}
|
|
305
|
+
return shown.join(" ")
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** `args` read against the table, every one of them an argument: `parseFrom(args, 0)`. */
|
|
309
|
+
parse(args: string[]): ParsedFlags {
|
|
310
|
+
return this.parseFrom(args, toI32(0))
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* `args` from index `first` on read against the table, so that a program
|
|
315
|
+
* passes `process.argv` and `1` to skip its own path. Stops at the first
|
|
316
|
+
* usage error, which the answer then carries; the command is still matched
|
|
317
|
+
* on the words read before it, so the usage hint can name it.
|
|
318
|
+
*/
|
|
319
|
+
parseFrom(args: string[], first: i32): ParsedFlags {
|
|
320
|
+
const got = new ParsedFlags(this)
|
|
321
|
+
const words: string[] = []
|
|
322
|
+
const trailing: string[] = []
|
|
323
|
+
let ended = false
|
|
324
|
+
// A value flag written without `=` waits here for the next argument,
|
|
325
|
+
// which is its value whatever it looks like, so that `--jobs -1` and
|
|
326
|
+
// `--grep --x` mean what they say.
|
|
327
|
+
let pending: i32 = -1
|
|
328
|
+
let pendingFlag = ""
|
|
329
|
+
let pendingArg = ""
|
|
330
|
+
for (let i: i32 = 0; i < toI32(args.length) && got.ok; i++) {
|
|
331
|
+
const arg = args[i]
|
|
332
|
+
if (i < first) {
|
|
333
|
+
// Before `first`, so not read at all.
|
|
334
|
+
} else if (pending >= 0) {
|
|
335
|
+
got.take(pending, pendingFlag, arg, pendingArg)
|
|
336
|
+
pending = -1
|
|
337
|
+
} else if (ended) {
|
|
338
|
+
trailing.push(arg)
|
|
339
|
+
} else if (arg === "--") {
|
|
340
|
+
ended = true
|
|
341
|
+
} else if (arg === "-" || !arg.startsWith("-")) {
|
|
342
|
+
words.push(arg)
|
|
343
|
+
} else if (!arg.startsWith("--")) {
|
|
344
|
+
got.fail(FLAG_ERR_UNKNOWN, `unknown flag ${arg}`, arg)
|
|
345
|
+
} else {
|
|
346
|
+
const eq = toI32(arg.indexOf("="))
|
|
347
|
+
const flag = eq < 0 ? arg : arg.slice(0, eq)
|
|
348
|
+
let at = this.indexOf(flag.substring(2))
|
|
349
|
+
let negated = false
|
|
350
|
+
if (at < 0 && flag.startsWith("--no-")) {
|
|
351
|
+
const cleared = this.indexOf(flag.substring(5))
|
|
352
|
+
if (cleared >= 0 && this.flags[cleared].kind === FLAG_BOOLEAN) {
|
|
353
|
+
at = cleared
|
|
354
|
+
negated = true
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
if (at < 0) {
|
|
358
|
+
got.fail(FLAG_ERR_UNKNOWN, `unknown flag ${flag}`, arg)
|
|
359
|
+
} else {
|
|
360
|
+
const spec = this.flags[at]
|
|
361
|
+
if (spec.kind === FLAG_BOOLEAN) {
|
|
362
|
+
if (eq >= 0) {
|
|
363
|
+
got.fail(FLAG_ERR_BOOLEAN_VALUE, `${flag} takes no value`, arg)
|
|
364
|
+
} else if (got.counts[at] > 0) {
|
|
365
|
+
got.fail(FLAG_ERR_REPEATED, `--${spec.name} is given twice`, arg)
|
|
366
|
+
} else {
|
|
367
|
+
got.bools[at] = !negated
|
|
368
|
+
got.counts[at] = 1
|
|
369
|
+
}
|
|
370
|
+
} else if (eq >= 0) {
|
|
371
|
+
got.take(at, flag, arg.substring(eq + 1), arg)
|
|
372
|
+
} else {
|
|
373
|
+
pending = at
|
|
374
|
+
pendingFlag = flag
|
|
375
|
+
pendingArg = arg
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
if (pending >= 0 && got.ok) {
|
|
381
|
+
const meta = this.flags[pending].meta
|
|
382
|
+
const wanted = meta.length > 0 ? meta : "a value"
|
|
383
|
+
got.fail(FLAG_ERR_MISSING_VALUE, `${pendingFlag} needs ${wanted}`, pendingArg)
|
|
384
|
+
}
|
|
385
|
+
if (this.commands.length > 0) {
|
|
386
|
+
const at = this.matchCommand(words)
|
|
387
|
+
if (at >= 0) {
|
|
388
|
+
got.commandAt = at
|
|
389
|
+
got.command = this.commands[at].path
|
|
390
|
+
} else if (words.length > 0 && got.ok) {
|
|
391
|
+
const shown = this.unknownCommand(words)
|
|
392
|
+
got.fail(FLAG_ERR_UNKNOWN_COMMAND, `unknown command "${shown}"`, shown)
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
const skip: i32 = got.commandAt >= 0 ? toI32(this.commands[got.commandAt].words.length) : 0
|
|
396
|
+
for (let w: i32 = 0; w < toI32(words.length); w++) {
|
|
397
|
+
if (w >= skip) {
|
|
398
|
+
got.positionals.push(words[w])
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
for (const t of trailing) {
|
|
402
|
+
got.positionals.push(t)
|
|
403
|
+
}
|
|
404
|
+
return got
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* The table as help rows: a `Commands` section when any command is
|
|
409
|
+
* declared (its path, shape and description), then a `Flags` section, each
|
|
410
|
+
* in declaration order. The menu's header and usage lines are the caller's,
|
|
411
|
+
* written before this.
|
|
412
|
+
*/
|
|
413
|
+
writeHelp(menu: HelpMenu): void {
|
|
414
|
+
if (this.commands.length > 0) {
|
|
415
|
+
menu.section("Commands")
|
|
416
|
+
for (const c of this.commands) {
|
|
417
|
+
menu.row(c.path, c.shape, c.description)
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
if (this.flags.length > 0) {
|
|
421
|
+
menu.section("Flags")
|
|
422
|
+
for (const f of this.flags) {
|
|
423
|
+
menu.row(`--${f.name}`, f.meta, f.description)
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* What `parse` answered: a usage error, or every flag's value. Values sit in
|
|
431
|
+
* arrays parallel to the table's `flags`: `counts` says how often each was
|
|
432
|
+
* given, `texts` and `numbers` hold a string or number flag's value,
|
|
433
|
+
* `bools` a boolean's and `lists` a repeatable flag's values in the order
|
|
434
|
+
* given. A flag that was not given
|
|
435
|
+
* reads as `""`, `0`, `false` or no values; `has` tells that apart from one
|
|
436
|
+
* given as such.
|
|
437
|
+
*/
|
|
438
|
+
export class ParsedFlags {
|
|
439
|
+
table: FlagTable
|
|
440
|
+
ok: boolean
|
|
441
|
+
code: i32
|
|
442
|
+
message: string
|
|
443
|
+
argument: string
|
|
444
|
+
command: string
|
|
445
|
+
commandAt: i32
|
|
446
|
+
positionals: string[]
|
|
447
|
+
counts: i32[]
|
|
448
|
+
texts: string[]
|
|
449
|
+
numbers: f64[]
|
|
450
|
+
bools: boolean[]
|
|
451
|
+
lists: string[][]
|
|
452
|
+
|
|
453
|
+
constructor(table: FlagTable) {
|
|
454
|
+
this.table = table
|
|
455
|
+
this.ok = true
|
|
456
|
+
this.code = FLAG_OK
|
|
457
|
+
this.message = ""
|
|
458
|
+
this.argument = ""
|
|
459
|
+
this.command = ""
|
|
460
|
+
this.commandAt = -1
|
|
461
|
+
this.positionals = []
|
|
462
|
+
this.counts = []
|
|
463
|
+
this.texts = []
|
|
464
|
+
this.numbers = []
|
|
465
|
+
this.bools = []
|
|
466
|
+
this.lists = []
|
|
467
|
+
for (let i: i32 = 0; i < toI32(table.flags.length); i++) {
|
|
468
|
+
this.counts.push(toI32(0))
|
|
469
|
+
this.texts.push("")
|
|
470
|
+
this.numbers.push(toF64(0))
|
|
471
|
+
this.bools.push(false)
|
|
472
|
+
const none: string[] = []
|
|
473
|
+
this.lists.push(none)
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/** Records the first usage error; a parse stops at it. */
|
|
478
|
+
fail(code: i32, message: string, argument: string): void {
|
|
479
|
+
this.ok = false
|
|
480
|
+
this.code = code
|
|
481
|
+
this.message = message
|
|
482
|
+
this.argument = argument
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/** Stores `value` for the flag at `at`, spelled `flag` in `arg`, or fails as its kind requires. */
|
|
486
|
+
take(at: i32, flag: string, value: string, arg: string): void {
|
|
487
|
+
const kind = this.table.flags[at].kind
|
|
488
|
+
if (kind === FLAG_REPEATABLE) {
|
|
489
|
+
this.lists[at].push(value)
|
|
490
|
+
this.counts[at] = this.counts[at] + 1
|
|
491
|
+
return
|
|
492
|
+
}
|
|
493
|
+
if (this.counts[at] > 0) {
|
|
494
|
+
this.fail(FLAG_ERR_REPEATED, `${flag} is given twice`, arg)
|
|
495
|
+
return
|
|
496
|
+
}
|
|
497
|
+
if (kind === FLAG_NUMBER) {
|
|
498
|
+
// `n - n` is zero exactly when `n` is finite: a decimal too large for
|
|
499
|
+
// an f64 (`1e999`) reads as Infinity, which is not a count of anything.
|
|
500
|
+
const decimal = isDecimal(value)
|
|
501
|
+
const n: f64 = decimal ? Number(value) : toF64(0)
|
|
502
|
+
if (!decimal || n - n !== 0) {
|
|
503
|
+
this.fail(FLAG_ERR_NOT_A_NUMBER, `${flag} needs a number, not "${value}"`, value)
|
|
504
|
+
return
|
|
505
|
+
}
|
|
506
|
+
this.numbers[at] = n
|
|
507
|
+
}
|
|
508
|
+
this.texts[at] = value
|
|
509
|
+
this.counts[at] = 1
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/** The index of the flag `name` of `kind`; a name the table does not declare so is a bug, and panics. */
|
|
513
|
+
slot(name: string, kind: i32): i32 {
|
|
514
|
+
const at = this.table.indexOf(name)
|
|
515
|
+
if (at < 0 || this.table.flags[at].kind !== kind) {
|
|
516
|
+
panic(`nish/cli-flags: ${this.table.program} declares no such flag --${name} of that kind`)
|
|
517
|
+
}
|
|
518
|
+
return at
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/** A string flag's value, or `""` when it was not given. */
|
|
522
|
+
str(name: string): string {
|
|
523
|
+
return this.texts[this.slot(name, FLAG_STRING)]
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/** A number flag's value, or `0` when it was not given. */
|
|
527
|
+
num(name: string): f64 {
|
|
528
|
+
return this.numbers[this.slot(name, FLAG_NUMBER)]
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/** A boolean flag's value: true for `--name`, false for `--no-name` or when it was not given. */
|
|
532
|
+
bool(name: string): boolean {
|
|
533
|
+
return this.bools[this.slot(name, FLAG_BOOLEAN)]
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/** Every value a repeatable flag was given, in order, as a fresh array; empty when it was not given. */
|
|
537
|
+
all(name: string): string[] {
|
|
538
|
+
const out: string[] = []
|
|
539
|
+
for (const v of this.lists[this.slot(name, FLAG_REPEATABLE)]) {
|
|
540
|
+
out.push(v)
|
|
541
|
+
}
|
|
542
|
+
return out
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/** Whether the flag `name`, of any kind, was given at all (`--no-name` counts). */
|
|
546
|
+
has(name: string): boolean {
|
|
547
|
+
const at = this.table.indexOf(name)
|
|
548
|
+
if (at < 0) {
|
|
549
|
+
panic(`nish/cli-flags: ${this.table.program} declares no flag --${name}`)
|
|
550
|
+
}
|
|
551
|
+
return this.counts[at] > 0
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* The usage line the error's hint shows: the matched command's path and
|
|
556
|
+
* shape, or `<command>` when the table has commands and none matched, then
|
|
557
|
+
* `[flags]`.
|
|
558
|
+
*/
|
|
559
|
+
usageLine(): string {
|
|
560
|
+
const table = this.table
|
|
561
|
+
if (this.commandAt >= 0) {
|
|
562
|
+
const c = table.commands[this.commandAt]
|
|
563
|
+
const shape = c.shape.length > 0 ? ` ${c.shape}` : ""
|
|
564
|
+
return `usage: ${table.program} ${c.path}${shape} [flags]`
|
|
565
|
+
}
|
|
566
|
+
if (table.commands.length > 0) {
|
|
567
|
+
return `usage: ${table.program} <command> [flags]`
|
|
568
|
+
}
|
|
569
|
+
return `usage: ${table.program} [flags]`
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/** What `exitUsage` prints: `<program>: <message>` and the usage line, each ending in a newline. */
|
|
573
|
+
usageError(): string {
|
|
574
|
+
return `${this.table.program}: ${this.message}\n${this.usageLine()}\n`
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/** The usage error on stderr, then exit status 2. On a parse that succeeded it panics: there is no error to report. */
|
|
578
|
+
exitUsage(): void {
|
|
579
|
+
if (this.ok) {
|
|
580
|
+
panic(`nish/cli-flags: ${this.table.program} calls exitUsage on a parse that succeeded`)
|
|
581
|
+
}
|
|
582
|
+
writeError(this.usageError())
|
|
583
|
+
process.exit(2)
|
|
584
|
+
}
|
|
585
|
+
}
|