@clerc/parser 1.0.0-beta.1
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/LICENSE +21 -0
- package/README.md +334 -0
- package/dist/index.d.ts +136 -0
- package/dist/index.js +319 -0
- package/package.json +64 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022 Ray <https://github.com/so1ve>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
# @clerc/parser
|
|
2
|
+
|
|
3
|
+
A powerful, lightweight, and flexible command-line arguments parser.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @clerc/parser
|
|
9
|
+
# or
|
|
10
|
+
yarn add @clerc/parser
|
|
11
|
+
# or
|
|
12
|
+
pnpm add @clerc/parser
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
The core of this package is the `parse` function. It takes an array of arguments and a configuration object, and returns a structured object containing the parsed flags, parameters, and other useful information.
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
import { parse } from "@clerc/parser";
|
|
21
|
+
|
|
22
|
+
const { flags, parameters, unknown } = parse(process.argv.slice(2), {
|
|
23
|
+
flags: {
|
|
24
|
+
// Define your flags here
|
|
25
|
+
port: {
|
|
26
|
+
type: Number,
|
|
27
|
+
alias: "p",
|
|
28
|
+
default: 8080,
|
|
29
|
+
},
|
|
30
|
+
help: {
|
|
31
|
+
type: Boolean,
|
|
32
|
+
alias: "h",
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
console.log("Flags:", flags);
|
|
38
|
+
console.log("Parameters:", parameters);
|
|
39
|
+
console.log("Unknown args:", unknown);
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## API
|
|
43
|
+
|
|
44
|
+
### `parse(args, options)`
|
|
45
|
+
|
|
46
|
+
#### `args`
|
|
47
|
+
|
|
48
|
+
- Type: `string[]`
|
|
49
|
+
- The array of command-line arguments to parse (e.g., `process.argv.slice(2)`).
|
|
50
|
+
|
|
51
|
+
#### `options`
|
|
52
|
+
|
|
53
|
+
- Type: `ParseOptions`
|
|
54
|
+
|
|
55
|
+
An object to configure the parser.
|
|
56
|
+
|
|
57
|
+
- **`flags`**: An object defining the flags to parse.
|
|
58
|
+
- Key: The name of the flag (in camelCase).
|
|
59
|
+
- Value: The flag's configuration, which can be a type constructor (`String`, `Number`, `Boolean`) or a custom function that takes a string and returns a parsed value or an object with the following properties:
|
|
60
|
+
- `type`: The type of the flag. Can be a constructor like `String`, `Number`, `Boolean`, `Object`, an array of a constructor (e.g., `[String]`), or a custom function that takes a string and returns a parsed value.
|
|
61
|
+
- `alias`: A string or an array of strings for alternative names (e.g., short flags).
|
|
62
|
+
- `default`: A default value for the flag if it's not provided in the arguments. Can be a value or a function that returns a value.
|
|
63
|
+
- `negatable`: (For Booleans) Whether to support `--no-<flag>` syntax. Defaults to `true`.
|
|
64
|
+
- **`ignore`**: A function to conditionally stop parsing. It receives the `type` (`flag` or `parameter`) and the `arg` string, and should return `true` to stop parsing from that point.
|
|
65
|
+
|
|
66
|
+
#### Returns
|
|
67
|
+
|
|
68
|
+
An object with the following properties:
|
|
69
|
+
|
|
70
|
+
- `flags`: An object containing the parsed flags.
|
|
71
|
+
- `parameters`: An array of positional arguments.
|
|
72
|
+
- `unknown`: An object containing flags that were not defined in the schema.
|
|
73
|
+
- `doubleDash`: An array of arguments that appear after a `--`.
|
|
74
|
+
- `ignored`: An array of arguments that were ignored due to the `ignore` function.
|
|
75
|
+
|
|
76
|
+
## Features
|
|
77
|
+
|
|
78
|
+
### Basic Types
|
|
79
|
+
|
|
80
|
+
Supports `Boolean`, `String`, and `Number`.
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
const { flags } = parse(["--bool", "--str", "hello", "--num", "42"], {
|
|
84
|
+
flags: {
|
|
85
|
+
bool: { type: Boolean },
|
|
86
|
+
str: { type: String },
|
|
87
|
+
num: { type: Number },
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
// flags: { bool: true, str: "hello", num: 42 }
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Aliases
|
|
94
|
+
|
|
95
|
+
Use `alias` to define short or alternative names for flags.
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
const { flags } = parse(["-b", "-s", "hello"], {
|
|
99
|
+
flags: {
|
|
100
|
+
bool: { type: Boolean, alias: "b" },
|
|
101
|
+
str: { type: String, alias: "s" },
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
// flags: { bool: true, str: "hello" }
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Arrays and Counters
|
|
108
|
+
|
|
109
|
+
- To collect multiple values for a flag, use an array type like `[String]`.
|
|
110
|
+
- To count the occurrences of a boolean flag, use `[Boolean]`.
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
// Array of strings
|
|
114
|
+
const { flags: arrayFlags } = parse(["--arr", "a", "--arr", "b"], {
|
|
115
|
+
flags: {
|
|
116
|
+
arr: { type: [String] },
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
// arrayFlags: { arr: ["a", "b"] }
|
|
120
|
+
|
|
121
|
+
// Counter
|
|
122
|
+
const { flags: counterFlags } = parse(["-vvv"], {
|
|
123
|
+
flags: {
|
|
124
|
+
verbose: { type: [Boolean], alias: "v" },
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
// counterFlags: { verbose: 3 }
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Merged Short Flags
|
|
131
|
+
|
|
132
|
+
Multiple boolean short flags can be combined. The last flag in the group can take a value.
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
// -abc => a=true, b=true, c=true
|
|
136
|
+
const { flags: merged } = parse(["-abc"], {
|
|
137
|
+
flags: {
|
|
138
|
+
a: Boolean,
|
|
139
|
+
b: Boolean,
|
|
140
|
+
c: Boolean,
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
// merged: { a: true, b: true, c: true }
|
|
144
|
+
|
|
145
|
+
// -ab val => a=true, b="val"
|
|
146
|
+
const { flags: withValue } = parse(["-ab", "val"], {
|
|
147
|
+
flags: {
|
|
148
|
+
a: Boolean,
|
|
149
|
+
b: String,
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
// or -abval, b is not boolean
|
|
153
|
+
const { flags: withValue } = parse(["-abval"], {
|
|
154
|
+
flags: {
|
|
155
|
+
a: Boolean,
|
|
156
|
+
b: String,
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
// withValue: { a: true, b: "val" }
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Default Values
|
|
163
|
+
|
|
164
|
+
Provide a `default` value for flags that are not present.
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
const { flags } = parse([], {
|
|
168
|
+
flags: {
|
|
169
|
+
str: { type: String, default: "default" },
|
|
170
|
+
num: { type: Number, default: 123 },
|
|
171
|
+
fn: { type: String, default: () => "computed" },
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
// flags: { str: "default", num: 123, fn: "computed" }
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
By default, booleans, counters, arrays and objects have implicit defaults when `default` is not provided:
|
|
178
|
+
|
|
179
|
+
- Boolean: `false`
|
|
180
|
+
- Counter: `0`
|
|
181
|
+
- Array: `[]`
|
|
182
|
+
- Object: `{}`
|
|
183
|
+
|
|
184
|
+
### Negatable Flags
|
|
185
|
+
|
|
186
|
+
Boolean flags are negatable by default using the `--no-` prefix.
|
|
187
|
+
|
|
188
|
+
If you want to disable this behavior, set `negatable: false` in the flag configuration. Passing `--no-<flag>` will then be treated as an unknown flag.
|
|
189
|
+
|
|
190
|
+
```typescript
|
|
191
|
+
const { flags, unknown } = parse(
|
|
192
|
+
["--no-cache", "--no-ssl=false", "--no-disable-negate"],
|
|
193
|
+
{
|
|
194
|
+
flags: {
|
|
195
|
+
cache: { type: Boolean, default: true },
|
|
196
|
+
ssl: { type: Boolean, default: true },
|
|
197
|
+
disableNegate: { type: Boolean, negatable: false, default: true },
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
);
|
|
201
|
+
// flags: { cache: false, ssl: true, disableNegate: true }
|
|
202
|
+
// unknown: { noDisableNegate: true }
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### Custom Type Functions
|
|
206
|
+
|
|
207
|
+
You can provide a custom function to the `type` property for advanced parsing logic. The function receives the string value and should return the parsed value.
|
|
208
|
+
|
|
209
|
+
```typescript
|
|
210
|
+
// Let's limit port between 1 and 65535
|
|
211
|
+
const { flags } = parse(["--port", "8080"], {
|
|
212
|
+
flags: {
|
|
213
|
+
port: {
|
|
214
|
+
type: (value: string) => {
|
|
215
|
+
const parsed = Number.parseInt(value, 10);
|
|
216
|
+
if (Number.isNaN(parsed)) {
|
|
217
|
+
throw new TypeError("Port must be a number!");
|
|
218
|
+
}
|
|
219
|
+
if (parsed < 1 || parsed > 65_535) {
|
|
220
|
+
throw new Error("Port must be between 1 and 65535!");
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return parsed;
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
// flags: { port: 8080 }
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### Dot-Nested Objects
|
|
232
|
+
|
|
233
|
+
Define flags with `Object` type to parse dot-notation arguments into a nested object.
|
|
234
|
+
|
|
235
|
+
```typescript
|
|
236
|
+
const { flags } = parse(
|
|
237
|
+
["--config.port", "8080", "--config.host", "localhost"],
|
|
238
|
+
{
|
|
239
|
+
flags: {
|
|
240
|
+
config: { type: Object },
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
);
|
|
244
|
+
// flags: { config: { port: "8080", host: "localhost" } }
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
Multi-level nesting is also supported.
|
|
248
|
+
|
|
249
|
+
```typescript
|
|
250
|
+
const { flags } = parse(
|
|
251
|
+
[
|
|
252
|
+
"--db.host",
|
|
253
|
+
"localhost",
|
|
254
|
+
"--db.port",
|
|
255
|
+
"5432",
|
|
256
|
+
"--db.credentials.user",
|
|
257
|
+
"admin",
|
|
258
|
+
"--db.credentials.password",
|
|
259
|
+
"secret",
|
|
260
|
+
],
|
|
261
|
+
{
|
|
262
|
+
flags: {
|
|
263
|
+
db: { type: Object },
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
);
|
|
267
|
+
// flags: { db: { host: "localhost", port: "5432", credentials: { user: "admin", password: "secret" } } }
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### Unknown Flags
|
|
271
|
+
|
|
272
|
+
Flags that are not defined in the schema are collected in the `unknown` object. If possible, they will be converted to boolean `true`.
|
|
273
|
+
|
|
274
|
+
```typescript
|
|
275
|
+
const { flags, unknown } = parse([
|
|
276
|
+
"--unknown1",
|
|
277
|
+
"--unknown2=foo",
|
|
278
|
+
"--unknown3",
|
|
279
|
+
"bar",
|
|
280
|
+
"--unknown.foo",
|
|
281
|
+
]);
|
|
282
|
+
|
|
283
|
+
// unknown: { unknown1: true, unknown2: "foo", unknown3: "bar", "unknown.foo": true }
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
### Stop Parsing
|
|
287
|
+
|
|
288
|
+
Use the `ignore` function to stop parsing when a certain condition is met. Subsequent arguments will be added to the `ignored` array.
|
|
289
|
+
|
|
290
|
+
```typescript
|
|
291
|
+
const { flags, ignored } = parse(["--a", "--b", "stop", "--c"], {
|
|
292
|
+
flags: {
|
|
293
|
+
a: Boolean,
|
|
294
|
+
b: Boolean,
|
|
295
|
+
c: Boolean,
|
|
296
|
+
},
|
|
297
|
+
ignore: (type, arg) => arg === "stop",
|
|
298
|
+
});
|
|
299
|
+
// flags: { a: true, b: true, c: false }
|
|
300
|
+
// ignored: ["stop", "--c"]
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
You can ignore everything after the first positional parameter by checking the `type`.
|
|
304
|
+
|
|
305
|
+
```typescript
|
|
306
|
+
const { flags, parameters, ignored } = parse(
|
|
307
|
+
["--allow-all", "./deno.ts", "--param"],
|
|
308
|
+
{
|
|
309
|
+
flags: {
|
|
310
|
+
allowAll: Boolean,
|
|
311
|
+
},
|
|
312
|
+
ignore: (type) => type === "parameter",
|
|
313
|
+
},
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
// flags: { allowAll: true }
|
|
317
|
+
// parameters: []
|
|
318
|
+
// ignored: ["./deno.ts", "--param"]
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
### Double Dash (`--`)
|
|
322
|
+
|
|
323
|
+
Arguments after `--` are not parsed as flags and are collected in the `doubleDash` array.
|
|
324
|
+
|
|
325
|
+
```typescript
|
|
326
|
+
const { flags, doubleDash } = parse(["--foo", "--", "--bar"], {
|
|
327
|
+
flags: {
|
|
328
|
+
foo: Boolean,
|
|
329
|
+
bar: Boolean,
|
|
330
|
+
},
|
|
331
|
+
});
|
|
332
|
+
// flags: { foo: true, bar: false }
|
|
333
|
+
// doubleDash: ["--bar"]
|
|
334
|
+
```
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
//#region src/errors.d.ts
|
|
2
|
+
declare class InvalidSchemaError extends Error {
|
|
3
|
+
constructor(message: string);
|
|
4
|
+
}
|
|
5
|
+
//#endregion
|
|
6
|
+
//#region ../utils/src/types/type-fest.d.ts
|
|
7
|
+
type Prettify<T> = { [K in keyof T]: T[K] } & {};
|
|
8
|
+
//#endregion
|
|
9
|
+
//#region ../utils/src/types/index.d.ts
|
|
10
|
+
type MaybeArray<T> = T | T[];
|
|
11
|
+
//#endregion
|
|
12
|
+
//#region src/types.d.ts
|
|
13
|
+
type FlagDefaultValue<T = unknown> = T | (() => T);
|
|
14
|
+
/**
|
|
15
|
+
* Defines how a string input is converted to the target type T.
|
|
16
|
+
*
|
|
17
|
+
* @template T The target type.
|
|
18
|
+
*/
|
|
19
|
+
type FlagTypeFunction<T = unknown> = (value: string) => T;
|
|
20
|
+
/**
|
|
21
|
+
* A callback function to conditionally stop parsing.
|
|
22
|
+
* When it returns true, parsing stops and remaining arguments are preserved in `ignored`.
|
|
23
|
+
*
|
|
24
|
+
* @param type - The type of the current argument: 'known-flag' or 'unknown-flag' for flags, 'parameter' for positional arguments
|
|
25
|
+
* @param arg - The current argument being processed
|
|
26
|
+
* @returns true to stop parsing, false to continue
|
|
27
|
+
*/
|
|
28
|
+
type IgnoreFunction = (type: typeof KNOWN_FLAG | typeof UNKNOWN_FLAG | typeof PARAMETER, arg: string) => boolean;
|
|
29
|
+
type FlagType<T = unknown> = FlagTypeFunction<T> | readonly [FlagTypeFunction<T>];
|
|
30
|
+
interface BaseFlagOptions<T extends FlagType = FlagType> {
|
|
31
|
+
/**
|
|
32
|
+
* The type constructor or a function to convert the string value.
|
|
33
|
+
* To support multiple occurrences of a flag (e.g., --file a --file b), wrap the type in an array: [String], [Number].
|
|
34
|
+
* e.g., String, Number, [String], (val) => val.split(',')
|
|
35
|
+
*/
|
|
36
|
+
type: T;
|
|
37
|
+
/** Aliases for the flag. */
|
|
38
|
+
alias?: MaybeArray<string>;
|
|
39
|
+
/** The default value of the flag. */
|
|
40
|
+
default?: unknown;
|
|
41
|
+
}
|
|
42
|
+
type FlagOptions = (BaseFlagOptions<BooleanConstructor> & {
|
|
43
|
+
/**
|
|
44
|
+
* Whether to enable the `--no-<flag>` syntax to set the value to false.
|
|
45
|
+
* Only useful for boolean flags.
|
|
46
|
+
* When set on a non-boolean flag, a type error will be shown.
|
|
47
|
+
*
|
|
48
|
+
* @default true
|
|
49
|
+
*/
|
|
50
|
+
negatable?: boolean;
|
|
51
|
+
}) | (BaseFlagOptions & {
|
|
52
|
+
negatable?: never;
|
|
53
|
+
});
|
|
54
|
+
type FlagDefinitionValue = FlagOptions | FlagType;
|
|
55
|
+
type FlagsDefinition = Record<string, FlagDefinitionValue>;
|
|
56
|
+
/**
|
|
57
|
+
* Configuration options for the parser.
|
|
58
|
+
*/
|
|
59
|
+
interface ParserOptions<T extends FlagsDefinition = {}> {
|
|
60
|
+
/**
|
|
61
|
+
* Detailed configuration for flags.
|
|
62
|
+
* Supports the full object syntax or a type constructor as a shorthand.
|
|
63
|
+
* The key is the flag name (e.g., "file" for "--file").
|
|
64
|
+
*/
|
|
65
|
+
flags?: T;
|
|
66
|
+
/**
|
|
67
|
+
* Delimiters to split flag names and values.
|
|
68
|
+
*
|
|
69
|
+
* @default ['=', ':']
|
|
70
|
+
*/
|
|
71
|
+
delimiters?: string[];
|
|
72
|
+
/**
|
|
73
|
+
* A callback function to conditionally stop parsing.
|
|
74
|
+
* When it returns true, parsing stops and remaining arguments are preserved in `ignored`.
|
|
75
|
+
*/
|
|
76
|
+
ignore?: IgnoreFunction;
|
|
77
|
+
}
|
|
78
|
+
type RawInputType = string | boolean;
|
|
79
|
+
interface ObjectInputType {
|
|
80
|
+
[key: string]: RawInputType | ObjectInputType;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* The parsed result.
|
|
84
|
+
* @template TFlags The specific flags type inferred from ParserOptions.
|
|
85
|
+
*/
|
|
86
|
+
interface ParsedResult<TFlags extends Record<string, any>> {
|
|
87
|
+
/** Positional arguments or commands. */
|
|
88
|
+
parameters: string[];
|
|
89
|
+
/** Arguments after the `--` delimiter. */
|
|
90
|
+
doubleDash: string[];
|
|
91
|
+
/**
|
|
92
|
+
* The parsed flags.
|
|
93
|
+
* This is a strongly-typed object whose structure is inferred from the `flags` configuration in ParserOptions.
|
|
94
|
+
*/
|
|
95
|
+
flags: TFlags;
|
|
96
|
+
/** The raw command-line arguments. */
|
|
97
|
+
raw: string[];
|
|
98
|
+
/** Unknown flags encountered during parsing. */
|
|
99
|
+
unknown: Record<string, RawInputType>;
|
|
100
|
+
/** Arguments that were not parsed due to ignore callback. */
|
|
101
|
+
ignored: string[];
|
|
102
|
+
}
|
|
103
|
+
type InferFlagDefault<T extends FlagDefinitionValue, Fallback> = T extends {
|
|
104
|
+
default: FlagDefaultValue<infer DefaultType>;
|
|
105
|
+
} ? DefaultType : Fallback;
|
|
106
|
+
type _InferFlags<T extends FlagsDefinition> = { [K in keyof T]: T[K] extends readonly [BooleanConstructor] | {
|
|
107
|
+
type: readonly [BooleanConstructor];
|
|
108
|
+
} ? number : T[K] extends ObjectConstructor | {
|
|
109
|
+
type: ObjectConstructor;
|
|
110
|
+
} ? ObjectInputType : T[K] extends readonly [FlagType<infer U>] | {
|
|
111
|
+
type: readonly [FlagType<infer U>];
|
|
112
|
+
} ? U[] | InferFlagDefault<T[K], never> : T[K] extends FlagType<infer U> | {
|
|
113
|
+
type: FlagType<infer U>;
|
|
114
|
+
} ? U | InferFlagDefault<T[K], [U] extends [boolean] ? never : undefined> : never };
|
|
115
|
+
/**
|
|
116
|
+
* An advanced utility type that infers the exact type of the `flags` object in the parsed result,
|
|
117
|
+
* based on the provided `flags` configuration object T.
|
|
118
|
+
*
|
|
119
|
+
* @template T The type of the flags configuration object.
|
|
120
|
+
*/
|
|
121
|
+
type InferFlags<T extends FlagsDefinition> = Prettify<_InferFlags<T>>;
|
|
122
|
+
//#endregion
|
|
123
|
+
//#region src/iterator.d.ts
|
|
124
|
+
declare const KNOWN_FLAG = "known-flag";
|
|
125
|
+
declare const UNKNOWN_FLAG = "unknown-flag";
|
|
126
|
+
declare const PARAMETER = "parameter";
|
|
127
|
+
//#endregion
|
|
128
|
+
//#region src/parse.d.ts
|
|
129
|
+
declare const DOUBLE_DASH = "--";
|
|
130
|
+
type ParseFunction<T extends FlagsDefinition> = (args: string[]) => ParsedResult<InferFlags<T>>;
|
|
131
|
+
declare function createParser<T extends FlagsDefinition>(options?: ParserOptions<T>): {
|
|
132
|
+
parse: ParseFunction<T>;
|
|
133
|
+
};
|
|
134
|
+
declare const parse: <T extends FlagsDefinition>(args: string[], options?: ParserOptions<T>) => ParsedResult<InferFlags<T>>;
|
|
135
|
+
//#endregion
|
|
136
|
+
export { BaseFlagOptions, DOUBLE_DASH, FlagDefaultValue, FlagDefinitionValue, FlagOptions, FlagType, FlagTypeFunction, FlagsDefinition, IgnoreFunction, InferFlags, InvalidSchemaError, KNOWN_FLAG, ObjectInputType, PARAMETER, ParsedResult, ParserOptions, RawInputType, UNKNOWN_FLAG, createParser, parse };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
//#region src/errors.ts
|
|
2
|
+
var InvalidSchemaError = class extends Error {
|
|
3
|
+
constructor(message) {
|
|
4
|
+
super(`Invalid schema: ${message}`);
|
|
5
|
+
this.name = "InvalidSchemaError";
|
|
6
|
+
}
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
//#endregion
|
|
10
|
+
//#region src/iterator.ts
|
|
11
|
+
const KNOWN_FLAG = "known-flag";
|
|
12
|
+
const UNKNOWN_FLAG = "unknown-flag";
|
|
13
|
+
const PARAMETER = "parameter";
|
|
14
|
+
function iterateArgs(args, result, shouldProcessAsFlag, isKnownFlag, ignore, callback) {
|
|
15
|
+
let index = 0;
|
|
16
|
+
let stopped = false;
|
|
17
|
+
const argsLength = args.length;
|
|
18
|
+
const iterator = {
|
|
19
|
+
current: "",
|
|
20
|
+
index: 0,
|
|
21
|
+
hasNext: false,
|
|
22
|
+
next: "",
|
|
23
|
+
shouldIgnore: (arg) => {
|
|
24
|
+
if (ignore) return ignore(shouldProcessAsFlag(arg) ? isKnownFlag(arg) ? KNOWN_FLAG : UNKNOWN_FLAG : PARAMETER, arg);
|
|
25
|
+
return false;
|
|
26
|
+
},
|
|
27
|
+
eat: () => {
|
|
28
|
+
if (index + 1 >= argsLength) return;
|
|
29
|
+
const nextArg = args[index + 1];
|
|
30
|
+
if (iterator.shouldIgnore(nextArg)) {
|
|
31
|
+
iterator.exit();
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
index++;
|
|
35
|
+
next();
|
|
36
|
+
return args[index];
|
|
37
|
+
},
|
|
38
|
+
exit: (push = true) => {
|
|
39
|
+
if (!stopped) {
|
|
40
|
+
if (push) result.ignored.push(...args.slice(index + 1));
|
|
41
|
+
stopped = true;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
function next() {
|
|
46
|
+
iterator.current = args[index];
|
|
47
|
+
iterator.index = index;
|
|
48
|
+
iterator.hasNext = index + 1 < argsLength;
|
|
49
|
+
iterator.next = iterator.hasNext ? args[index + 1] : "";
|
|
50
|
+
}
|
|
51
|
+
for (index = 0; index < argsLength; index++) {
|
|
52
|
+
if (stopped) break;
|
|
53
|
+
next();
|
|
54
|
+
callback(iterator);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
//#endregion
|
|
59
|
+
//#region ../utils/dist/index.js
|
|
60
|
+
const toArray = (a) => Array.isArray(a) ? a : [a];
|
|
61
|
+
/**
|
|
62
|
+
* Converts a dash-separated string to camelCase.
|
|
63
|
+
* Not using regexp for better performance, because this function is used in parser.
|
|
64
|
+
*/
|
|
65
|
+
function camelCase(str) {
|
|
66
|
+
const dashIdx = str.indexOf("-");
|
|
67
|
+
if (dashIdx === -1) return str;
|
|
68
|
+
let result = str.slice(0, dashIdx);
|
|
69
|
+
for (let i = dashIdx; i < str.length; i++) if (str[i] === "-" && i + 1 < str.length) {
|
|
70
|
+
const nextChar = str.charCodeAt(i + 1);
|
|
71
|
+
if (nextChar >= 97 && nextChar <= 122) {
|
|
72
|
+
result += String.fromCharCode(nextChar - 32);
|
|
73
|
+
i++;
|
|
74
|
+
} else {
|
|
75
|
+
result += str[i + 1];
|
|
76
|
+
i++;
|
|
77
|
+
}
|
|
78
|
+
} else if (str[i] !== "-") result += str[i];
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
//#endregion
|
|
83
|
+
//#region src/utils.ts
|
|
84
|
+
const looseIsArray = (arr) => Array.isArray(arr);
|
|
85
|
+
const isArrayOfType = (arr, type) => Array.isArray(arr) && arr[0] === type;
|
|
86
|
+
/**
|
|
87
|
+
* Check if it's a letter (a-z: 97-122, A-Z: 65-90)
|
|
88
|
+
*/
|
|
89
|
+
const isLetter = (charCode) => charCode >= 65 && charCode <= 90 || charCode >= 97 && charCode <= 122;
|
|
90
|
+
/**
|
|
91
|
+
* Check if it's a digit (0-9: 48-57)
|
|
92
|
+
*/
|
|
93
|
+
const isDigit = (charCode) => charCode >= 48 && charCode <= 57;
|
|
94
|
+
function setValueByType(flags, key, value, config) {
|
|
95
|
+
const { type } = config;
|
|
96
|
+
if (looseIsArray(type)) if (isArrayOfType(type, Boolean)) flags[key] = (flags[key] ?? 0) + 1;
|
|
97
|
+
else (flags[key] ??= []).push(type[0](value));
|
|
98
|
+
else flags[key] = type(value);
|
|
99
|
+
}
|
|
100
|
+
function setDotValues(obj, path, value) {
|
|
101
|
+
const keys = path.split(".");
|
|
102
|
+
let current = obj;
|
|
103
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
104
|
+
const key = keys[i];
|
|
105
|
+
current[key] ??= {};
|
|
106
|
+
current = current[key];
|
|
107
|
+
}
|
|
108
|
+
const lastKey = keys[keys.length - 1];
|
|
109
|
+
if (value === "true" || value === "") current[lastKey] = true;
|
|
110
|
+
else if (value === "false") current[lastKey] = false;
|
|
111
|
+
else current[lastKey] = value;
|
|
112
|
+
}
|
|
113
|
+
function splitNameAndValue(arg, delimiters) {
|
|
114
|
+
let sepIdx = -1;
|
|
115
|
+
let delimiterLen = 0;
|
|
116
|
+
for (const delimiter of delimiters) {
|
|
117
|
+
const idx = arg.indexOf(delimiter);
|
|
118
|
+
if (idx !== -1 && (sepIdx === -1 || idx < sepIdx)) {
|
|
119
|
+
sepIdx = idx;
|
|
120
|
+
delimiterLen = delimiter.length;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (!(sepIdx !== -1)) return {
|
|
124
|
+
rawName: arg,
|
|
125
|
+
rawValue: void 0,
|
|
126
|
+
hasSep: false
|
|
127
|
+
};
|
|
128
|
+
return {
|
|
129
|
+
rawName: arg.slice(0, sepIdx),
|
|
130
|
+
rawValue: arg.slice(sepIdx + delimiterLen),
|
|
131
|
+
hasSep: true
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
//#endregion
|
|
136
|
+
//#region src/config.ts
|
|
137
|
+
const defaultParserOptions = { delimiters: ["=", ":"] };
|
|
138
|
+
const resolveParserOptions = (options = {}) => ({
|
|
139
|
+
...defaultParserOptions,
|
|
140
|
+
...options
|
|
141
|
+
});
|
|
142
|
+
const normalizeConfig = (config) => typeof config === "function" || looseIsArray(config) ? { type: config } : config;
|
|
143
|
+
const BUILDTIN_DELIMITERS_RE = /[\s.]/;
|
|
144
|
+
function buildConfigsAndAliases(delimiters, flags) {
|
|
145
|
+
const configs = /* @__PURE__ */ new Map();
|
|
146
|
+
const aliases = /* @__PURE__ */ new Map();
|
|
147
|
+
const isNameInvalid = (name) => delimiters.some((char) => name.includes(char)) || BUILDTIN_DELIMITERS_RE.test(name);
|
|
148
|
+
function validateFlagOptions(name, options) {
|
|
149
|
+
const prefix = `Flag "${name}"`;
|
|
150
|
+
if (Array.isArray(options.type) && options.type.length > 1) throw new InvalidSchemaError(`${prefix} has an invalid type array. Only single-element arrays are allowed to denote multiple occurrences.`);
|
|
151
|
+
const names = [name];
|
|
152
|
+
if (options.alias) names.push(...toArray(options.alias));
|
|
153
|
+
if (names.some(isNameInvalid)) throw new InvalidSchemaError(`${prefix} contains reserved characters, which are used as delimiters.`);
|
|
154
|
+
}
|
|
155
|
+
for (const [name, config] of Object.entries(flags)) {
|
|
156
|
+
const normalized = normalizeConfig(config);
|
|
157
|
+
validateFlagOptions(name, normalized);
|
|
158
|
+
configs.set(name, normalized);
|
|
159
|
+
aliases.set(name, name);
|
|
160
|
+
aliases.set(camelCase(name), name);
|
|
161
|
+
if (normalized.alias) {
|
|
162
|
+
const list = Array.isArray(normalized.alias) ? normalized.alias : [normalized.alias];
|
|
163
|
+
for (const a of list) aliases.set(a, name);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
configs,
|
|
168
|
+
aliases
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
//#endregion
|
|
173
|
+
//#region src/parse.ts
|
|
174
|
+
const DOUBLE_DASH = "--";
|
|
175
|
+
function createParser(options = {}) {
|
|
176
|
+
const { flags: flagsConfig = {}, delimiters, ignore } = resolveParserOptions(options);
|
|
177
|
+
const { configs, aliases } = buildConfigsAndAliases(delimiters, flagsConfig);
|
|
178
|
+
function resolve(name) {
|
|
179
|
+
const dotIdx = name.indexOf(".");
|
|
180
|
+
const rootName = dotIdx === -1 ? name : name.slice(0, dotIdx);
|
|
181
|
+
let key = aliases.get(rootName);
|
|
182
|
+
if (!key) {
|
|
183
|
+
key = aliases.get(camelCase(rootName));
|
|
184
|
+
if (!key) return;
|
|
185
|
+
}
|
|
186
|
+
const config = configs.get(key);
|
|
187
|
+
return {
|
|
188
|
+
key,
|
|
189
|
+
config,
|
|
190
|
+
path: dotIdx === -1 ? void 0 : name.slice(dotIdx + 1)
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
function resolveNegated(name) {
|
|
194
|
+
if (!name.startsWith("no")) return;
|
|
195
|
+
const possibleName = name[2] === "-" ? name.slice(3) : name.length > 2 && /[A-Z]/.test(name[2]) ? name[2].toLowerCase() + name.slice(3) : "";
|
|
196
|
+
if (possibleName) {
|
|
197
|
+
const possibleResolved = resolve(possibleName);
|
|
198
|
+
if (possibleResolved?.config.type === Boolean && possibleResolved.config.negatable !== false) return possibleResolved;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
function shouldProcessAsFlag(arg) {
|
|
202
|
+
if (arg.charCodeAt(0) !== 45) return false;
|
|
203
|
+
const len = arg.length;
|
|
204
|
+
if (len < 2) return false;
|
|
205
|
+
const secondChar = arg.charCodeAt(1);
|
|
206
|
+
if (isLetter(secondChar)) return true;
|
|
207
|
+
if (isDigit(secondChar)) return !!resolve(secondChar !== 45 ? arg[1] : arg.slice(2));
|
|
208
|
+
if (secondChar === 45 && len > 2) return isLetter(arg.charCodeAt(2));
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
function isKnownFlag(arg) {
|
|
212
|
+
const secondChar = arg.charCodeAt(1);
|
|
213
|
+
if (secondChar === 45) {
|
|
214
|
+
const { rawName } = splitNameAndValue(arg.slice(2), delimiters);
|
|
215
|
+
if (resolve(rawName)) return true;
|
|
216
|
+
if (resolveNegated(rawName)) return true;
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
if (isDigit(secondChar)) return true;
|
|
220
|
+
const chars = arg.slice(1);
|
|
221
|
+
for (const char of chars) if (!resolve(char)) return false;
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
const parse$1 = (args) => {
|
|
225
|
+
const result = {
|
|
226
|
+
parameters: [],
|
|
227
|
+
doubleDash: [],
|
|
228
|
+
flags: {},
|
|
229
|
+
raw: args,
|
|
230
|
+
unknown: {},
|
|
231
|
+
ignored: []
|
|
232
|
+
};
|
|
233
|
+
iterateArgs(args, result, shouldProcessAsFlag, isKnownFlag, ignore, ({ current, eat, exit, hasNext, index, next, shouldIgnore }) => {
|
|
234
|
+
if (current === DOUBLE_DASH) {
|
|
235
|
+
result.doubleDash.push(...args.slice(index + 1));
|
|
236
|
+
exit(false);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
if (shouldIgnore(current)) {
|
|
240
|
+
result.ignored.push(current);
|
|
241
|
+
exit();
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
if (!shouldProcessAsFlag(current)) {
|
|
245
|
+
result.parameters.push(current);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
const isAlias = !current.startsWith(DOUBLE_DASH);
|
|
249
|
+
const chars = current.slice(isAlias ? 1 : 2);
|
|
250
|
+
if (isAlias) {
|
|
251
|
+
const charsLen = chars.length;
|
|
252
|
+
for (let j = 0; j < charsLen; j++) {
|
|
253
|
+
const char = chars[j];
|
|
254
|
+
const resolved = resolve(char);
|
|
255
|
+
if (!resolved) {
|
|
256
|
+
result.unknown[char] = true;
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
const { key, config } = resolved;
|
|
260
|
+
const configType = config.type;
|
|
261
|
+
if (configType === Boolean || isArrayOfType(configType, Boolean)) setValueByType(result.flags, key, "true", config);
|
|
262
|
+
else if (j + 1 < charsLen) {
|
|
263
|
+
setValueByType(result.flags, key, chars.slice(j + 1), config);
|
|
264
|
+
break;
|
|
265
|
+
} else {
|
|
266
|
+
const nextValue = eat();
|
|
267
|
+
if (nextValue && !shouldProcessAsFlag(nextValue)) setValueByType(result.flags, key, nextValue, config);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
} else {
|
|
271
|
+
const { rawName, rawValue, hasSep } = splitNameAndValue(chars, delimiters);
|
|
272
|
+
let resolved = resolve(rawName);
|
|
273
|
+
let isNegated = false;
|
|
274
|
+
if (!resolved) {
|
|
275
|
+
const negated = resolveNegated(rawName);
|
|
276
|
+
if (negated) {
|
|
277
|
+
resolved = negated;
|
|
278
|
+
isNegated = true;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
if (!resolved) {
|
|
282
|
+
const key$1 = camelCase(rawName);
|
|
283
|
+
if (hasSep) result.unknown[key$1] = rawValue;
|
|
284
|
+
else if (hasNext && !shouldProcessAsFlag(next)) {
|
|
285
|
+
const value = eat();
|
|
286
|
+
result.unknown[key$1] = value ?? true;
|
|
287
|
+
} else result.unknown[key$1] = true;
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
const { key, config, path } = resolved;
|
|
291
|
+
if (path) {
|
|
292
|
+
if (config.type === Object) {
|
|
293
|
+
result.flags[key] ??= {};
|
|
294
|
+
const value = hasSep ? rawValue : hasNext && !shouldProcessAsFlag(next) ? eat() ?? "" : "";
|
|
295
|
+
setDotValues(result.flags[key], path, value);
|
|
296
|
+
}
|
|
297
|
+
} else if (config.type === Boolean) {
|
|
298
|
+
const value = hasSep ? rawValue !== "false" : true;
|
|
299
|
+
result.flags[key] = isNegated ? !value : value;
|
|
300
|
+
} else {
|
|
301
|
+
const value = hasSep ? rawValue : hasNext && !shouldProcessAsFlag(next) ? eat() ?? "" : "";
|
|
302
|
+
setValueByType(result.flags, key, value, config);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
for (const [key, config] of configs.entries()) if (result.flags[key] === void 0) {
|
|
307
|
+
if (config.default !== void 0) result.flags[key] = typeof config.default === "function" ? config.default() : config.default;
|
|
308
|
+
else if (Array.isArray(config.type)) result.flags[key] = isArrayOfType(config.type, Boolean) ? 0 : [];
|
|
309
|
+
else if (config.type === Object) result.flags[key] = {};
|
|
310
|
+
else if (config.type === Boolean) result.flags[key] = false;
|
|
311
|
+
}
|
|
312
|
+
return result;
|
|
313
|
+
};
|
|
314
|
+
return { parse: parse$1 };
|
|
315
|
+
}
|
|
316
|
+
const parse = (args, options = {}) => createParser(options).parse(args);
|
|
317
|
+
|
|
318
|
+
//#endregion
|
|
319
|
+
export { DOUBLE_DASH, InvalidSchemaError, KNOWN_FLAG, PARAMETER, UNKNOWN_FLAG, createParser, parse };
|
package/package.json
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@clerc/parser",
|
|
3
|
+
"version": "1.0.0-beta.1",
|
|
4
|
+
"author": "Ray <i@mk1.io> (https://github.com/so1ve)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "Clerc parser",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"args",
|
|
9
|
+
"arguments",
|
|
10
|
+
"argv",
|
|
11
|
+
"clerc",
|
|
12
|
+
"cli",
|
|
13
|
+
"getopt",
|
|
14
|
+
"parser",
|
|
15
|
+
"terminal",
|
|
16
|
+
"utils"
|
|
17
|
+
],
|
|
18
|
+
"homepage": "https://github.com/clercjs/clerc/tree/main/packages/parser#readme",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/clercjs/clerc.git",
|
|
22
|
+
"directory": "/"
|
|
23
|
+
},
|
|
24
|
+
"bugs": {
|
|
25
|
+
"url": "https://github.com/clercjs/clerc/issues"
|
|
26
|
+
},
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"sideEffects": false,
|
|
29
|
+
"exports": {
|
|
30
|
+
".": "./dist/index.js"
|
|
31
|
+
},
|
|
32
|
+
"main": "./dist/index.js",
|
|
33
|
+
"module": "./dist/index.js",
|
|
34
|
+
"types": "dist/index.d.ts",
|
|
35
|
+
"typesVersions": {
|
|
36
|
+
"*": {
|
|
37
|
+
"*": [
|
|
38
|
+
"./dist/*",
|
|
39
|
+
"./dist/index.d.ts"
|
|
40
|
+
]
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
"files": [
|
|
44
|
+
"dist"
|
|
45
|
+
],
|
|
46
|
+
"publishConfig": {
|
|
47
|
+
"access": "public"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@types/minimist": "^1.2.5",
|
|
51
|
+
"@types/nopt": "^3.0.32",
|
|
52
|
+
"@types/yargs-parser": "^21.0.3",
|
|
53
|
+
"minimist": "^1.2.8",
|
|
54
|
+
"mri": "^1.2.0",
|
|
55
|
+
"nopt": "^9.0.0",
|
|
56
|
+
"type-flag": "^4.0.3",
|
|
57
|
+
"yargs-parser": "^22.0.0",
|
|
58
|
+
"@clerc/utils": "1.0.0-beta.1"
|
|
59
|
+
},
|
|
60
|
+
"scripts": {
|
|
61
|
+
"build": "tsdown",
|
|
62
|
+
"watch": "tsdown --watch"
|
|
63
|
+
}
|
|
64
|
+
}
|