@thi.ng/args 2.2.45 → 2.3.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/CHANGELOG.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Change Log
2
2
 
3
- - **Last updated**: 2023-12-09T19:12:03Z
3
+ - **Last updated**: 2023-12-18T13:41:19Z
4
4
  - **Generator**: [thi.ng/monopub](https://thi.ng/monopub)
5
5
 
6
6
  All notable changes to this project will be documented in this file.
@@ -9,6 +9,24 @@ See [Conventional Commits](https://conventionalcommits.org/) for commit guidelin
9
9
  **Note:** Unlisted _patch_ versions only involve non-code or otherwise excluded changes
10
10
  and/or version bumps of transitive dependencies.
11
11
 
12
+ ## [2.3.0](https://github.com/thi-ng/umbrella/tree/@thi.ng/args@2.3.0) (2023-12-18)
13
+
14
+ #### 🚀 Features
15
+
16
+ - add cliApp() runner ([b2248fa](https://github.com/thi-ng/umbrella/commit/b2248fa))
17
+ - update lifecycle hooks, add NO_COLOR support, add docs ([4a0ebda](https://github.com/thi-ng/umbrella/commit/4a0ebda))
18
+ - add CLIAppConfig pre/post lifecycle hooks
19
+ - update UsageOpts.color handling
20
+ - add `NO_COLOR` env var support in cliApp()
21
+ - add doc strings
22
+ - update deps
23
+ - update cliApp() to support command context extensions ([61d9fb8](https://github.com/thi-ng/umbrella/commit/61d9fb8))
24
+ - update cliApp() error handling ([019e5a1](https://github.com/thi-ng/umbrella/commit/019e5a1))
25
+ - update argv handling in cliApp() ([b1ed768](https://github.com/thi-ng/umbrella/commit/b1ed768))
26
+ - add NO_COLOR aware formatters to CommandCtx ([0e7ddda](https://github.com/thi-ng/umbrella/commit/0e7ddda))
27
+ - update deps
28
+ - update cliApp() to use StreamLogger (target: process.stderr) ([b249295](https://github.com/thi-ng/umbrella/commit/b249295))
29
+
12
30
  ### [2.2.28](https://github.com/thi-ng/umbrella/tree/@thi.ng/args@2.2.28) (2023-08-04)
13
31
 
14
32
  #### ♻️ Refactoring
package/README.md CHANGED
@@ -69,14 +69,16 @@ For Node.js REPL:
69
69
  const args = await import("@thi.ng/args");
70
70
  ```
71
71
 
72
- Package sizes (brotli'd, pre-treeshake): ESM: 2.39 KB
72
+ Package sizes (brotli'd, pre-treeshake): ESM: 2.75 KB
73
73
 
74
74
  ## Dependencies
75
75
 
76
76
  - [@thi.ng/api](https://github.com/thi-ng/umbrella/tree/develop/packages/api)
77
77
  - [@thi.ng/checks](https://github.com/thi-ng/umbrella/tree/develop/packages/checks)
78
78
  - [@thi.ng/errors](https://github.com/thi-ng/umbrella/tree/develop/packages/errors)
79
+ - [@thi.ng/logger](https://github.com/thi-ng/umbrella/tree/develop/packages/logger)
79
80
  - [@thi.ng/strings](https://github.com/thi-ng/umbrella/tree/develop/packages/strings)
81
+ - [@thi.ng/text-format](https://github.com/thi-ng/umbrella/tree/develop/packages/text-format)
80
82
 
81
83
  ## API
82
84
 
package/api.d.ts CHANGED
@@ -1,4 +1,6 @@
1
- import type { Fn, IDeref, IObjectOf } from "@thi.ng/api";
1
+ import type { Fn, Fn2, IDeref, IObjectOf } from "@thi.ng/api";
2
+ import type { ILogger } from "@thi.ng/logger";
3
+ import type { FormatPresets } from "@thi.ng/text-format";
2
4
  export interface ArgSpecBase {
3
5
  /**
4
6
  * Shorthand for given arg/option
@@ -112,9 +114,13 @@ export interface UsageOpts {
112
114
  /**
113
115
  * If false, ANSI colors will be stripped from output.
114
116
  *
117
+ * @remarks
118
+ * When using {@link cliApp}, the default for this value will depend on the
119
+ * `NO_COLOR` env var being set. See https://no-color.org/ for details.
120
+ *
115
121
  * @defaultValue true
116
122
  */
117
- color: Partial<ColorTheme> | false;
123
+ color: Partial<ColorTheme> | boolean;
118
124
  /**
119
125
  * If true (default), display argument default values. Nullish or false
120
126
  * default values will never be shown.
@@ -172,4 +178,95 @@ export declare class Tuple<T> implements IDeref<T[]> {
172
178
  constructor(value: T[]);
173
179
  deref(): T[];
174
180
  }
181
+ export interface CLIAppConfig<OPTS extends object, CTX extends CommandCtx<OPTS, OPTS> = CommandCtx<OPTS, OPTS>> {
182
+ /**
183
+ * App (CLI command) short name.
184
+ */
185
+ name: string;
186
+ /**
187
+ * Shared args for all commands
188
+ */
189
+ opts: Args<OPTS>;
190
+ /**
191
+ * Command spec registry
192
+ */
193
+ commands: IObjectOf<Command<any, OPTS, CTX>>;
194
+ /**
195
+ * If true, the app will only use the single command entry in
196
+ * {@link CLIAppConfig.commands} and not expect the first CLI args to be a
197
+ * command name.
198
+ */
199
+ single?: boolean;
200
+ /**
201
+ * Usage options, same as {@link UsageOpts}. Usage will be shown
202
+ * automatically in case of arg parse errors.
203
+ */
204
+ usage: Partial<UsageOpts>;
205
+ /**
206
+ * Arguments vector to use for arg parsing. If omitted, uses `process.argv`
207
+ */
208
+ argv?: string[];
209
+ /**
210
+ * {@link CLIAppConfig.argv} index to start parsing from.
211
+ *
212
+ * @defaultValue 2
213
+ */
214
+ start?: number;
215
+ /**
216
+ * {@link CommandCtx} augmentation handler, i.e. an async function which
217
+ * will be called just before the actual command for additional setup/config
218
+ * purposes. The context object returned will be the one passed to the
219
+ * command.
220
+ */
221
+ ctx: Fn2<CommandCtx<OPTS, OPTS>, Command<any, OPTS, CTX>, Promise<CTX>>;
222
+ /**
223
+ * Lifecycle hook. Function which will be called just after the actual
224
+ * command handler, e.g. for teardown purposes.
225
+ */
226
+ post?: Fn2<CTX, Command<any, OPTS, CTX>, Promise<void>>;
227
+ }
228
+ export interface Command<OPTS extends BASE, BASE extends object, CTX extends CommandCtx<OPTS, BASE> = CommandCtx<OPTS, BASE>> {
229
+ /**
230
+ * Command description (short, single line)
231
+ */
232
+ desc: string;
233
+ /**
234
+ * Command specific CLI arg specs
235
+ */
236
+ opts: Args<Omit<OPTS, keyof BASE>>;
237
+ /**
238
+ * Number of required rest input value (after all parsed options). Leave
239
+ * unset to allow any number.
240
+ */
241
+ inputs?: number;
242
+ /**
243
+ * Actual command function/implementation.
244
+ */
245
+ fn: Fn<CTX, Promise<void>>;
246
+ }
247
+ export interface CommandCtx<OPTS extends BASE, BASE extends object> {
248
+ /**
249
+ * Logger to be used by all commands. By default uses a console logger with
250
+ * log level INFO. Can be customized via {@link CLIAppConfig.pre}.
251
+ */
252
+ logger: ILogger;
253
+ /**
254
+ * `NO_COLOR`-aware text formatting presets. If color output is NOT disabled
255
+ * via the `NO_COLOR` env var, this defaults to
256
+ * [`PRESET_ANSI16`](https://github.com/thi-ng/umbrella/blob/develop/packages/text-format/README.md),
257
+ * otherwise `PRESET_NONE` (i.e. same API, but ignoring any color requests).
258
+ *
259
+ * See https://no-color.org for context.
260
+ */
261
+ format: FormatPresets;
262
+ /**
263
+ * Parsed CLI args (according to provided command spec)
264
+ */
265
+ opts: OPTS;
266
+ /**
267
+ * Array of remaining CLI args (after parsed options). Individual commands
268
+ * can specify the number of items required via {@link Command.inputs}.
269
+ */
270
+ inputs: string[];
271
+ }
175
272
  //# sourceMappingURL=api.d.ts.map
package/api.js CHANGED
@@ -1,19 +1,19 @@
1
- export const DEFAULT_THEME = {
2
- default: 95,
3
- hint: 90,
4
- multi: 90,
5
- param: 96,
6
- required: 33,
1
+ const DEFAULT_THEME = {
2
+ default: 95,
3
+ hint: 90,
4
+ multi: 90,
5
+ param: 96,
6
+ required: 33
7
7
  };
8
- /**
9
- * Wrapper for fixed size tuple values produced by {@link tuple}.
10
- */
11
- export class Tuple {
12
- value;
13
- constructor(value) {
14
- this.value = value;
15
- }
16
- deref() {
17
- return this.value;
18
- }
8
+ class Tuple {
9
+ constructor(value) {
10
+ this.value = value;
11
+ }
12
+ deref() {
13
+ return this.value;
14
+ }
19
15
  }
16
+ export {
17
+ DEFAULT_THEME,
18
+ Tuple
19
+ };
package/args.js CHANGED
@@ -1,214 +1,105 @@
1
1
  import { identity } from "@thi.ng/api/fn";
2
2
  import { repeat } from "@thi.ng/strings/repeat";
3
- import { coerceFloat, coerceFloats, coerceHexInt, coerceHexInts, coerceInt, coerceInts, coerceJson, coerceKV, coerceOneOf, coerceTuple, } from "./coerce.js";
3
+ import {
4
+ coerceFloat,
5
+ coerceFloats,
6
+ coerceHexInt,
7
+ coerceHexInts,
8
+ coerceInt,
9
+ coerceInts,
10
+ coerceJson,
11
+ coerceKV,
12
+ coerceOneOf,
13
+ coerceTuple
14
+ } from "./coerce.js";
4
15
  const $single = (coerce, hint) => (spec) => ({
5
- coerce,
6
- hint,
7
- group: "main",
8
- ...spec,
16
+ coerce,
17
+ hint,
18
+ group: "main",
19
+ ...spec
9
20
  });
10
21
  const $multi = (coerce, hint) => (spec) => ({
11
- hint: $hint(hint, spec.delim),
12
- multi: true,
13
- coerce,
14
- group: "main",
15
- ...spec,
22
+ hint: $hint(hint, spec.delim),
23
+ multi: true,
24
+ coerce,
25
+ group: "main",
26
+ ...spec
16
27
  });
17
28
  const $hint = (hint, delim) => hint + (delim ? `[${delim}..]` : "");
18
- /**
19
- * Returns a full {@link ArgSpec} for a boolean flag. The mere presence of this
20
- * arg will enable the flag.
21
- *
22
- * @param spec -
23
- */
24
- export const flag = (spec) => ({
25
- flag: true,
26
- default: false,
27
- group: "flags",
28
- ...spec,
29
+ const flag = (spec) => ({
30
+ flag: true,
31
+ default: false,
32
+ group: "flags",
33
+ ...spec
29
34
  });
30
- /**
31
- * Returns a full {@link ArgSpec} for a string value arg.
32
- *
33
- * @param spec -
34
- */
35
- export const string = $single(identity, "STR");
36
- /**
37
- * Multi-arg version of {@link string}. Returns a full {@link ArgSpec} for a
38
- * multi string value arg. This argument can be provided mutiple times with
39
- * values collected into an array.
40
- *
41
- * @param spec -
42
- */
43
- export const strings = $multi(identity, "STR");
44
- /**
45
- * Returns a full {@link ArgSpec} for a floating point value arg. The value
46
- * will be autoatically coerced into a number using {@link coerceFloat}.
47
- *
48
- * @param spec -
49
- */
50
- export const float = $single(coerceFloat, "NUM");
51
- /**
52
- * Returns a full {@link ArgSpec} for a single hex integer value arg. The value
53
- * will be autoatically coerced into a number using {@link coerceHexInt}.
54
- *
55
- * @param spec -
56
- */
57
- export const hex = $single(coerceHexInt, "HEX");
58
- /**
59
- * Returns a full {@link ArgSpec} for a single integer value arg. The value
60
- * will be autoatically coerced into a number using {@link coerceInt}.
61
- *
62
- * @param spec -
63
- */
64
- export const int = $single(coerceInt, "INT");
65
- /**
66
- * Multi-arg version of {@link float}. Returns a full {@link ArgSpec} for a
67
- * multi floating point value arg. This argument can be provided mutiple times
68
- * with values being coerced into numbers and collected into an array.
69
- *
70
- * @param spec -
71
- */
72
- export const floats = $multi(coerceFloats, "NUM");
73
- /**
74
- * Multi-arg version of {@link hex}. Returns a full {@link ArgSpec} for a multi
75
- * hex integer value arg. This argument can be provided mutiple times with
76
- * values being coerced into numbers and collected into an array.
77
- *
78
- * @param spec -
79
- */
80
- export const hexes = $multi(coerceHexInts, "HEX");
81
- /**
82
- * Multi-arg version of {@link int}. Returns a full {@link ArgSpec} for a multi
83
- * integer value arg. This argument can be provided mutiple times with values
84
- * being coerced into numbers and collected into an array.
85
- *
86
- * @param spec -
87
- */
88
- export const ints = $multi(coerceInts, "INT");
89
- /**
90
- * Returns full {@link ArgSpec} for a JSON value arg. The raw CLI value string
91
- * will be automcatically coerced using {@link coerceJson}.
92
- *
93
- * @param spec -
94
- */
95
- export const json = (spec) => ({
96
- coerce: coerceJson,
97
- hint: "JSON",
98
- group: "main",
99
- ...spec,
35
+ const string = $single(identity, "STR");
36
+ const strings = $multi(identity, "STR");
37
+ const float = $single(coerceFloat, "NUM");
38
+ const hex = $single(coerceHexInt, "HEX");
39
+ const int = $single(coerceInt, "INT");
40
+ const floats = $multi(coerceFloats, "NUM");
41
+ const hexes = $multi(coerceHexInts, "HEX");
42
+ const ints = $multi(coerceInts, "INT");
43
+ const json = (spec) => ({
44
+ coerce: coerceJson,
45
+ hint: "JSON",
46
+ group: "main",
47
+ ...spec
100
48
  });
101
49
  const $desc = (opts, prefix) => `${prefix ? prefix + ": " : ""}${opts.map((x) => `"${x}"`).join(", ")}`;
102
- /**
103
- * Returns full {@link ArgSpec} for an enum-like string value arg. The raw CLI
104
- * value string will be automcatically validated using {@link coerceOneOf}.
105
- *
106
- * @param opts -
107
- * @param spec -
108
- */
109
- export const oneOf = (opts, spec) => ({
110
- coerce: coerceOneOf(opts),
111
- hint: "ID",
112
- group: "main",
113
- ...spec,
114
- desc: $desc(opts, spec.desc),
50
+ const oneOf = (opts, spec) => ({
51
+ coerce: coerceOneOf(opts),
52
+ hint: "ID",
53
+ group: "main",
54
+ ...spec,
55
+ desc: $desc(opts, spec.desc)
115
56
  });
116
- /**
117
- * Multi-arg version of {@link oneOf}. Returns full {@link ArgSpec} for multiple
118
- * enum-like string value args. The raw CLI value strings will be automcatically
119
- * validated using {@link coerceOneOf} and collected into an array.
120
- *
121
- * @param opts -
122
- * @param spec -
123
- */
124
- export const oneOfMulti = (opts, spec) => ({
125
- coerce: (xs) => xs.map(coerceOneOf(opts)),
126
- hint: $hint("ID", spec.delim),
127
- multi: true,
128
- group: "main",
129
- ...spec,
130
- desc: $desc(opts, spec.desc),
57
+ const oneOfMulti = (opts, spec) => ({
58
+ coerce: (xs) => xs.map(coerceOneOf(opts)),
59
+ hint: $hint("ID", spec.delim),
60
+ multi: true,
61
+ group: "main",
62
+ ...spec,
63
+ desc: $desc(opts, spec.desc)
131
64
  });
132
- /**
133
- * Returns a full {@link ArgSpec} for multiple `key=value` pair args, coerced
134
- * into a result object.
135
- *
136
- * @remarks
137
- * The default delimiter (`=`) can be overridden. Also by default, key-only args
138
- * are allowed and will receive a `"true"` as their value. However, if `strict`
139
- * is true, only full KV pairs are allowed.
140
- *
141
- * @param spec -
142
- * @param delim -
143
- */
144
- export const kvPairs = (spec, delim = "=", strict) => ({
145
- coerce: coerceKV(delim, strict),
146
- hint: `key${delim}val`,
147
- multi: true,
148
- group: "main",
149
- ...spec,
65
+ const kvPairs = (spec, delim = "=", strict) => ({
66
+ coerce: coerceKV(delim, strict),
67
+ hint: `key${delim}val`,
68
+ multi: true,
69
+ group: "main",
70
+ ...spec
150
71
  });
151
- /**
152
- * Like {@link kvPairs}, but coerces KV pairs into a result {@link KVMultiDict}
153
- * which supports multiple values per given key (each key's values are collected
154
- * into arrays).
155
- *
156
- * @param spec -
157
- * @param delim -
158
- * @param strict -
159
- */
160
- export const kvPairsMulti = (spec, delim = "=", strict) => ({
161
- coerce: coerceKV(delim, strict, true),
162
- hint: `key${delim}val(s)`,
163
- multi: true,
164
- group: "main",
165
- ...spec,
72
+ const kvPairsMulti = (spec, delim = "=", strict) => ({
73
+ coerce: coerceKV(delim, strict, true),
74
+ hint: `key${delim}val(s)`,
75
+ multi: true,
76
+ group: "main",
77
+ ...spec
166
78
  });
167
- /**
168
- * Returns a full {@link ArgSpec} for a fixed `size` tuple extracted from a
169
- * single value string. The individual values are delimited by `delim` and will
170
- * be coerced into their target type via `coerce`. The result tuple will be
171
- * wrapped in a {@link Tuple} instance.
172
- *
173
- * @remarks
174
- * An error will be thrown if the number of extracted values differs from the
175
- * specified tuple size or any value coercion fails.
176
- *
177
- * @example
178
- * ```ts
179
- * parse({ a: tuple(coerceInt, 2, {})}, ["--a", "1,2"])
180
- * // {
181
- * // result: { a: Tuple { value: [1, 2] } },
182
- * // index: 2,
183
- * // rest: [],
184
- * // done: true
185
- * // }
186
- * ```
187
- *
188
- * @param coerce -
189
- * @param size -
190
- * @param spec -
191
- * @param delim -
192
- */
193
- export const tuple = (coerce, size, spec, delim = ",") => ({
194
- coerce: coerceTuple(coerce, size, delim),
195
- hint: [...repeat("N", size)].join(delim),
196
- group: "main",
197
- ...spec,
79
+ const tuple = (coerce, size2, spec, delim = ",") => ({
80
+ coerce: coerceTuple(coerce, size2, delim),
81
+ hint: [...repeat("N", size2)].join(delim),
82
+ group: "main",
83
+ ...spec
198
84
  });
199
- /**
200
- * Syntax sugar for `tuple(coerceInt, size, {...}, delim)`.
201
- *
202
- * @param size -
203
- * @param spec -
204
- * @param delim -
205
- */
206
- export const size = (size, spec, delim = "x") => tuple(coerceInt, size, spec, delim);
207
- /**
208
- * Syntax sugar for `tuple(coerceFloat, size, {...}, delim)`.
209
- *
210
- * @param size -
211
- * @param spec -
212
- * @param delim -
213
- */
214
- export const vec = (size, spec, delim = ",") => tuple(coerceFloat, size, spec, delim);
85
+ const size = (size2, spec, delim = "x") => tuple(coerceInt, size2, spec, delim);
86
+ const vec = (size2, spec, delim = ",") => tuple(coerceFloat, size2, spec, delim);
87
+ export {
88
+ flag,
89
+ float,
90
+ floats,
91
+ hex,
92
+ hexes,
93
+ int,
94
+ ints,
95
+ json,
96
+ kvPairs,
97
+ kvPairsMulti,
98
+ oneOf,
99
+ oneOfMulti,
100
+ size,
101
+ string,
102
+ strings,
103
+ tuple,
104
+ vec
105
+ };
package/cli.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ import type { CLIAppConfig, CommandCtx } from "./api.js";
2
+ export declare const cliApp: <OPTS extends object, CTX extends CommandCtx<OPTS, OPTS>>(config: CLIAppConfig<OPTS, CTX>) => Promise<void>;
3
+ //# sourceMappingURL=cli.d.ts.map
package/cli.js ADDED
@@ -0,0 +1,78 @@
1
+ import { illegalArgs } from "@thi.ng/errors/illegal-arguments";
2
+ import { StreamLogger } from "@thi.ng/logger/stream";
3
+ import { padRight } from "@thi.ng/strings/pad-right";
4
+ import { PRESET_ANSI16, PRESET_NONE } from "@thi.ng/text-format/presets";
5
+ import { parse } from "./parse.js";
6
+ import { usage } from "./usage.js";
7
+ const cliApp = async (config) => {
8
+ const argv = config.argv || process.argv;
9
+ const isColor = !process.env.NO_COLOR;
10
+ const usageOpts = {
11
+ prefix: "",
12
+ color: isColor,
13
+ ...config.usage
14
+ };
15
+ try {
16
+ let cmdID;
17
+ let cmd;
18
+ let start = config.start ?? 2;
19
+ if (config.single) {
20
+ cmdID = Object.keys(config.commands)[0];
21
+ if (!cmdID)
22
+ illegalArgs("no command provided");
23
+ cmd = config.commands[cmdID];
24
+ } else {
25
+ cmdID = argv[start];
26
+ cmd = config.commands[cmdID];
27
+ usageOpts.prefix += __descriptions(config.commands);
28
+ if (!cmd)
29
+ __usageAndExit(config, usageOpts);
30
+ start++;
31
+ }
32
+ let parsed;
33
+ try {
34
+ parsed = parse({ ...config.opts, ...cmd.opts }, argv, {
35
+ showUsage: true,
36
+ usageOpts,
37
+ start
38
+ });
39
+ } catch (_) {
40
+ }
41
+ if (!parsed)
42
+ process.exit(1);
43
+ if (cmd.inputs !== void 0 && cmd.inputs !== parsed.rest.length) {
44
+ process.stderr.write(`expected ${cmd.inputs || 0} input(s)
45
+ `);
46
+ __usageAndExit(config, usageOpts);
47
+ }
48
+ const ctx = await config.ctx(
49
+ {
50
+ logger: new StreamLogger(process.stderr, config.name, "INFO"),
51
+ format: isColor ? PRESET_ANSI16 : PRESET_NONE,
52
+ opts: parsed.result,
53
+ inputs: parsed.rest
54
+ },
55
+ cmd
56
+ );
57
+ await cmd.fn(ctx);
58
+ if (config.post)
59
+ await config.post(ctx, cmd);
60
+ } catch (e) {
61
+ process.stderr.write(e.message + "\n\n");
62
+ process.exit(1);
63
+ }
64
+ };
65
+ const __usageAndExit = (config, usageOpts) => {
66
+ process.stderr.write(usage(config.opts, usageOpts));
67
+ process.exit(1);
68
+ };
69
+ const __descriptions = (commands) => [
70
+ "\nAvailable commands:\n",
71
+ ...Object.keys(commands).map(
72
+ (x) => `${padRight(16)(x)}: ${commands[x].desc}`
73
+ ),
74
+ "\n"
75
+ ].join("\n");
76
+ export {
77
+ cliApp
78
+ };
package/coerce.js CHANGED
@@ -2,42 +2,50 @@ import { isHex } from "@thi.ng/checks/is-hex";
2
2
  import { isNumericFloat, isNumericInt } from "@thi.ng/checks/is-numeric";
3
3
  import { illegalArgs } from "@thi.ng/errors/illegal-arguments";
4
4
  import { Tuple } from "./api.js";
5
- export const coerceString = (x) => x;
6
- export const coerceFloat = (x) => isNumericFloat(x)
7
- ? parseFloat(x)
8
- : illegalArgs(`not a numeric value: ${x}`);
9
- export const coerceFloats = (xs) => xs.map(coerceFloat);
10
- export const coerceHexInt = (x) => isHex(x) ? parseInt(x, 16) : illegalArgs(`not a hex value: ${x}`);
11
- export const coerceHexInts = (xs) => xs.map(coerceHexInt);
12
- export const coerceInt = (x) => isNumericInt(x) ? parseInt(x) : illegalArgs(`not an integer: ${x}`);
13
- export const coerceInts = (xs) => xs.map(coerceInt);
14
- export const coerceJson = (x) => JSON.parse(x);
15
- export const coerceOneOf = (xs) => (x) => xs.includes(x) ? x : illegalArgs(`invalid option: ${x}`);
16
- export function coerceKV(delim = "=", strict = false, multi = false) {
17
- return (pairs) => pairs.reduce((acc, x) => {
18
- const idx = x.indexOf(delim);
19
- strict &&
20
- idx < 1 &&
21
- illegalArgs(`got '${x}', but expected a 'key${delim}value' pair`);
22
- if (idx > 0) {
23
- const id = x.substring(0, idx);
24
- const val = x.substring(idx + 1);
25
- if (multi) {
26
- acc[id] ? acc[id].push(val) : (acc[id] = [val]);
27
- }
28
- else {
29
- acc[id] = val;
30
- }
31
- }
32
- else {
33
- acc[x] = multi ? ["true"] : "true";
34
- }
35
- return acc;
36
- }, {});
5
+ const coerceString = (x) => x;
6
+ const coerceFloat = (x) => isNumericFloat(x) ? parseFloat(x) : illegalArgs(`not a numeric value: ${x}`);
7
+ const coerceFloats = (xs) => xs.map(coerceFloat);
8
+ const coerceHexInt = (x) => isHex(x) ? parseInt(x, 16) : illegalArgs(`not a hex value: ${x}`);
9
+ const coerceHexInts = (xs) => xs.map(coerceHexInt);
10
+ const coerceInt = (x) => isNumericInt(x) ? parseInt(x) : illegalArgs(`not an integer: ${x}`);
11
+ const coerceInts = (xs) => xs.map(coerceInt);
12
+ const coerceJson = (x) => JSON.parse(x);
13
+ const coerceOneOf = (xs) => (x) => xs.includes(x) ? x : illegalArgs(`invalid option: ${x}`);
14
+ function coerceKV(delim = "=", strict = false, multi = false) {
15
+ return (pairs) => pairs.reduce((acc, x) => {
16
+ const idx = x.indexOf(delim);
17
+ strict && idx < 1 && illegalArgs(
18
+ `got '${x}', but expected a 'key${delim}value' pair`
19
+ );
20
+ if (idx > 0) {
21
+ const id = x.substring(0, idx);
22
+ const val = x.substring(idx + 1);
23
+ if (multi) {
24
+ acc[id] ? acc[id].push(val) : acc[id] = [val];
25
+ } else {
26
+ acc[id] = val;
27
+ }
28
+ } else {
29
+ acc[x] = multi ? ["true"] : "true";
30
+ }
31
+ return acc;
32
+ }, {});
37
33
  }
38
- export const coerceTuple = (coerce, size, delim = ",") => (src) => {
39
- const parts = src.split(delim);
40
- parts.length !== size &&
41
- illegalArgs(`got '${src}', but expected a tuple of ${size} values`);
42
- return new Tuple(parts.map(coerce));
34
+ const coerceTuple = (coerce, size, delim = ",") => (src) => {
35
+ const parts = src.split(delim);
36
+ parts.length !== size && illegalArgs(`got '${src}', but expected a tuple of ${size} values`);
37
+ return new Tuple(parts.map(coerce));
38
+ };
39
+ export {
40
+ coerceFloat,
41
+ coerceFloats,
42
+ coerceHexInt,
43
+ coerceHexInts,
44
+ coerceInt,
45
+ coerceInts,
46
+ coerceJson,
47
+ coerceKV,
48
+ coerceOneOf,
49
+ coerceString,
50
+ coerceTuple
43
51
  };
package/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export * from "./api.js";
2
2
  export * from "./args.js";
3
+ export * from "./cli.js";
3
4
  export * from "./coerce.js";
4
5
  export * from "./parse.js";
5
6
  export * from "./usage.js";
package/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  export * from "./api.js";
2
2
  export * from "./args.js";
3
+ export * from "./cli.js";
3
4
  export * from "./coerce.js";
4
5
  export * from "./parse.js";
5
6
  export * from "./usage.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thi.ng/args",
3
- "version": "2.2.45",
3
+ "version": "2.3.0",
4
4
  "description": "Declarative, functional & typechecked CLI argument/options parser, value coercions etc.",
5
5
  "type": "module",
6
6
  "module": "./index.js",
@@ -24,7 +24,9 @@
24
24
  "author": "Karsten Schmidt (https://thi.ng)",
25
25
  "license": "Apache-2.0",
26
26
  "scripts": {
27
- "build": "yarn clean && tsc --declaration",
27
+ "build": "yarn build:esbuild && yarn build:decl",
28
+ "build:decl": "tsc --declaration --emitDeclarationOnly",
29
+ "build:esbuild": "esbuild --format=esm --platform=neutral --target=es2022 --tsconfig=tsconfig.json --outdir=. src/**/*.ts",
28
30
  "clean": "rimraf --glob '*.js' '*.d.ts' '*.map' doc",
29
31
  "doc": "typedoc --excludePrivate --excludeInternal --out doc src/index.ts",
30
32
  "doc:ae": "mkdir -p .ae/doc .ae/temp && api-extractor run --local --verbose",
@@ -33,13 +35,16 @@
33
35
  "test": "bun test"
34
36
  },
35
37
  "dependencies": {
36
- "@thi.ng/api": "^8.9.11",
37
- "@thi.ng/checks": "^3.4.11",
38
- "@thi.ng/errors": "^2.4.5",
39
- "@thi.ng/strings": "^3.7.2"
38
+ "@thi.ng/api": "^8.9.13",
39
+ "@thi.ng/checks": "^3.4.13",
40
+ "@thi.ng/errors": "^2.4.7",
41
+ "@thi.ng/logger": "^2.1.0",
42
+ "@thi.ng/strings": "^3.7.4",
43
+ "@thi.ng/text-format": "^2.0.0"
40
44
  },
41
45
  "devDependencies": {
42
46
  "@microsoft/api-extractor": "^7.38.3",
47
+ "esbuild": "^0.19.8",
43
48
  "rimraf": "^5.0.5",
44
49
  "tools": "^0.0.1",
45
50
  "typedoc": "^0.25.4",
@@ -55,6 +60,7 @@
55
60
  "declarative",
56
61
  "functional",
57
62
  "hex",
63
+ "logger",
58
64
  "nodejs",
59
65
  "parser",
60
66
  "tuple",
@@ -65,7 +71,7 @@
65
71
  "access": "public"
66
72
  },
67
73
  "engines": {
68
- "node": ">=12.7"
74
+ "node": ">=18"
69
75
  },
70
76
  "files": [
71
77
  "./*.js",
@@ -81,6 +87,9 @@
81
87
  "./args": {
82
88
  "default": "./args.js"
83
89
  },
90
+ "./cli": {
91
+ "default": "./cli.js"
92
+ },
84
93
  "./coerce": {
85
94
  "default": "./coerce.js"
86
95
  },
@@ -94,5 +103,5 @@
94
103
  "thi.ng": {
95
104
  "year": 2018
96
105
  },
97
- "gitHead": "25f2ac8ff795a432a930119661b364d4d93b59a0\n"
106
+ "gitHead": "25a42a81fac8603a1e440a7aa8bc343276211ff4\n"
98
107
  }
package/parse.js CHANGED
@@ -3,118 +3,120 @@ import { defError } from "@thi.ng/errors/deferror";
3
3
  import { illegalArgs } from "@thi.ng/errors/illegal-arguments";
4
4
  import { camel } from "@thi.ng/strings/case";
5
5
  import { usage } from "./usage.js";
6
- export const ParseError = defError(() => "parse error");
7
- export const parse = (specs, argv, opts) => {
8
- opts = { start: 2, showUsage: true, help: ["--help", "-h"], ...opts };
9
- try {
10
- return parseOpts(specs, argv, opts);
11
- }
12
- catch (e) {
13
- if (opts.showUsage) {
14
- console.log(e.message + "\n\n" + usage(specs, opts.usageOpts));
15
- }
16
- throw new ParseError(e.message);
6
+ const ParseError = defError(() => "parse error");
7
+ const parse = (specs, argv, opts) => {
8
+ opts = { start: 2, showUsage: true, help: ["--help", "-h"], ...opts };
9
+ try {
10
+ return parseOpts(specs, argv, opts);
11
+ } catch (e) {
12
+ if (opts.showUsage) {
13
+ console.log(
14
+ e.message + "\n\n" + usage(specs, opts.usageOpts)
15
+ );
17
16
  }
17
+ throw new ParseError(e.message);
18
+ }
18
19
  };
19
20
  const parseOpts = (specs, argv, opts) => {
20
- const aliases = aliasIndex(specs);
21
- const acc = {};
22
- let id;
23
- let spec;
24
- let i = opts.start;
25
- for (; i < argv.length;) {
26
- const a = argv[i];
27
- if (!id) {
28
- if (opts.help.includes(a)) {
29
- console.log(usage(specs, opts.usageOpts));
30
- return;
31
- }
32
- const state = parseKey(specs, aliases, acc, a);
33
- id = state.id;
34
- spec = state.spec;
35
- i = i + ~~(state.state < 2);
36
- if (state.state)
37
- break;
38
- }
39
- else {
40
- if (parseValue(spec, acc, id, a))
41
- break;
42
- id = null;
43
- i++;
44
- }
21
+ const aliases = aliasIndex(specs);
22
+ const acc = {};
23
+ let id;
24
+ let spec;
25
+ let i = opts.start;
26
+ for (; i < argv.length; ) {
27
+ const a = argv[i];
28
+ if (!id) {
29
+ if (opts.help.includes(a)) {
30
+ console.log(usage(specs, opts.usageOpts));
31
+ return;
32
+ }
33
+ const state = parseKey(specs, aliases, acc, a);
34
+ id = state.id;
35
+ spec = state.spec;
36
+ i = i + ~~(state.state < 2);
37
+ if (state.state)
38
+ break;
39
+ } else {
40
+ if (parseValue(spec, acc, id, a))
41
+ break;
42
+ id = null;
43
+ i++;
45
44
  }
46
- id && illegalArgs(`missing value for: --${id}`);
47
- return {
48
- result: processResults(specs, acc),
49
- index: i,
50
- rest: argv.slice(i),
51
- done: i >= argv.length,
52
- };
45
+ }
46
+ id && illegalArgs(`missing value for: --${id}`);
47
+ return {
48
+ result: processResults(specs, acc),
49
+ index: i,
50
+ rest: argv.slice(i),
51
+ done: i >= argv.length
52
+ };
53
53
  };
54
- const aliasIndex = (specs) => Object.entries(specs).reduce((acc, [k, v]) => (v.alias ? ((acc[v.alias] = k), acc) : acc), {});
54
+ const aliasIndex = (specs) => Object.entries(specs).reduce(
55
+ (acc, [k, v]) => v.alias ? (acc[v.alias] = k, acc) : acc,
56
+ {}
57
+ );
55
58
  const parseKey = (specs, aliases, acc, a) => {
56
- if (a[0] === "-") {
57
- let id;
58
- if (a[1] === "-") {
59
- // terminator arg, stop parsing
60
- if (a === "--")
61
- return { state: 1 };
62
- id = camel(a.substring(2));
63
- }
64
- else {
65
- id = aliases[a.substring(1)];
66
- !id && illegalArgs(`unknown option: ${a}`);
67
- }
68
- const spec = specs[id];
69
- !spec && illegalArgs(id);
70
- if (spec.flag) {
71
- acc[id] = true;
72
- id = undefined;
73
- // stop parsing if fn returns false
74
- if (spec.fn && !spec.fn("true"))
75
- return { state: 1, spec };
76
- }
77
- return { state: 0, id, spec };
59
+ if (a[0] === "-") {
60
+ let id;
61
+ if (a[1] === "-") {
62
+ if (a === "--")
63
+ return { state: 1 };
64
+ id = camel(a.substring(2));
65
+ } else {
66
+ id = aliases[a.substring(1)];
67
+ !id && illegalArgs(`unknown option: ${a}`);
68
+ }
69
+ const spec = specs[id];
70
+ !spec && illegalArgs(id);
71
+ if (spec.flag) {
72
+ acc[id] = true;
73
+ id = void 0;
74
+ if (spec.fn && !spec.fn("true"))
75
+ return { state: 1, spec };
78
76
  }
79
- // no option arg, stop parsing
80
- return { state: 2 };
77
+ return { state: 0, id, spec };
78
+ }
79
+ return { state: 2 };
81
80
  };
82
81
  const parseValue = (spec, acc, id, a) => {
83
- /^-[a-z]/i.test(a) && illegalArgs(`missing value for: --${id}`);
84
- if (spec.multi) {
85
- isArray(acc[id]) ? acc[id].push(a) : (acc[id] = [a]);
86
- }
87
- else {
88
- acc[id] = a;
89
- }
90
- return spec.fn && !spec.fn(a);
82
+ /^-[a-z]/i.test(a) && illegalArgs(`missing value for: --${id}`);
83
+ if (spec.multi) {
84
+ isArray(acc[id]) ? acc[id].push(a) : acc[id] = [a];
85
+ } else {
86
+ acc[id] = a;
87
+ }
88
+ return spec.fn && !spec.fn(a);
91
89
  };
92
90
  const processResults = (specs, acc) => {
93
- let spec;
94
- for (let id in specs) {
95
- spec = specs[id];
96
- if (acc[id] === undefined) {
97
- if (spec.default !== undefined) {
98
- acc[id] = spec.default;
99
- }
100
- else if (spec.optional === false) {
101
- illegalArgs(`missing arg: --${id}`);
102
- }
103
- }
104
- else if (spec.coerce) {
105
- coerceValue(spec, acc, id);
106
- }
91
+ let spec;
92
+ for (let id in specs) {
93
+ spec = specs[id];
94
+ if (acc[id] === void 0) {
95
+ if (spec.default !== void 0) {
96
+ acc[id] = spec.default;
97
+ } else if (spec.optional === false) {
98
+ illegalArgs(`missing arg: --${id}`);
99
+ }
100
+ } else if (spec.coerce) {
101
+ coerceValue(spec, acc, id);
107
102
  }
108
- return acc;
103
+ }
104
+ return acc;
109
105
  };
110
106
  const coerceValue = (spec, acc, id) => {
111
- try {
112
- if (spec.multi && spec.delim) {
113
- acc[id] = acc[id].reduce((acc, x) => (acc.push(...x.split(spec.delim)), acc), []);
114
- }
115
- acc[id] = spec.coerce(acc[id]);
116
- }
117
- catch (e) {
118
- throw new Error(`arg --${id}: ${e.message}`);
107
+ try {
108
+ if (spec.multi && spec.delim) {
109
+ acc[id] = acc[id].reduce(
110
+ (acc2, x) => (acc2.push(...x.split(spec.delim)), acc2),
111
+ []
112
+ );
119
113
  }
114
+ acc[id] = spec.coerce(acc[id]);
115
+ } catch (e) {
116
+ throw new Error(`arg --${id}: ${e.message}`);
117
+ }
118
+ };
119
+ export {
120
+ ParseError,
121
+ parse
120
122
  };
package/usage.js CHANGED
@@ -1,76 +1,76 @@
1
+ import { isPlainObject } from "@thi.ng/checks/is-plain-object";
1
2
  import { lengthAnsi } from "@thi.ng/strings/ansi";
2
3
  import { capitalize, kebab } from "@thi.ng/strings/case";
3
4
  import { padRight } from "@thi.ng/strings/pad-right";
4
5
  import { repeat } from "@thi.ng/strings/repeat";
5
6
  import { stringify } from "@thi.ng/strings/stringify";
6
7
  import { SPLIT_ANSI, wordWrapLines } from "@thi.ng/strings/word-wrap";
7
- import { DEFAULT_THEME, } from "./api.js";
8
- export const usage = (specs, opts = {}) => {
9
- opts = {
10
- lineWidth: 80,
11
- paramWidth: 32,
12
- showDefaults: true,
13
- prefix: "",
14
- suffix: "",
15
- groups: ["flags", "main"],
16
- ...opts,
17
- };
18
- const theme = opts.color !== false
19
- ? { ...DEFAULT_THEME, ...opts.color }
20
- : {};
21
- const indent = repeat(" ", opts.paramWidth);
22
- const format = (ids) => ids.map((id) => argUsage(id, specs[id], opts, theme, indent));
23
- const sortedIDs = Object.keys(specs).sort();
24
- const groups = opts.groups
25
- ? opts.groups
26
- .map((gid) => [
27
- gid,
28
- sortedIDs.filter((id) => specs[id].group === gid),
29
- ])
30
- .filter((g) => !!g[1].length)
31
- : [["options", sortedIDs]];
32
- return [
33
- ...wrap(opts.prefix, opts.lineWidth),
34
- ...groups.map(([gid, ids]) => [
35
- ...(opts.showGroupNames ? [`${capitalize(gid)}:\n`] : []),
36
- ...format(ids),
37
- "",
38
- ].join("\n")),
39
- ...wrap(opts.suffix, opts.lineWidth),
40
- ].join("\n");
8
+ import {
9
+ DEFAULT_THEME
10
+ } from "./api.js";
11
+ const usage = (specs, opts = {}) => {
12
+ opts = {
13
+ lineWidth: 80,
14
+ paramWidth: 32,
15
+ showDefaults: true,
16
+ prefix: "",
17
+ suffix: "",
18
+ groups: ["flags", "main"],
19
+ ...opts
20
+ };
21
+ const theme = isPlainObject(opts.color) ? { ...DEFAULT_THEME, ...opts.color } : opts.color ? DEFAULT_THEME : {};
22
+ const indent = repeat(" ", opts.paramWidth);
23
+ const format = (ids) => ids.map((id) => argUsage(id, specs[id], opts, theme, indent));
24
+ const sortedIDs = Object.keys(specs).sort();
25
+ const groups = opts.groups ? opts.groups.map(
26
+ (gid) => [
27
+ gid,
28
+ sortedIDs.filter((id) => specs[id].group === gid)
29
+ ]
30
+ ).filter((g) => !!g[1].length) : [["options", sortedIDs]];
31
+ return [
32
+ ...wrap(opts.prefix, opts.lineWidth),
33
+ ...groups.map(
34
+ ([gid, ids]) => [
35
+ ...opts.showGroupNames ? [`${capitalize(gid)}:
36
+ `] : [],
37
+ ...format(ids),
38
+ ""
39
+ ].join("\n")
40
+ ),
41
+ ...wrap(opts.suffix, opts.lineWidth)
42
+ ].join("\n");
41
43
  };
42
44
  const argUsage = (id, spec, opts, theme, indent) => {
43
- const hint = argHint(spec, theme);
44
- const alias = argAlias(spec, theme, hint);
45
- const name = ansi(`--${kebab(id)}`, theme.param);
46
- const params = `${alias}${name}${hint}`;
47
- const isRequired = spec.optional === false && spec.default === undefined;
48
- const prefixes = [];
49
- isRequired && prefixes.push("required");
50
- spec.multi && prefixes.push("multiple");
51
- const body = argPrefix(prefixes, theme, isRequired) +
52
- (spec.desc || "") +
53
- argDefault(spec, opts, theme);
54
- return (padRight(opts.paramWidth)(params, lengthAnsi(params)) +
55
- wrap(body, opts.lineWidth - opts.paramWidth)
56
- .map((l, i) => (i > 0 ? indent + l : l))
57
- .join("\n"));
45
+ const hint = argHint(spec, theme);
46
+ const alias = argAlias(spec, theme, hint);
47
+ const name = ansi(`--${kebab(id)}`, theme.param);
48
+ const params = `${alias}${name}${hint}`;
49
+ const isRequired = spec.optional === false && spec.default === void 0;
50
+ const prefixes = [];
51
+ isRequired && prefixes.push("required");
52
+ spec.multi && prefixes.push("multiple");
53
+ const body = argPrefix(prefixes, theme, isRequired) + (spec.desc || "") + argDefault(spec, opts, theme);
54
+ return padRight(opts.paramWidth)(params, lengthAnsi(params)) + wrap(body, opts.lineWidth - opts.paramWidth).map((l, i) => i > 0 ? indent + l : l).join("\n");
58
55
  };
59
56
  const argHint = (spec, theme) => spec.hint ? ansi(" " + spec.hint, theme.hint) : "";
60
57
  const argAlias = (spec, theme, hint) => spec.alias ? `${ansi("-" + spec.alias, theme.param)}${hint}, ` : "";
61
- const argPrefix = (prefixes, theme, isRequired) => prefixes.length
62
- ? ansi(`[${prefixes.join(", ")}] `, isRequired ? theme.required : theme.multi)
63
- : "";
64
- const argDefault = (spec, opts, theme) => opts.showDefaults && spec.default != null && spec.default !== false
65
- ? ansi(` (default: ${stringify(true)(spec.defaultHint != undefined
66
- ? spec.defaultHint
67
- : spec.default)})`, theme.default)
68
- : "";
69
- const ansi = (x, col) => col != null ? `\x1b[${col}m${x}\x1b[0m` : x;
70
- const wrap = (str, width) => str
71
- ? wordWrapLines(str, {
72
- width,
73
- splitter: SPLIT_ANSI,
74
- hard: true,
75
- })
76
- : [];
58
+ const argPrefix = (prefixes, theme, isRequired) => prefixes.length ? ansi(
59
+ `[${prefixes.join(", ")}] `,
60
+ isRequired ? theme.required : theme.multi
61
+ ) : "";
62
+ const argDefault = (spec, opts, theme) => opts.showDefaults && spec.default != null && spec.default !== false ? ansi(
63
+ ` (default: ${stringify(true)(
64
+ spec.defaultHint != void 0 ? spec.defaultHint : spec.default
65
+ )})`,
66
+ theme.default
67
+ ) : "";
68
+ const ansi = (x, col) => col != null ? `\x1B[${col}m${x}\x1B[0m` : x;
69
+ const wrap = (str, width) => str ? wordWrapLines(str, {
70
+ width,
71
+ splitter: SPLIT_ANSI,
72
+ hard: true
73
+ }) : [];
74
+ export {
75
+ usage
76
+ };