Skip to content

Commit ca1b5bb

Browse files
authored
fix: trying to fix #34 again (#35)
1 parent c01bb7a commit ca1b5bb

File tree

4 files changed

+81
-49
lines changed

4 files changed

+81
-49
lines changed

src/CAC.ts

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { EventEmitter } from 'events'
22
import path from 'path'
3-
import mri, { Options as MriOpts } from 'mri'
3+
import mri from 'mri'
44
import Command, {
55
GlobalCommand,
66
CommandConfig,
@@ -165,8 +165,7 @@ class CAC extends EventEmitter {
165165

166166
// Search sub-commands
167167
for (const command of this.commands) {
168-
const mriOptions = getMriOptions(this.globalCommand, command)
169-
const mriResult = this.mri(argv.slice(2), mriOptions)
168+
const mriResult = this.mri(argv.slice(2), command)
170169

171170
const commandName = mriResult.args[0]
172171
if (command.isMatched(commandName)) {
@@ -185,17 +184,15 @@ class CAC extends EventEmitter {
185184
for (const command of this.commands) {
186185
if (command.name === '') {
187186
shouldParse = false
188-
const mriOptions = getMriOptions(this.globalCommand, command)
189-
const mriResult = this.mri(argv.slice(2), mriOptions)
187+
const mriResult = this.mri(argv.slice(2), command)
190188
this.setParsedInfo(mriResult, command)
191189
this.emit(`command:!`, command)
192190
}
193191
}
194192
}
195193

196194
if (shouldParse) {
197-
const globalMriOptions = getMriOptions(this.globalCommand)
198-
const mriResult = this.mri(argv.slice(2), globalMriOptions)
195+
const mriResult = this.mri(argv.slice(2))
199196
this.setParsedInfo(mriResult)
200197
}
201198

@@ -224,13 +221,25 @@ class CAC extends EventEmitter {
224221
return parsedArgv
225222
}
226223

227-
private mri(argv: string[], mriOptions: MriOpts): MriResult {
224+
private mri(
225+
argv: string[],
226+
/** Matched command */ command?: Command
227+
): MriResult {
228+
// All added options
229+
const cliOptions = [
230+
...this.globalCommand.options,
231+
...(command ? command.options : [])
232+
]
233+
const mriOptions = getMriOptions(cliOptions)
234+
235+
// Extract everything after `--` since mri doesn't support it
228236
let argsAfterDoubleDashes: string[] = []
229237
const doubleDashesIndex = argv.indexOf('--')
230238
if (doubleDashesIndex > -1) {
231239
argsAfterDoubleDashes = argv.slice(0, doubleDashesIndex)
232240
argv = argv.slice(doubleDashesIndex + 1)
233241
}
242+
234243
const parsed = mri(argv, mriOptions)
235244

236245
const args = parsed._
@@ -239,6 +248,24 @@ class CAC extends EventEmitter {
239248
const options: { [k: string]: any } = {
240249
'--': argsAfterDoubleDashes
241250
}
251+
252+
// Set option default value
253+
const ignoreDefault =
254+
command && command.config.ignoreOptionDefaultValue
255+
? command.config.ignoreOptionDefaultValue
256+
: this.globalCommand.config.ignoreOptionDefaultValue
257+
258+
if (!ignoreDefault) {
259+
for (const cliOption of cliOptions) {
260+
if (cliOption.config.default !== undefined) {
261+
for (const name of cliOption.names) {
262+
options[name] = cliOption.config.default
263+
}
264+
}
265+
}
266+
}
267+
268+
// Camelcase option names and set dot nested option values
242269
for (const key of Object.keys(parsed)) {
243270
const keys = key.split('.').map((v, i) => {
244271
return i === 0 ? camelcase(v) : v

src/Option.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export default class Option {
1111
// `required` will be a boolean for options with brackets
1212
required?: boolean
1313
config: OptionConfig
14+
negated?: boolean
1415

1516
constructor(
1617
public rawName: string,
@@ -19,18 +20,18 @@ export default class Option {
1920
) {
2021
this.config = Object.assign({}, config)
2122

22-
let negated = false
2323
this.names = removeBrackets(rawName)
2424
.split(',')
2525
.map((v: string) => {
2626
let name = v.trim().replace(/^-{1,2}/, '')
2727
if (name.startsWith('no-')) {
28-
negated = true
28+
this.negated = true
2929
name = name.replace(/^no-/, '')
3030
}
3131
return name
3232
})
33-
if (negated) {
33+
34+
if (this.negated) {
3435
this.config.default = true
3536
}
3637

src/__test__/index.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,9 @@ test('negated option', () => {
5353

5454
cli.option('--foo [foo]', 'Set foo').option('--no-foo', 'Disable foo')
5555

56-
cli.option('--no-bar', 'Disable bar')
56+
cli.option('--bar [bar]', 'Set bar').option('--no-bar', 'Disable bar')
5757

58-
const { options } = cli.parse(['node', 'bin', '--foo', 'foo'])
58+
const { options } = cli.parse(['node', 'bin', '--foo', 'foo', '--bar'])
5959
expect(options).toEqual({
6060
'--': [],
6161
foo: 'foo',

src/utils.ts

Lines changed: 40 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import Command from './Command'
1+
import Option from './Option'
22

33
export const removeBrackets = (v: string) => v.replace(/[<[].+/, '').trim()
44

@@ -35,43 +35,47 @@ export const findAllBrackets = (v: string) => {
3535
return res
3636
}
3737

38-
export const getMriOptions = (globalCommand: Command, subCommand?: Command) => {
39-
const options = [
40-
...globalCommand.options,
41-
...(subCommand ? subCommand.options : [])
42-
]
43-
const ignoreDefault =
44-
subCommand && subCommand.config.ignoreOptionDefaultValue
45-
? subCommand.config.ignoreOptionDefaultValue
46-
: globalCommand.config.ignoreOptionDefaultValue
47-
return {
48-
default: ignoreDefault
49-
? {}
50-
: options.reduce((res: { [k: string]: any }, option) => {
51-
if (option.config.default !== undefined) {
52-
// Only need to set the default value of the first name
53-
// Since mri will automatically do the rest for alias names
54-
res[option.names[0]] = option.config.default
55-
}
56-
return res
57-
}, {}),
58-
boolean: options
59-
.filter(option => option.isBoolean)
60-
.reduce((res: string[], option) => {
61-
return res.concat(option.names)
62-
}, []),
63-
alias: options.reduce((res: { [k: string]: string[] }, option) => {
64-
if (option.names.length > 1) {
65-
res[option.names[0]] = option.names.slice(1)
38+
interface MriOptions {
39+
alias: {
40+
[k: string]: string[]
41+
}
42+
boolean: string[]
43+
}
44+
45+
export const getMriOptions = (options: Option[]) => {
46+
const result: MriOptions = { alias: {}, boolean: [] }
47+
48+
for (const [index, option] of options.entries()) {
49+
// We do not set default values in mri options
50+
// Since its type (typeof) will be used to cast parsed arguments.
51+
// Which mean `--foo foo` will be parsed as `{foo: true}` if we have `{default:{foo: true}}`
52+
53+
// Set alias
54+
if (option.names.length > 1) {
55+
result.alias[option.names[0]] = option.names.slice(1)
56+
}
57+
// Set boolean
58+
if (option.isBoolean) {
59+
if (option.negated) {
60+
// For negated option
61+
// We only set it to `boolean` type when there's no string-type option with the same name
62+
const hasStringTypeOption = options.some((o, i) => {
63+
return (
64+
i !== index &&
65+
o.names.some(name => option.names.includes(name)) &&
66+
typeof o.required === 'boolean'
67+
)
68+
})
69+
if (!hasStringTypeOption) {
70+
result.boolean.push(option.names[0])
71+
}
72+
} else {
73+
result.boolean.push(option.names[0])
6674
}
67-
return res
68-
}, {}),
69-
string: options
70-
.filter(option => typeof option.required === 'boolean')
71-
.reduce((res: string[], option) => {
72-
return res.concat(option.names)
73-
}, [])
75+
}
7476
}
77+
78+
return result
7579
}
7680

7781
export const findLongest = (arr: string[]) => {

0 commit comments

Comments
 (0)