Skip to content

Commit eed85e6

Browse files
committed
impl verb & subverb parser
1 parent b1aae3f commit eed85e6

File tree

2 files changed

+180
-60
lines changed

2 files changed

+180
-60
lines changed

src/sifter/parser.ts

Lines changed: 177 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,7 @@ import { ParseContext, ParseResult } from './state.js';
22
import type { CmdArgument, CmdOption, CmdVerb } from './types.ts';
33

44
/**
5-
* Parse argument. This will throw an error if there's no at least 1
6-
* argument left on the token stream. This will not mutate `result`
7-
* on transformation error.
5+
* Parse argument.
86
* @param ctx The parsing context.
97
* @param result Where to set the result.
108
* @param argDef The argument definition.
@@ -13,16 +11,11 @@ import type { CmdArgument, CmdOption, CmdVerb } from './types.ts';
1311
export function parseArgument(ctx: ParseContext, result: ParseResult,
1412
argDef: CmdArgument): void
1513
{
16-
if (ctx.isEndOfTokens)
17-
throw 'Insufficient arguments';
1814
result.set(argDef.id, argDef.type(ctx));
1915
}
2016

2117
/**
22-
* Parse option arguments. This will parse all the required arguments
23-
* mandatorily. Optional arguments will parse until one fails. After
24-
* a failed optional argument, only the defaults will be set. It will
25-
* also require parsing the first argument if `reqFirstOptArg` is set.
18+
* Parse option arguments.
2619
* @param ctx The parsing context.
2720
* @param result Where to set the results.
2821
* @param optArgDefs The option-argument definitions.
@@ -67,7 +60,7 @@ export function parseLongOption(ctx: ParseContext, result: ParseResult,
6760
optDefs: CmdOption[]): void
6861
{
6962
const state = ctx.getCurrentState();
70-
if (state.stopOptions || ctx.isEndOfTokens)
63+
if (ctx.isEndOfTokens)
7164
return;
7265

7366
const tok = ctx.currentToken;
@@ -105,7 +98,7 @@ export function parseShortOption(ctx: ParseContext, result: ParseResult,
10598
optDefs: CmdOption[]): void
10699
{
107100
const state = ctx.getCurrentState();
108-
if (state.stopOptions || ctx.isEndOfTokens)
101+
if (ctx.isEndOfTokens)
109102
return;
110103

111104
const tok = ctx.currentToken;
@@ -147,60 +140,190 @@ export function parseShortOption(ctx: ParseContext, result: ParseResult,
147140
*/
148141
export function findOption(optDefs: CmdOption[], optName: string): CmdOption
149142
{
150-
const def = optDefs.find(def => def.names.includes(optName));
143+
const def = optDefs?.find(def => def.names.includes(optName));
151144
if (!def) throw 'Unknown option: ' + optName;
152145
return def;
153146
}
154147

