1/**
2 * The lexer module converts raw command-line arguments into the token stream
3 * consumed by the unstable CLI parser. It recognizes long options
4 * (`--name` / `--name=value`), short options (`-x`), clustered short options
5 * (`-abc`), option values supplied with `=`, and positional values.
6 *
7 * The `--` delimiter is handled before tokenization: everything after it is
8 * returned as trailing operands so values that look like options are preserved
9 * exactly. A lone `-` is also preserved as a value instead of being interpreted
10 * as an option, as are negative numbers such as `-3` or `-3.70`.
11 */
12/**
13 * @internal
14 */
15export type Token =
16 | { _tag: "LongOption"; name: string; raw: string; value?: string }
17 | { _tag: "ShortOption"; flag: string; raw: string; value?: string }
18 | { _tag: "Value"; value: string }
19
20/**
21 * @internal
22 */
23export interface LexResult {
24 readonly tokens: ReadonlyArray<Token>
25 readonly trailingOperands: ReadonlyArray<string>
26}
27
28/**
29 * @internal
30 */
31export function lex(argv: ReadonlyArray<string>): LexResult {
32 const endIndex = argv.indexOf("--")
33
34 if (endIndex === -1) {
35 // No -- delimiter, lex everything normally
36 return {
37 tokens: lexTokens(argv),
38 trailingOperands: []
39 }
40 }
41
42 // Split at -- delimiter
43 return {
44 tokens: lexTokens(argv.slice(0, endIndex)),
45 trailingOperands: argv.slice(endIndex + 1)
46 }
47}
48
49const negativeNumberRegex = /^-(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?$/
50
51const lexTokens = (args: ReadonlyArray<string>): ReadonlyArray<Token> => {
52 const tokens: Array<Token> = []
53
54 for (const arg of args) {
55 if (!arg.startsWith("-") || negativeNumberRegex.test(arg)) {
56 tokens.push({ _tag: "Value", value: arg })
57 } else if (arg.startsWith("--")) {
58 const equalIndex = arg.indexOf("=")
59 if (equalIndex !== -1) {
60 const name = arg.slice(2, equalIndex)
61 const value = arg.slice(equalIndex + 1)
62 tokens.push({ _tag: "LongOption", name, raw: arg, value })
63 } else {
64 tokens.push({ _tag: "LongOption", name: arg.slice(2), raw: arg })
65 }
66 } else if (arg.length > 1) {
67 const flags = arg.slice(1)
68 const equalIndex = flags.indexOf("=")
69 if (equalIndex !== -1) {
70 const flag = flags.slice(0, equalIndex)
71 const value = flags.slice(equalIndex + 1)
72 tokens.push({ _tag: "ShortOption", flag, raw: `-${flag}`, value })
73 } else {
74 for (const ch of flags) {
75 tokens.push({ _tag: "ShortOption", flag: ch, raw: `-${ch}` })
76 }
77 }
78 } else {
79 tokens.push({ _tag: "Value", value: arg })
80 }
81 }
82
83 return tokens
84}