Skip to content

Commit 7d782a3

Browse files
committed
feat(@angular/cli): add support for parsing enums
Options can now contain enumerations of values.
1 parent 34818b0 commit 7d782a3

File tree

3 files changed

+99
-19
lines changed

3 files changed

+99
-19
lines changed

packages/angular/cli/models/interface.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,11 +63,11 @@ export interface CommandContext {
6363
* Value types of an Option.
6464
*/
6565
export enum OptionType {
66-
String = 'string',
67-
Number = 'number',
68-
Boolean = 'boolean',
69-
Array = 'array',
7066
Any = 'any',
67+
Array = 'array',
68+
Boolean = 'boolean',
69+
Number = 'number',
70+
String = 'string',
7171
}
7272

7373
/**
@@ -95,6 +95,15 @@ export interface Option {
9595
*/
9696
types?: OptionType[];
9797

98+
/**
99+
* If this field is set, only values contained in this field are valid. This array can be mixed
100+
* types (strings, numbers, boolean). For example, if this field is "enum: ['hello', true]",
101+
* then "type" will be either string or boolean, types will be at least both, and the values
102+
* accepted will only be either 'hello' or true (not false or any other string).
103+
* This mean that prefixing with `no-` will not work on this field.
104+
*/
105+
enum?: Value[];
106+
98107
/**
99108
* If this option maps to a subcommand in the parent command, will contain all the subcommands
100109
* supported. There is a maximum of 1 subcommand Option per command, and the type of this

packages/angular/cli/models/parser.ts

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { Arguments, Option, OptionType, Value } from './interface';
1212

1313
function _coerceType(str: string | undefined, type: OptionType, v?: Value): Value | undefined {
1414
switch (type) {
15-
case 'any':
15+
case OptionType.Any:
1616
if (Array.isArray(v)) {
1717
return v.concat(str || '');
1818
}
@@ -23,10 +23,10 @@ function _coerceType(str: string | undefined, type: OptionType, v?: Value): Valu
2323
? _coerceType(str, OptionType.Number, v)
2424
: _coerceType(str, OptionType.String, v);
2525

26-
case 'string':
26+
case OptionType.String:
2727
return str || '';
2828

29-
case 'boolean':
29+
case OptionType.Boolean:
3030
switch (str) {
3131
case 'false':
3232
return false;
@@ -40,7 +40,7 @@ function _coerceType(str: string | undefined, type: OptionType, v?: Value): Valu
4040
return undefined;
4141
}
4242

43-
case 'number':
43+
case OptionType.Number:
4444
if (str === undefined) {
4545
return 0;
4646
} else if (Number.isFinite(+str)) {
@@ -49,7 +49,7 @@ function _coerceType(str: string | undefined, type: OptionType, v?: Value): Valu
4949
return undefined;
5050
}
5151

52-
case 'array':
52+
case OptionType.Array:
5353
return Array.isArray(v) ? v.concat(str || '') : [str || ''];
5454

5555
default:
@@ -61,7 +61,20 @@ function _coerce(str: string | undefined, o: Option | null, v?: Value): Value |
6161
if (!o) {
6262
return _coerceType(str, OptionType.Any, v);
6363
} else {
64-
return _coerceType(str, o.type, v);
64+
const types = o.types || [o.type];
65+
66+
// Try all the types one by one and pick the first one that returns a value contained in the
67+
// enum. If there's no enum, just return the first one that matches.
68+
for (const type of types) {
69+
const maybeResult = _coerceType(str, type, v);
70+
if (maybeResult !== undefined) {
71+
if (!o.enum || o.enum.includes(maybeResult)) {
72+
return maybeResult;
73+
}
74+
}
75+
}
76+
77+
return undefined;
6578
}
6679
}
6780

@@ -118,10 +131,18 @@ function _assignOption(
118131
// Set it to true if it's a boolean and the next argument doesn't match true/false.
119132
const maybeOption = _getOptionFromName(key, options);
120133
if (maybeOption) {
121-
// Not of type boolean, consume the next value.
122134
value = args[0];
123-
// Only absorb it if it leads to a value.
124-
if (_coerce(value, maybeOption) !== undefined) {
135+
let shouldShift = true;
136+
137+
if (value && value.startsWith('-')) {
138+
// Verify if not having a value results in a correct parse, if so don't shift.
139+
if (_coerce(undefined, maybeOption) !== undefined) {
140+
shouldShift = false;
141+
}
142+
}
143+
144+
// Only absorb it if it leads to a better value.
145+
if (shouldShift && _coerce(value, maybeOption) !== undefined) {
125146
args.shift();
126147
} else {
127148
value = '';
@@ -134,9 +155,6 @@ function _assignOption(
134155
option = _getOptionFromName(key, options) || null;
135156
if (option) {
136157
value = arg.substring(i + 1);
137-
if (option.type === 'boolean' && _coerce(value, option) === undefined) {
138-
value = 'true';
139-
}
140158
}
141159
}
142160
if (option === null) {
@@ -150,6 +168,8 @@ function _assignOption(
150168
const v = _coerce(value, option, parsedOptions[option.name]);
151169
if (v !== undefined) {
152170
parsedOptions[option.name] = v;
171+
} else {
172+
leftovers.push(arg);
153173
}
154174
}
155175
}

packages/angular/cli/models/parser_spec.ts

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,23 @@ describe('parseArguments', () => {
1919
{ name: 'arr', aliases: [ 'a' ], type: OptionType.Array, description: '' },
2020
{ name: 'p1', positional: 0, aliases: [], type: OptionType.String, description: '' },
2121
{ name: 'p2', positional: 1, aliases: [], type: OptionType.String, description: '' },
22+
{ name: 't1', aliases: [], type: OptionType.Boolean,
23+
types: [OptionType.Boolean, OptionType.String], description: '' },
24+
{ name: 't2', aliases: [], type: OptionType.Boolean,
25+
types: [OptionType.Boolean, OptionType.Number], description: '' },
26+
{ name: 't3', aliases: [], type: OptionType.Number,
27+
types: [OptionType.Number, OptionType.Any], description: '' },
28+
{ name: 'e1', aliases: [], type: OptionType.String, enum: ['hello', 'world'], description: '' },
29+
{ name: 'e2', aliases: [], type: OptionType.String, enum: ['hello', ''], description: '' },
30+
{ name: 'e3', aliases: [], type: OptionType.Boolean,
31+
types: [OptionType.String, OptionType.Boolean], enum: ['json', true, false],
32+
description: '' },
2233
];
2334

2435
const tests: { [test: string]: Partial<Arguments> } = {
2536
'--bool': { bool: true },
26-
'--bool=1': { bool: true },
37+
'--bool=1': { '--': ['--bool=1'] },
38+
'--bool=yellow': { '--': ['--bool=yellow'] },
2739
'--bool=true': { bool: true },
2840
'--bool=false': { bool: false },
2941
'--no-bool': { bool: false },
@@ -33,7 +45,7 @@ describe('parseArguments', () => {
3345
'--b true': { bool: true },
3446
'--b false': { bool: false },
3547
'--bool --num': { bool: true, num: 0 },
36-
'--bool --num=true': { bool: true },
48+
'--bool --num=true': { bool: true, '--': ['--num=true'] },
3749
'--bool=true --num': { bool: true, num: 0 },
3850
'--bool true --num': { bool: true, num: 0 },
3951
'--bool=false --num': { bool: false, num: 0 },
@@ -51,16 +63,55 @@ describe('parseArguments', () => {
5163
'--bool val1 --etc --num val2 --v': { bool: true, num: 0, p1: 'val1', p2: 'val2',
5264
'--': ['--etc', '--v'] },
5365
'--arr=a --arr=b --arr c d': { arr: ['a', 'b', 'c'], p1: 'd' },
54-
'--arr=1 --arr --arr c d': { arr: ['1', '--arr'], p1: 'c', p2: 'd' },
66+
'--arr=1 --arr --arr c d': { arr: ['1', '', 'c'], p1: 'd' },
67+
'--arr=1 --arr --arr c d e': { arr: ['1', '', 'c'], p1: 'd', p2: 'e' },
5568
'--str=1': { str: '1' },
5669
'--hello-world=1': { helloWorld: '1' },
5770
'--hello-bool': { helloBool: true },
5871
'--helloBool': { helloBool: true },
5972
'--no-helloBool': { helloBool: false },
6073
'--noHelloBool': { helloBool: false },
74+
'--noBool': { bool: false },
6175
'-b': { bool: true },
6276
'-sb': { bool: true, str: '' },
6377
'-bs': { bool: true, str: '' },
78+
'--t1=true': { t1: true },
79+
'--t1': { t1: true },
80+
'--t1 --num': { t1: true, num: 0 },
81+
'--no-t1': { t1: false },
82+
'--t1=yellow': { t1: 'yellow' },
83+
'--no-t1=true': { '--': ['--no-t1=true'] },
84+
'--t1=123': { t1: '123' },
85+
'--t2=true': { t2: true },
86+
'--t2': { t2: true },
87+
'--no-t2': { t2: false },
88+
'--t2=yellow': { '--': ['--t2=yellow'] },
89+
'--no-t2=true': { '--': ['--no-t2=true'] },
90+
'--t2=123': { t2: 123 },
91+
'--t3=a': { t3: 'a' },
92+
'--t3': { t3: 0 },
93+
'--t3 true': { t3: true },
94+
'--e1 hello': { e1: 'hello' },
95+
'--e1=hello': { e1: 'hello' },
96+
'--e1 yellow': { p1: 'yellow', '--': ['--e1'] },
97+
'--e1=yellow': { '--': ['--e1=yellow'] },
98+
'--e1': { '--': ['--e1'] },
99+
'--e1 true': { p1: 'true', '--': ['--e1'] },
100+
'--e1=true': { '--': ['--e1=true'] },
101+
'--e2 hello': { e2: 'hello' },
102+
'--e2=hello': { e2: 'hello' },
103+
'--e2 yellow': { p1: 'yellow', e2: '' },
104+
'--e2=yellow': { '--': ['--e2=yellow'] },
105+
'--e2': { e2: '' },
106+
'--e2 true': { p1: 'true', e2: '' },
107+
'--e2=true': { '--': ['--e2=true'] },
108+
'--e3 json': { e3: 'json' },
109+
'--e3=json': { e3: 'json' },
110+
'--e3 yellow': { p1: 'yellow', e3: true },
111+
'--e3=yellow': { '--': ['--e3=yellow'] },
112+
'--e3': { e3: true },
113+
'--e3 true': { e3: true },
114+
'--e3=true': { e3: true },
64115
};
65116

66117
Object.entries(tests).forEach(([str, expected]) => {

0 commit comments

Comments
 (0)