148+
/**
149+
* Parse a verb.
150+
* @param ctx The parsing context.
151+
* @param result Where to set the results.
152+
* @param verbDef The verb's definition.
153+
* @throws This function can throw errors.
154+
*/
155+
export function parseVerb(ctx: ParseContext, result: ParseResult,
156+
verbDef: CmdVerb): void
157+
{
158+
let stopOptions = false;
159+
let argIdx = 0;
155160

156-
declare const scriptArgs: string[];
161+
while (!ctx.isEndOfTokens) {
162+
const tok = ctx.currentToken;
157163

158-
const info: CmdOption[] = [
159-
{
160-
id: 'opt',
161-
names: ['--opt', '--bac', '-o'],
162-
args: [
163-
{
164-
id: 'num',
165-
type: (ctx) => +ctx.consumeToken(),
166-
},
167-
{
168-
id: 'hello',
169-
type: (ctx) => ctx.consumeToken(),
170-
default: 1,
171-
optional: true,
164+
if (tok[0] == '-' && tok.length >= 2 && !stopOptions) {
165+
/* short opts */
166+
if (tok[1] != '-') {
167+
parseShortOption(ctx, result, verbDef.options);
168+
continue;
172169
}
173-
]
174-
},
175-
{
176-
id: 'another',
177-
names: ['--another', '--ano', '-a'],
178-
args: [
179-
{
180-
id: 'val',
181-
type: (ctx) => {
182-
const tok = ctx.consumeToken();
183-
if (!/^[+-]?[0-9]+(?:\.[0-9]+)?$/.test(tok))
184-
throw 'invalid number: ' + tok;
185-
return +tok;
186-
},
187-
optional: true,
188-
default: 0,
170+
/* end-of-options delimeter */
171+
if (tok.length == 2) {
172+
stopOptions = true;
173+
ctx.consumeToken();
174+
continue;
189175
}
190-
]
191-
},
192-
{
193-
id: 'verbose',
194-
names: ['-v', '--verbose'],
176+
/* long opts */
177+
parseLongOption(ctx, result, verbDef.options);
178+
continue;
179+
}
180+
181+
/* positional arguments */
182+
if (argIdx < verbDef.args?.length) {
183+
const argDef = verbDef.args[argIdx++];
184+
parseArgument(ctx, result, argDef);
185+
continue;
186+
}
187+
188+
/* try subcommands */
189+
if (verbDef.subverbs?.length) {
190+
parseSubVerb(ctx, result, verbDef.subverbs);
191+
break;
192+
}
193+
194+
throw 'Too many arguments';
195195
}
196-
];
196+
197+
/* set the defaults of other optional positional params */
198+
while (argIdx < verbDef.args?.length) {
199+
const argDef = verbDef.args[argIdx++];
200+
if (!argDef.optional)
201+
throw 'Insufficient arguments';
202+
result.set(argDef.id, argDef.default);
203+
}
204+
}
205+
206+
/**
207+
* Parse a subverb.
208+
* @param ctx The parsing context.
209+
* @param result Where to set the results.
210+
* @param verbDefs Where to lookup subverb defs.
211+
* @throws This function can throw errors.
212+
*/
213+
export function parseSubVerb(ctx: ParseContext, result: ParseResult,
214+
verbDefs: CmdVerb[]): void
215+
{
216+
const subName = ctx.consumeToken();
217+
const verbDef = verbDefs.find(def =>
218+
def.name == subName || def.aliases?.includes(subName));
219+
220+
if (!verbDef)
221+
throw 'Unknown subcommand: ' + subName;
222+
223+
const subRes = new ParseResult();
224+
parseVerb(ctx, subRes, verbDef);
225+
result.set(verbDef.id, subRes);
226+
}
227+
228+
229+
230+
231+
232+
233+
declare const scriptArgs: string[];
234+
235+
const info: CmdVerb = {
236+
id: 'cmd',
237+
name: 'cmd',
238+
options: [
239+
{
240+
id: 'opt',
241+
names: ['--opt', '--bac', '-o'],
242+
args: [
243+
{
244+
id: 'num',
245+
type: (ctx) => +ctx.consumeToken(),
246+
},
247+
{
248+
id: 'hello',
249+
type: (ctx) => ctx.consumeToken(),
250+
default: 1,
251+
optional: true,
252+
}
253+
]
254+
},
255+
{
256+
id: 'another',
257+
names: ['--another', '--ano', '-a'],
258+
args: [
259+
{
260+
id: 'val',
261+
type: (ctx) => {
262+
const tok = ctx.consumeToken();
263+
if (!/^[+-]?[0-9]+(?:\.[0-9]+)?$/.test(tok))
264+
throw 'invalid number: ' + tok;
265+
return +tok;
266+
},
267+
optional: true,
268+
default: 0,
269+
}
270+
]
271+
},
272+
{
273+
id: 'verbose',
274+
names: ['-v', '--verbose'],
275+
}
276+
],
277+
args: [
278+
{
279+
id: 'arg1S',
280+
type: (ctx) => ctx.consumeToken(),
281+
},
282+
{
283+
id: 'arg2B',
284+
type: (ctx) => {
285+
const tok = ctx.consumeToken();
286+
const val = ['false', 'true'].indexOf(tok);
287+
if (val == -1) throw 'invalid boolean: ' + tok;
288+
return !!val;
289+
},
290+
},
291+
{
292+
id: 'arg3N',
293+
type: (ctx) => +ctx.consumeToken(),
294+
optional: true,
295+
default: 391
296+
}
297+
],
298+
subverbs: [
299+
{
300+
id: 'sub',
301+
name: 'sub',
302+
args: [
303+
{
304+
id: 'subArg',
305+
type: (ctx) => ctx.consumeToken(),
306+
optional: true,
307+
}
308+
],
309+
options: [
310+
{
311+
id: 'subOpt',
312+
names: ['--sub', '-s'],
313+
args: [
314+
{
315+
id: 'subOptArg',
316+
type: (ctx) => ctx.consumeToken(),
317+
}
318+
]
319+
}
320+
]
321+
}
322+
]
323+
};
197324

198325
const ctx = new ParseContext(scriptArgs.slice(1));
199326
const result = new ParseResult();
200327

201-
while (!ctx.isEndOfTokens) {
202-
parseLongOption(ctx, result, info);
203-
if (ctx.currentToken?.[1] != '-')
204-
parseShortOption(ctx, result, info);
205-
}
328+
parseVerb(ctx, result, info);
206329
console.log(JSON.stringify(result.getMap()));

src/sifter/state.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -88,12 +88,13 @@ export class ParseContext
8888

8989
/**
9090
* Returns the current token and advance the position pointer.
91-
* @returns The current token string.
91+
* @returns The current (consumed) token string.
92+
* @throws When there's no more tokens to consume.
9293
*/
9394
public consumeToken(): string
9495
{
9596
if (this.isEndOfTokens)
96-
return null;
97+
throw 'Unexpected end of input';
9798
return this._current.tokens[this._current.position++];
9899
}
99100

@@ -261,8 +262,4 @@ export type ParseState = {
261262
* the first option argument.
262263
*/
263264
reqFirstOptArg?: boolean,
264-
/**
265-
* Whether to stop parsing options.
266-
*/
267-
stopOptions?: boolean,
268265
};

0 commit comments

Comments
 (0)