Skip to content

Commit 67bf7ee

Browse files
committed
feat: aliases
1 parent 0b08260 commit 67bf7ee

File tree

9 files changed

+122
-69
lines changed

9 files changed

+122
-69
lines changed

src/args.ts

Lines changed: 29 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,7 @@ import { InternalCommand, InternalArgument, CoercedValue, InternalPositionalArgu
88
import { Err, Ok, Result } from './internal/result'
99
import { ParserOpts, StoredParserOpts, defaultParserOpts } from './opts'
1010
import { PrefixTree } from './internal/prefix-tree'
11-
12-
const flagValidationRegex = /-+(?:[a-z]+)/
11+
import { getAliasDenotion, internaliseFlagString } from './internal/util'
1312

1413
// What happened when we parsed
1514
interface FoundCommand {
@@ -155,70 +154,55 @@ export class Args<TArgTypes extends DefaultArgTypes = DefaultArgTypes> {
155154
}
156155

157156
public arg<TArg extends CoercedValue, TLong extends string> (
158-
[longFlag, shortFlag]: [`--${TLong}`, `-${string}`?],
157+
[_longFlag, ..._aliases]: [`--${TLong}`, ...Array<`-${string}` | `--${string}`>],
159158
declaration: MinimalArgument<TArg>
160159
): Args<TArgTypes & {
161160
// Add the key to our object of known args
162161
[key in TLong]: TArg
163162
}> {
164-
if (!longFlag.startsWith('--')) {
165-
throw new SchemaError(`long flags must start with '--', got '${longFlag}'`)
166-
}
163+
const [,longFlag] = internaliseFlagString(_longFlag)
164+
const aliases = _aliases.map(a => {
165+
const [type, value] = internaliseFlagString(a)
166+
return {
167+
type,
168+
value
169+
}
170+
})
167171

168-
if (this.arguments.has(longFlag.substring(2))) {
169-
throw new SchemaError(`duplicate long flag '${longFlag}'`)
172+
if (this.arguments.has(longFlag)) {
173+
throw new SchemaError(`duplicate long flag '${_longFlag}'`)
170174
}
171175

172-
if (!flagValidationRegex.test(longFlag)) {
173-
throw new SchemaError(`long flags must match '--abcdef...' got '${longFlag}'`)
176+
for (const alias of aliases) {
177+
if (this.arguments.has(alias.value)) {
178+
throw new SchemaError(`duplicate alias '${getAliasDenotion(alias)}'`)
179+
}
180+
181+
this.arguments.insert(alias.value, {
182+
type: 'flag',
183+
isLongFlag: true,
184+
inner: declaration,
185+
longFlag,
186+
aliases
187+
})
174188
}
175189

176-
this.arguments.insert(longFlag.substring(2), {
190+
this.arguments.insert(longFlag, {
177191
type: 'flag',
178192
isLongFlag: true,
179193
inner: declaration,
180-
longFlag: longFlag.substring(2),
181-
shortFlag: shortFlag?.substring(1)
194+
longFlag,
195+
aliases
182196
})
183197

184198
this.argumentsList.push({
185199
type: 'flag',
186200
isLongFlag: true,
187201
inner: declaration,
188-
longFlag: longFlag.substring(2),
189-
shortFlag: shortFlag?.substring(1)
202+
longFlag,
203+
aliases
190204
})
191205

192-
if (shortFlag) {
193-
if (!shortFlag.startsWith('-')) {
194-
throw new SchemaError(`short flags must start with '-', got '${shortFlag}'`)
195-
}
196-
197-
if (this.arguments.has(shortFlag.substring(1))) {
198-
throw new SchemaError(`duplicate short flag '${shortFlag}'`)
199-
}
200-
201-
if (!flagValidationRegex.test(shortFlag)) {
202-
throw new SchemaError(`short flags must match '-abcdef...' got '${shortFlag}'`)
203-
}
204-
205-
this.arguments.insert(shortFlag.substring(1), {
206-
type: 'flag',
207-
inner: declaration,
208-
isLongFlag: false,
209-
longFlag: longFlag.substring(2),
210-
shortFlag: shortFlag.substring(1)
211-
})
212-
213-
this.argumentsList.push({
214-
type: 'flag',
215-
inner: declaration,
216-
isLongFlag: false,
217-
longFlag: longFlag.substring(2),
218-
shortFlag: shortFlag.substring(1)
219-
})
220-
}
221-
222206
// @ts-expect-error can't infer this because of weird subtyping, not a priority
223207
return this
224208
}

src/internal/parse/coerce.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,14 @@ function validateFlagSchematically (
3434
resolveres: Resolver[]
3535
): Result<AnyParsedFlagArgument[] | undefined, CoercionError> {
3636
let foundFlags = flags.get(argument.longFlag)
37-
if (argument.shortFlag && !foundFlags) {
38-
foundFlags = flags.get(argument.shortFlag)
37+
if (argument.aliases.length && !foundFlags) {
38+
for (const alias of argument.aliases) {
39+
foundFlags = flags.get(alias.value)
40+
41+
if (foundFlags) {
42+
break
43+
}
44+
}
3945
}
4046

4147
let { specifiedDefault, unspecifiedDefault, optional, dependencies, conflicts, exclusive, requiredUnlessPresent } = argument.inner._meta
@@ -333,7 +339,7 @@ async function resolveArgumentDefault (
333339
isDefault: true,
334340
value: {
335341
isMulti: false,
336-
raw: `<default value for group member '${argument.shortFlag}' of '${userArgument.rawInput}'`,
342+
raw: `<default value for group member '${argument.longFlag}' of '${userArgument.rawInput}'`,
337343
coerced: argument.inner._meta.specifiedDefault
338344
}
339345
})

src/internal/parse/types.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,16 @@ interface ArgumentBase {
55
inner: MinimalArgument<CoercedValue>
66
}
77

8+
export interface FlagAlias {
9+
type: 'long' | 'short'
10+
value: string
11+
}
12+
813
export interface InternalFlagArgument extends ArgumentBase {
914
type: 'flag'
1015
isLongFlag: boolean
1116
longFlag: string
12-
shortFlag: string | undefined
17+
aliases: FlagAlias[]
1318
}
1419

1520
export interface InternalPositionalArgument extends ArgumentBase {
@@ -25,7 +30,7 @@ export interface InternalCommand {
2530
isBase: boolean
2631
aliases: string[]
2732
inner: Command
28-
parser: Args<unknown>
33+
parser: Args<{}>
2934
}
3035

3136
export interface ParsedPair {

src/internal/util.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { InternalArgument } from './parse/types'
1+
import { InternalError, SchemaError } from '../error'
2+
import { FlagAlias, InternalArgument } from './parse/types'
23

34
export function getArgDenotion (argument: InternalArgument): string {
45
if (argument.type === 'flag') {
@@ -7,3 +8,30 @@ export function getArgDenotion (argument: InternalArgument): string {
78
return `<${argument.key}>`
89
}
910
}
11+
12+
export function getAliasDenotion (alias: FlagAlias): string {
13+
if (alias.type === 'long') {
14+
return `--${alias.value}`
15+
} else {
16+
return `-${alias.value}`
17+
}
18+
}
19+
20+
const flagValidationRegex = /-+(?:[a-z]+)/
21+
22+
export function internaliseFlagString (flag: string): ['long' | 'short', string] {
23+
if (!flagValidationRegex.test(flag)) {
24+
throw new SchemaError(`flags must match '--abcdef...' or '-abcdef' got '${flag}'`)
25+
}
26+
27+
// Long flag
28+
if (flag.startsWith('--')) {
29+
return ['long', flag.substring(2)]
30+
}
31+
32+
if (flag.startsWith('-')) {
33+
return ['short', flag.substring(1)]
34+
}
35+
36+
throw new InternalError('impossible')
37+
}

src/util/help.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Args } from '../args'
22
import { InternalArgument } from '../internal/parse/types'
3+
import { getAliasDenotion } from '../internal/util'
34

45
/**
56
* Generate a help string from a parser schema.
@@ -17,11 +18,11 @@ export function generateHelp (parser: Args<{}>): string {
1718
return `[<${value.key.toUpperCase()}>]`
1819
}
1920

20-
if (value.shortFlag) {
21+
if (value.aliases.length) {
2122
if (isMultiType) {
22-
return `[--${value.longFlag} | -${value.shortFlag} <${value.inner.type}...>]`
23+
return `[--${value.longFlag}${value.aliases.map(getAliasDenotion).join(' | ')}<${value.inner.type}...>]`
2324
}
24-
return `[--${value.longFlag} | -${value.shortFlag} <${value.inner.type}>]`
25+
return `[--${value.longFlag}${value.aliases.map(getAliasDenotion).join(' | ')}<${value.inner.type}>]`
2526
}
2627
return `[--${value.longFlag} <${value.inner.type}>]`
2728
} else {
@@ -32,11 +33,11 @@ export function generateHelp (parser: Args<{}>): string {
3233
return `<${value.key.toUpperCase()}>`
3334
}
3435

35-
if (value.shortFlag) {
36+
if (value.aliases.length) {
3637
if (isMultiType) {
37-
return `(--${value.longFlag} | -${value.shortFlag} <${value.inner.type}...>)`
38+
return `(--${value.longFlag}${value.aliases.map(getAliasDenotion).join(' | ')}<${value.inner.type}...>)`
3839
}
39-
return `(--${value.longFlag} | -${value.shortFlag} <${value.inner.type}>)`
40+
return `(--${value.longFlag}${value.aliases.map(getAliasDenotion).join(' | ')}<${value.inner.type}>)`
4041
}
4142

4243
return `(--${value.longFlag} <${value.inner.type}>)`

test/integrations/simple.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,35 @@ import { Args } from '../../src'
44
import { a } from '../../src/builder'
55
import { parserOpts } from '../shared'
66

7+
describe('Alias integrations', () => {
8+
it('can parse a long-flag alias', async () => {
9+
const parser = new Args(parserOpts)
10+
.arg(['--long-flag', '--alias'], a.bool())
11+
12+
const result = await runArgsExecution(parser, '--alias')
13+
expect(result['long-flag']).toBe(true)
14+
expect(result.alias).toBe(undefined)
15+
})
16+
17+
it('can parse a short-flag alias', async () => {
18+
const parser = new Args(parserOpts)
19+
.arg(['--long-flag', '-a'], a.bool())
20+
21+
const result = await runArgsExecution(parser, '-a')
22+
expect(result['long-flag']).toBe(true)
23+
expect(result.alias).toBe(undefined)
24+
})
25+
26+
it('can falls back to unspecified default when nothing is given', async () => {
27+
const parser = new Args(parserOpts)
28+
.arg(['--long-flag', '-a'], a.bool())
29+
30+
const result = await runArgsExecution(parser, '')
31+
expect(result['long-flag']).toBe(false)
32+
expect(result.alias).toBe(undefined)
33+
})
34+
})
35+
736
describe('Flag integrations', () => {
837
it('can parse a long-flag flag', async () => {
938
const parser = new Args(parserOpts)

test/parsing/utils.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { MinimalArgument, StoredParserOpts, defaultCommandOpts } from '../../src
44
import { CoercedArguments, coerce } from '../../src/internal/parse/coerce'
55
import { tokenise } from '../../src/internal/parse/lexer'
66
import { ParsedArguments, parse } from '../../src/internal/parse/parser'
7-
import { CoercedValue, InternalArgument, InternalCommand } from '../../src/internal/parse/types'
7+
import { CoercedValue, FlagAlias, InternalArgument, InternalCommand } from '../../src/internal/parse/types'
88
import { PrefixTree } from '../../src/internal/prefix-tree'
99

1010
export function makeInternalCommand (
@@ -36,17 +36,17 @@ export function makeInternalCommand (
3636
}
3737

3838
export function makeInternalFlag (
39-
{ isPrimary, long, short, inner }: {
39+
{ isPrimary, long, aliases, inner }: {
4040
isPrimary: boolean
4141
long: string
42-
short?: string
42+
aliases?: FlagAlias[]
4343
inner: MinimalArgument<CoercedValue>
4444
}): InternalArgument {
4545
return {
4646
type: 'flag',
4747
isLongFlag: isPrimary,
4848
longFlag: long,
49-
shortFlag: short,
49+
aliases: aliases ?? [],
5050
inner
5151
}
5252
}
@@ -89,8 +89,8 @@ export async function parseAndCoerce (argStr: string, opts: StoredParserOpts, ar
8989
const argMap = args.reduce<PrefixTree<InternalArgument>>((acc, val) => {
9090
if (val.type === 'flag') {
9191
acc.insert(val.longFlag, val)
92-
if (val.shortFlag) {
93-
acc.insert(val.shortFlag, val)
92+
if (val.aliases) {
93+
val.aliases.forEach(a => acc.insert(a.value, val))
9494
}
9595
} else {
9696
acc.insert(val.key, val)

test/schema/validation.test.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ describe('Schema validation', () => {
3030
expect(() => {
3131
// @ts-expect-error we are testing runtime validation, for JS users, or people who dont like playing by the rules
3232
parser.arg(['-1'], a.string())
33-
}).toThrowErrorMatchingInlineSnapshot(`"long flags must start with '--', got '-1'"`)
33+
}).toThrowErrorMatchingInlineSnapshot(`"flags must match '--abcdef...' or '-abcdef' got '-1'"`)
3434
})
3535

3636
it('rejects positionals not prefixed by <', () => {
@@ -57,23 +57,23 @@ describe('Schema validation', () => {
5757
expect(() => {
5858
// @ts-expect-error we are testing runtime validation, for JS users, or people who dont like playing by the rules
5959
parser.arg(['--flag', '1'], a.string())
60-
}).toThrowErrorMatchingInlineSnapshot(`"short flags must start with '-', got '1'"`)
60+
}).toThrowErrorMatchingInlineSnapshot(`"flags must match '--abcdef...' or '-abcdef' got '1'"`)
6161
})
6262

6363
it('rejects long flags that do not have a valid ID', () => {
6464
const parser = new Args(parserOpts)
6565

6666
expect(() => {
6767
parser.arg(['--1'], a.string())
68-
}).toThrowErrorMatchingInlineSnapshot(`"long flags must match '--abcdef...' got '--1'"`)
68+
}).toThrowErrorMatchingInlineSnapshot(`"flags must match '--abcdef...' or '-abcdef' got '--1'"`)
6969
})
7070

7171
it('rejects short flags that do not have a valid ID', () => {
7272
const parser = new Args(parserOpts)
7373

7474
expect(() => {
7575
parser.arg(['--flag', '-1'], a.string())
76-
}).toThrowErrorMatchingInlineSnapshot(`"short flags must match '-abcdef...' got '-1'"`)
76+
}).toThrowErrorMatchingInlineSnapshot(`"flags must match '--abcdef...' or '-abcdef' got '-1'"`)
7777
})
7878

7979
it('rejects duplicate long flags', () => {
@@ -91,7 +91,7 @@ describe('Schema validation', () => {
9191

9292
expect(() => {
9393
parser.arg(['--flag2', '-f'], a.string())
94-
}).toThrowErrorMatchingInlineSnapshot(`"duplicate short flag '-f'"`)
94+
}).toThrowErrorMatchingInlineSnapshot(`"duplicate alias '-f'"`)
9595
})
9696

9797
it('rejects non-string input', async () => {

test/util.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ describe('Help generation utils', () => {
137137
expect(util.generateHelp(parser)).toMatchInlineSnapshot(`
138138
"program-name - program description
139139
140-
Usage: program-name [--flag | -f <string>] [--opt-multi | -o <string...>] (--opt-req | -r <string...>) (--enum | -e <a | b | c>) (--long <number>) [--long-optional <number>] <POSITIONALREQ> [<POSITIONAL>] <POSMULTI...>
140+
Usage: program-name [--flag-f<string>] [--opt-multi-o<string...>] (--opt-req-r<string...>) (--enum-e<a | b | c>) (--long <number>) [--long-optional <number>] <POSITIONALREQ> [<POSITIONAL>] <POSMULTI...>
141141
142142
Commands:
143143
program-name [help, nohelp] (--cmd-arg <string>)"

0 commit comments

Comments
 (0)