argot-cli 0.0.0-stage → 0.4.0-beta

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/src/errors.ts ADDED
@@ -0,0 +1,272 @@
1
+ /**
2
+ * Runtime error.
3
+ * Represents errors that occur while parsing input arguments.
4
+ * These errors are triggered by invalid or unexpected user input,
5
+ * such as unknown options, missing values, or values of the wrong type.
6
+ * Runtime errors are produced during the execution of the `.parse()`
7
+ * method and depend on the provided input rather than the parser
8
+ * configuration.
9
+ */
10
+ export class RuntimeError extends Error {}
11
+
12
+ /**
13
+ * Configuration error.
14
+ * Represents errors caused by an invalid parser configuration.
15
+ * These errors occur when defining or initializing the parser,
16
+ * such as using unsupported option types or referencing invalid
17
+ * alias targets.
18
+ * Configuration errors are independent of user input and are
19
+ * typically raised before any parsing takes place.
20
+ */
21
+ export class ConfigError extends Error {
22
+ private _errors: Error[] = [];
23
+
24
+ public push(error: Error): void {
25
+ this._errors.push(error);
26
+ }
27
+
28
+ get errors(): Error[] {
29
+ return this._errors;
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Invalid integer argument error.
35
+ * Raised when an option of type "int" is provided with an associated
36
+ * value that cannot be parsed as an integer.
37
+ * This error only applies when a value is present. Missing values are
38
+ * reported using NullIntError.
39
+ */
40
+ export class InvalidIntError extends RuntimeError {
41
+ public value: string;
42
+
43
+ constructor(value: string) {
44
+ super(`'${value}' is not a valid integer`);
45
+ this.value = value;
46
+ Object.freeze(this);
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Invalid float argument error.
52
+ * Raised when an option of type "float" is provided with an associated
53
+ * value that cannot be parsed as a number.
54
+ * This error only applies when a value is present. Missing values are
55
+ * reported using NullFloatError.
56
+ */
57
+ export class InvalidFloatError extends RuntimeError {
58
+ public value: string;
59
+
60
+ constructor(value: string) {
61
+ super(`'${value}' is not a valid integer`);
62
+ this.value = value;
63
+ Object.freeze(this);
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Unsafe integer error.
69
+ * Raised when a parsed integer cannot be safely represented
70
+ * by the underlying runtime.
71
+ * This error only applies to values that are successfully parsed
72
+ * as integers.
73
+ */
74
+ export class UnsafeIntegerError extends RuntimeError {
75
+ public value: number;
76
+
77
+ constructor(value: number) {
78
+ super(`${value} is not a safe integer`);
79
+ this.value = value;
80
+ Object.freeze(this);
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Missing argument error.
86
+ * Raised when an option that requires an associated value is provided
87
+ * without one.
88
+ * If the option is an alias, the error message includes both the
89
+ * alias name and its target.
90
+ */
91
+ export class NullArgError extends RuntimeError {
92
+ public option: string;
93
+ public target?: string;
94
+
95
+ constructor(name: string, target?: string) {
96
+ const msg = target
97
+ ? `option '${name}' (alias for '${target}') must take an argument`
98
+ : `option '${name}' must take an argument`;
99
+ super(msg);
100
+ this.option = name;
101
+ if (target) {
102
+ this.target = target;
103
+ }
104
+ Object.freeze(this);
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Missing integer argument error.
110
+ * Raised when an option of type "int" is provided without an
111
+ * associated value.
112
+ * This error does not cover invalid integer values. Invalid values
113
+ * are reported using InvalidIntError.
114
+ * If the option is an alias, the error message includes both the
115
+ * alias name and its target.
116
+ */
117
+ export class NullIntError extends RuntimeError {
118
+ public option: string;
119
+ public target?: string;
120
+
121
+ constructor(name: string, target?: string) {
122
+ const msg = target
123
+ ? `option '${name}' (alias for '${target}') requires an integer number argument`
124
+ : `option '${name}' requires an integer number argument`;
125
+ super(msg);
126
+ this.option = name;
127
+ if (target) {
128
+ this.target = target;
129
+ }
130
+ Object.freeze(this);
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Missing float argument error.
136
+ * Raised when an option of type "float" is provided without an
137
+ * associated value.
138
+ * This error does not cover invalid float values. Invalid values
139
+ * are reported using InvalidFloatError.
140
+ * If the option is an alias, the error message includes both the
141
+ * alias name and its target.
142
+ */
143
+ export class NullFloatError extends RuntimeError {
144
+ public option: string;
145
+ public target?: string;
146
+
147
+ constructor(name: string, target?: string) {
148
+ const msg = target
149
+ ? `option '${name}' (alias for '${target}') requires a numeric argument`
150
+ : `option '${name}' requires a numeric argument`;
151
+ super(msg);
152
+ this.option = name;
153
+ if (target) {
154
+ this.target = target;
155
+ }
156
+ Object.freeze(this);
157
+ }
158
+ }
159
+
160
+ /**
161
+ * Unknown option error.
162
+ * Raised when an input contains an option that is not defined in the
163
+ * parser configuration.
164
+ * This applies to both long and short options. The error is triggered
165
+ * as soon as the parser encounters an unrecognized option name.
166
+ */
167
+ export class UnknownOptionError extends RuntimeError {
168
+ public option: string;
169
+
170
+ constructor(name: string) {
171
+ super(`option '${name}' is not supported`);
172
+ this.option = name;
173
+ Object.freeze(this);
174
+ }
175
+ }
176
+
177
+ /**
178
+ * Invalid option type error.
179
+ * Raised when an option is declared with a type that is not supported
180
+ * by the parser.
181
+ * This is a configuration error and typically occurs during parser
182
+ * initialization rather than at parse time.
183
+ */
184
+ export class InvalidOptionTypeError extends ConfigError {
185
+ public type: string;
186
+
187
+ constructor(tag: string) {
188
+ super(`option type '${tag}' is not supported`);
189
+ this.type = tag;
190
+ Object.freeze(this);
191
+ }
192
+ }
193
+
194
+ /**
195
+ * Alias target not found error.
196
+ * Raised when an alias references a target option that does not exist
197
+ * in the parser configuration.
198
+ * This is a configuration error and indicates that the alias points
199
+ * to an undefined option.
200
+ */
201
+ export class AliasTargetNotFoundError extends ConfigError {
202
+ public option: string;
203
+ public target: string;
204
+
205
+ constructor(name: string, target: string) {
206
+ super(`target value '${target}' for option '${name}' was not found`);
207
+ this.option = name;
208
+ this.target = target;
209
+ Object.freeze(this);
210
+ }
211
+ }
212
+
213
+ /**
214
+ * Invalid alias target error.
215
+ * Raised when an alias references a target that is not a valid option
216
+ * for aliasing.
217
+ * This may occur if the target is itself an alias (when alias chaining
218
+ * is not supported) or if the target cannot accept the alias due to
219
+ * type or configuration constraints.
220
+ */
221
+ export class InvalidAliasTargetError extends ConfigError {
222
+ public option: string;
223
+ public target: string;
224
+
225
+ constructor(name: string, target: string) {
226
+ super(`cannot create an alias to another alias (${name} => ${target})`);
227
+ this.option = name;
228
+ this.target = target;
229
+ Object.freeze(this);
230
+ }
231
+ }
232
+
233
+ /**
234
+ * Missing option property error.
235
+ * Raised when an option entry in the parser configuration is missing
236
+ * one or more required properties.
237
+ * This is a configuration error and indicates that the option
238
+ * definition is incomplete or malformed.
239
+ * Implementations may include the option name and the missing
240
+ * property name to aid debugging.
241
+ */
242
+ export class MissingOptionPropertyError extends ConfigError {
243
+ public option: string;
244
+ public property: string;
245
+
246
+ constructor(option: string, property: string) {
247
+ super(`option '${option}' is missing required property '${property}'`);
248
+ this.option = option;
249
+ this.property = property;
250
+ Object.freeze(this);
251
+ }
252
+ }
253
+
254
+ /**
255
+ * Missing option type error.
256
+ * Raised when a configuration entry does not define a "type" property.
257
+ * The "type" property is required for all option entries and determines
258
+ * how the option is parsed and validated. Without it, the entry cannot
259
+ * be interpreted by the parser.
260
+ * This is a configuration error and is typically raised during
261
+ * configuration validation.
262
+ */
263
+ export class MissingOptionTypeError extends ConfigError {
264
+ public option: string;
265
+
266
+ constructor(name: string) {
267
+ const msg = `option '${name}' is missing required property 'type'`;
268
+ super(msg);
269
+ this.option = name;
270
+ Object.freeze(this);
271
+ }
272
+ }
package/src/mod.ts ADDED
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Argot CLI argument parser.
3
+ *
4
+ * This module provides a schema-based command-line argument parser.
5
+ * The parser processes an array of strings and produces a structured
6
+ * result containing options, parameters, and operands.
7
+ *
8
+ * Parsing behavior is fully determined by a configuration object.
9
+ *
10
+ * Example:
11
+ *
12
+ * ```ts
13
+ * import {
14
+ * ArgParser,
15
+ * ParserConfig,
16
+ * type ConfigEntries,
17
+ * } from '@argot-cli/argot';
18
+ *
19
+ * const entries: ConfigEntries = {
20
+ * verbose: { type: 'flag' },
21
+ * count: { type: 'int' },
22
+ * name: { type: 'text', default: 'world' },
23
+ * };
24
+ *
25
+ * const config = new ParserConfig(entries);
26
+ *
27
+ * const parser = new ArgParser(config);
28
+ *
29
+ * const result = parser.parse([
30
+ * '--verbose',
31
+ * '--count=3',
32
+ * 'mode=fast',
33
+ * 'file.txt',
34
+ * ]);
35
+ *
36
+ * console.log(result.options.get('verbose')); // true
37
+ * console.log(result.options.get('count')); // 3
38
+ * console.log(result.options.get('name')); // "world"
39
+ *
40
+ * console.log(result.parameters.get('mode')); // "fast"
41
+ *
42
+ * console.log(result.operands); // ["file.txt"]
43
+ * ```
44
+ * @module
45
+ */
46
+ export { ArgParser } from './arg_parser.ts';
47
+ export { ParserConfig } from './parser_config.ts';
48
+ export { readJSONConfig } from './read_config.ts';
49
+ export type { ConfigEntry, ConfigEntries } from './types.ts';
@@ -0,0 +1,104 @@
1
+ import type {
2
+ ConfigEntry,
3
+ ConfigEntries,
4
+ ParserConfigInput,
5
+ StrictParserOptions,
6
+ } from './types.ts';
7
+ import { validateEntries, validateEntriesAggregate } from './utils.ts';
8
+
9
+ function isObject(obj: unknown): obj is Record<string, unknown> {
10
+ if (Array.isArray(obj) || obj == null || typeof obj !== 'object')
11
+ return false;
12
+ return true;
13
+ }
14
+
15
+ /**
16
+ * Immutable configuration for the argument parser.
17
+ * The configuration defines the set of supported options and their types.
18
+ * Entries are provided as an object mapping option names to configuration
19
+ * entries.
20
+ * The configuration is normalized and validated during construction.
21
+ * The resulting object is immutable.
22
+ */
23
+ export class ParserConfig {
24
+ #entries: ConfigEntries;
25
+ #parser: StrictParserOptions;
26
+ #size: number;
27
+
28
+ constructor(input: ParserConfigInput) {
29
+ if (!isObject(input)) {
30
+ throw new TypeError('input value must be a plain object');
31
+ } else if (!isObject(input.options)) {
32
+ throw new TypeError('"options" value must be a plain object');
33
+ } else if (input.parser != null && !isObject(input.parser)) {
34
+ throw new TypeError('"parser" value must be a plain object');
35
+ }
36
+
37
+ const { options, parser } = input;
38
+
39
+ this.#parser = {
40
+ allowUnknown: parser?.allowUnknown ?? false,
41
+ parseParameters: parser?.parseParameters ?? true,
42
+ aggregateErrors: parser?.aggregateErrors ?? false,
43
+ };
44
+
45
+ this.#size = 0;
46
+ const entryMap = Object.create(null);
47
+ for (const [key, value] of Object.entries(options)) {
48
+ entryMap[key] = Object.freeze({ ...value });
49
+ this.#size += 1;
50
+ }
51
+ this.#entries = entryMap;
52
+
53
+ if (this.#parser.aggregateErrors)
54
+ validateEntriesAggregate(this.#entries);
55
+ else
56
+ validateEntries(this.#entries);
57
+ Object.freeze(this.#entries);
58
+ Object.freeze(this.#parser);
59
+ Object.freeze(this);
60
+ }
61
+
62
+ public entries(): Iterable<[string, ConfigEntry]> {
63
+ return Object.entries(this.#entries);
64
+ }
65
+
66
+ public get(key: string): ConfigEntry | undefined {
67
+ return this.#entries[key];
68
+ }
69
+
70
+ public has(key: string): boolean {
71
+ return Object.hasOwn(this.#entries, key);
72
+ }
73
+
74
+ public keys(): Iterable<string> {
75
+ return Object.keys(this.#entries);
76
+ }
77
+
78
+ public values(): Iterable<ConfigEntry> {
79
+ return Object.values(this.#entries);
80
+ }
81
+
82
+ public toJSON(): ParserConfigInput {
83
+ return {
84
+ options: this.#entries,
85
+ parser: this.#parser,
86
+ };
87
+ }
88
+
89
+ get size(): number {
90
+ return this.#size;
91
+ }
92
+
93
+ get allowUnknown(): boolean {
94
+ return this.#parser.allowUnknown;
95
+ }
96
+
97
+ get parseParameters(): boolean {
98
+ return this.#parser.parseParameters;
99
+ }
100
+
101
+ get aggregateErrors(): boolean {
102
+ return this.#parser.aggregateErrors;
103
+ }
104
+ }
@@ -0,0 +1,14 @@
1
+ import fs from 'node:fs';
2
+ import { ParserConfig } from './parser_config.ts';
3
+ import type { ParserConfigInput } from './types.ts';
4
+
5
+ /**
6
+ * Reads a parser configuration from a JSON file.
7
+ * The file must contain a valid configuration object that can be
8
+ * passed to the ParserConfig constructor.
9
+ */
10
+ export function readJSONConfig(configFile: string): ParserConfig {
11
+ const content: string = fs.readFileSync(configFile, { encoding: 'utf8' });
12
+ const configObj: ParserConfigInput = JSON.parse(content);
13
+ return new ParserConfig(configObj);
14
+ }
package/src/types.ts ADDED
@@ -0,0 +1,262 @@
1
+ /**
2
+ * Option type.
3
+ * Defines the kind of value an option accepts and how it is parsed.
4
+ */
5
+ export type OptionType =
6
+ | 'flag'
7
+ | 'text'
8
+ | 'int'
9
+ | 'float'
10
+ | 'count'
11
+ | 'list'
12
+ | 'alias';
13
+
14
+ /**
15
+ * Alias target type.
16
+ * Represents the set of option types that an alias may reference.
17
+ * Aliases cannot target other aliases.
18
+ */
19
+ export type AliasType =
20
+ | 'flag'
21
+ | 'text'
22
+ | 'int'
23
+ | 'float'
24
+ | 'count'
25
+ | 'list';
26
+
27
+ /**
28
+ * Flag option entry.
29
+ * A flag is a boolean option that is set to true when present.
30
+ */
31
+ export type FlagEntry = { type: 'flag' };
32
+
33
+ /**
34
+ * Text option entry.
35
+ * A text option accepts a string value. A default value may be
36
+ * defined for command-line options provided without an associated
37
+ * value.
38
+ */
39
+ export type TextEntry = { type: 'text'; default?: string };
40
+
41
+ /**
42
+ * Integer option entry.
43
+ * An integer option accepts a numeric value. The value must be a
44
+ * valid integer and must be a safe integer. A default value may
45
+ * be defined for command-line options provided without an
46
+ * associated value.
47
+ */
48
+ export type IntEntry = { type: 'int'; default?: number };
49
+
50
+ /**
51
+ * Float option entry.
52
+ * A float option takes any value that can be represented as a
53
+ * floating point number. Integer values are converted to floats.
54
+ */
55
+ export type FloatEntry = { type: 'float', default?: number };
56
+
57
+ /**
58
+ * Count option entry.
59
+ * A count option increments a numeric counter each time the option
60
+ * is provided.
61
+ */
62
+ export type CountEntry = { type: 'count' };
63
+
64
+ /**
65
+ * List option entry.
66
+ * A list option accepts a string value and splits it into multiple
67
+ * entries using a separator.
68
+ * The separator defaults to a comma if not specified.
69
+ */
70
+ export type ListEntry = { type: 'list'; sep?: string };
71
+
72
+ /**
73
+ * Alias option entry.
74
+ * An alias forwards its value to another option.
75
+ * The target must reference a valid, non-alias option.
76
+ */
77
+ export type AliasEntry = { type: 'alias'; target: string };
78
+
79
+ /**
80
+ * A configuration entry representing a single command-line option.
81
+ */
82
+ export type ConfigEntry = (
83
+ | FlagEntry
84
+ | TextEntry
85
+ | IntEntry
86
+ | FloatEntry
87
+ | CountEntry
88
+ | ListEntry
89
+ | AliasEntry
90
+ );
91
+
92
+ /**
93
+ * An object holding option configuration entries.
94
+ */
95
+ export type ConfigEntries = Record<string, ConfigEntry>;
96
+
97
+ /**
98
+ * ParserConfig configuration flags that don't refer to a single
99
+ * command-line option.
100
+ */
101
+ export type ParserOptions = {
102
+ allowUnknown?: boolean;
103
+ parseParameters?: boolean;
104
+ aggregateErrors?: boolean;
105
+ };
106
+
107
+ export type StrictParserOptions = {
108
+ allowUnknown: boolean;
109
+ parseParameters: boolean;
110
+ aggregateErrors: boolean;
111
+ };
112
+
113
+ /**
114
+ * The input object for the ParserConfig constructor.
115
+ * Contains configuration for specific command-line options
116
+ * and general configuration options.
117
+ */
118
+ export type ParserConfigInput = {
119
+ options: ConfigEntries;
120
+ parser?: ParserOptions;
121
+ }
122
+
123
+ /**
124
+ * Option value.
125
+ * Represents the possible values returned for parsed options.
126
+ */
127
+ export type OptionValue = boolean | string | number | string[];
128
+
129
+ /**
130
+ * Result mapping.
131
+ * A map-like structure used to store parsed key-value pairs.
132
+ * Instances can be frozen to prevent further modifications.
133
+ */
134
+ abstract class ResultMapping<K, V> extends Map<K, V> {
135
+ protected isFrozen = false;
136
+
137
+ constructor(entries?: [K, V][]) {
138
+ super(entries);
139
+
140
+ // Prevent property from showing up in console.log.
141
+ Object.defineProperty(this, 'isFrozen', {
142
+ enumerable: false,
143
+ });
144
+
145
+ // Prevent regular object property assignments.
146
+ Object.preventExtensions(this);
147
+ }
148
+
149
+ /**
150
+ * Sets a value for the given key.
151
+ * Throws if the mapping is frozen.
152
+ */
153
+ override set(key: K, value: V): this {
154
+ if (this.isFrozen) {
155
+ const name = this.constructor.name.toLowerCase();
156
+ throw new TypeError(`you cannot modify parsed ${name}`);
157
+ }
158
+ return super.set(key, value);
159
+ }
160
+
161
+ /**
162
+ * Deletes a key from the mapping.
163
+ * Throws if the mapping is frozen.
164
+ */
165
+ override delete(key: K): boolean {
166
+ if (this.isFrozen) {
167
+ const name = this.constructor.name.toLowerCase();
168
+ throw new TypeError(`you cannot delete parsed ${name}`);
169
+ }
170
+ return super.delete(key);
171
+ }
172
+
173
+ /**
174
+ * Clears all entries from the mapping.
175
+ * Throws if the mapping is frozen.
176
+ */
177
+ override clear(): void {
178
+ if (this.isFrozen) {
179
+ const name = this.constructor.name.toLowerCase();
180
+ throw new TypeError(`you cannot delete parsed ${name}`);
181
+ }
182
+ super.clear();
183
+ }
184
+
185
+ /**
186
+ * Freezes the mapping, preventing further modifications.
187
+ */
188
+ public freeze(): void {
189
+ this.isFrozen = true;
190
+ Object.freeze(this);
191
+ }
192
+
193
+ /**
194
+ * Serializes the mapping to a plain object.
195
+ */
196
+ public toJSON(): Record<string, V> {
197
+ return Object.fromEntries(this.entries());
198
+ }
199
+ }
200
+
201
+ /**
202
+ * Result list.
203
+ * An array-like structure used to store ordered values.
204
+ * Only numeric indices and the length property may be modified.
205
+ * Assigning any other property will throw an error.
206
+ */
207
+ abstract class ResultList<T> extends Array<T> {
208
+ constructor(args?: T[]) {
209
+ const length = (args as T[])?.length ?? 0;
210
+ super(length);
211
+ for (let i = 0; i < length; i++) {
212
+ this[i] = (args as T[])[i]!;
213
+ }
214
+
215
+ return new Proxy(this, {
216
+ set(target, prop, value) {
217
+ if (prop === 'length') {
218
+ target[prop] = value;
219
+ return true;
220
+ } else if (String(Number(prop)) === prop) {
221
+ target[Number(prop)] = value;
222
+ return true;
223
+ }
224
+
225
+ throw new TypeError(`Cannot add non-index property: ${String(prop)}`);
226
+ }
227
+ });
228
+ }
229
+ }
230
+
231
+ /**
232
+ * Parsed option values.
233
+ * Values are determined by the parser configuration. Repeated options
234
+ * either overwrite previous values or accumulate them, depending on
235
+ * their type.
236
+ */
237
+ export class Options extends ResultMapping<string, OptionValue> {}
238
+
239
+ /**
240
+ * Parsed key/value assignments.
241
+ * Parameters are parsed from arguments of the form "key=value" and do
242
+ * not depend on the parser configuration.
243
+ */
244
+ export class Parameters extends ResultMapping<string, string> {}
245
+
246
+ /**
247
+ * Positional arguments.
248
+ * Operands are arguments that are not parsed as options or parameters.
249
+ */
250
+ export class Operands extends ResultList<string> {}
251
+
252
+ /**
253
+ * The result of parsing operations.
254
+ * - options: parsed option values
255
+ * - parameters: parsed named parameters
256
+ * - operands: positional arguments
257
+ */
258
+ export interface ParseResult {
259
+ options: Options;
260
+ parameters?: Parameters;
261
+ operands: Operands;
262
+ }