Skip to content

Commit 64545ae

Browse files
committed
feat: transform output name for options
1 parent 0c6f7c4 commit 64545ae

8 files changed

Lines changed: 238 additions & 159 deletions

File tree

README.md

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -66,22 +66,32 @@ documentation of every option.
6666
const parser = massarg({
6767
name: 'my-cli',
6868
description: "Does really amazing stuff, you wouldn't believe!",
69-
bindHelpCommand: true,
7069
}) // or: new Massarg()
7170
.main((options) => console.log('main command', options))
71+
.command({
72+
name: 'foo',
73+
description: 'a sub command',
74+
aliases: ['f'],
75+
run: (options) => console.log('foo command'),
76+
})
7277
.command(
7378
massarg({
74-
name: 'sub',
75-
description: 'a sub command',
79+
name: 'bar',
80+
description: 'another sub command',
7681
aliases: ['s'],
77-
run: (options) => console.log('sub command', options),
82+
run: (options) => console.log('bar command', options),
7883
}).option({
7984
name: 'file',
8085
description: 'Filename to use',
8186
aliases: ['f'],
8287
parse: (filename) => path.resolve(process.cwd(), filename),
8388
}),
8489
)
90+
.option({
91+
name: 'my-string',
92+
description: 'A string argument',
93+
aliases: ['s'],
94+
})
8595
.flag({
8696
name: 'flag',
8797
description: 'a flag that will be related to any command (main or sub)',
@@ -93,8 +103,12 @@ const parser = massarg({
93103
output: 'Sub command: flag is true',
94104
})
95105
.help({
96-
binName: 'my-cli-app',
97-
footer: 'Copyright © 2021 Me, Myself and I',
106+
bindCommand: true,
107+
footerText: `Copyright © ${new Date().getFullYear()} Me, Myself and I`,
108+
titleStyle: {
109+
bold: true,
110+
color: 'brightWhite',
111+
},
98112
})
99113
```
100114

@@ -122,7 +136,10 @@ $ ./mybin
122136
# Main command runs without options
123137

124138
$ ./mybin --my-string "Some string"
125-
# Main command runs with option { myString: "Some string" }
139+
# Main command runs with options { myString: "Some string" }
140+
141+
$ ./mybin foo
142+
# Foo sub command run with options {}
126143
```
127144

128145
## Commands

src/help.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ export type HelpItem = {
136136
name: string
137137
aliases: string[]
138138
description: string
139+
hidden?: boolean
139140
}
140141

141142
export class HelpGenerator {
@@ -271,13 +272,16 @@ function generateHelpTable<T extends Partial<GenerateTableCommandConfig>>(
271272
...config
272273
}: Partial<T> = {},
273274
): string {
274-
const rows = items.map((o) => {
275-
const name = `${namePrefix}${o.name}${
276-
o.aliases.length ? ` | ${aliasPrefix}${o.aliases.join(`|${aliasPrefix}`)}` : ''
277-
}`
278-
const description = o.description
279-
return { name, description }
280-
})
275+
const rows = items
276+
.map((o) => {
277+
const name = `${namePrefix}${o.name}${
278+
o.aliases.length ? ` | ${aliasPrefix}${o.aliases.join(`|${aliasPrefix}`)}` : ''
279+
}`
280+
const description = o.description
281+
const hidden = o.hidden || false
282+
return { name, description, hidden }
283+
})
284+
.filter((r) => !r.hidden)
281285
const maxNameLength = Math.max(...rows.map((o) => o.name.length))
282286
const nameStyle = (name: string) => format(name, config.nameStyle)
283287
const descStyle = (desc: string) => format(desc, config.descriptionStyle)

src/option.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { z } from 'zod'
22
import { isZodError, ParseError } from './error'
3+
import { toCamelCase } from './utils'
34

45
export const OptionConfig = <T extends z.ZodType>(type: T) =>
56
z.object({
@@ -35,6 +36,10 @@ export const OptionConfig = <T extends z.ZodType>(type: T) =>
3536
* option.
3637
*/
3738
isDefault: z.boolean().optional(),
39+
/** Whether the option is hidden. Hidden options are not displayed in the help output. */
40+
hidden: z.boolean().optional(),
41+
/** Specify a custom name for the output, which will be used when parsing the args. */
42+
outputName: z.string().optional(),
3843
})
3944
export type OptionConfig<T = unknown> = z.infer<ReturnType<typeof OptionConfig<z.ZodType<T>>>>
4045

@@ -108,6 +113,7 @@ export class MassargOption<T = unknown> {
108113
parse: (value: string) => T
109114
isArray: boolean
110115
isDefault: boolean
116+
outputName?: string
111117

112118
constructor(options: OptionConfig<T>) {
113119
OptionConfig(z.any()).parse(options)
@@ -118,6 +124,7 @@ export class MassargOption<T = unknown> {
118124
this.parse = options.parse ?? ((x) => x as unknown as T)
119125
this.isArray = options.array ?? false
120126
this.isDefault = options.isDefault ?? false
127+
this.outputName = options.outputName
121128
}
122129

123130
static fromTypedConfig<T = unknown>(config: TypedOptionConfig<T>): MassargOption<T> {
@@ -143,7 +150,7 @@ export class MassargOption<T = unknown> {
143150
argv.shift()
144151
input = argv.shift()!
145152
const value = this.parse(input)
146-
return { key: this.name, value, argv }
153+
return { key: this.outputName || toCamelCase(this.name), value, argv }
147154
} catch (e) {
148155
if (isZodError(e)) {
149156
throw new ParseError({

src/utils.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,3 +86,30 @@ export function deepMerge<T1, T2>(obj1: T1, obj2: T2): NonNullable<T1> & NonNull
8686
}
8787
return res
8888
}
89+
/**
90+
* Splits a name into words, using camelCase, PascalCase, snake_case, and kebab-case or
91+
* regular spaced strings.
92+
*/
93+
export function splitWords(str: string): string[] {
94+
return str
95+
.replace(/([a-z])([A-Z])/g, '$1 $2')
96+
.replace(/([a-zA-Z])([0-9])/g, '$1 $2')
97+
.replace(/([0-9])([a-zA-Z])/g, '$1 $2')
98+
.replace(/([a-z])([_-])/g, '$1 $2')
99+
.replace(/([_-])([a-zA-Z])/g, '$1 $2')
100+
.split(/[_-]/)
101+
.map((s) => s.trim())
102+
.filter(Boolean)
103+
}
104+
105+
export function toCamelCase(str: string): string {
106+
return splitWords(str)
107+
.map((s, i) => (i === 0 ? s : s[0].toUpperCase() + s.slice(1)))
108+
.join('')
109+
}
110+
111+
export function toPascalCase(str: string): string {
112+
return splitWords(str)
113+
.map((s) => s[0].toUpperCase() + s.slice(1))
114+
.join('')
115+
}

test/command.test.ts

Lines changed: 1 addition & 144 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ test('constructor', () => {
1010
expect(massarg(opts)).toBeInstanceOf(MassargCommand)
1111
})
1212

13-
describe('command', () => {
13+
describe('sub command', () => {
1414
test('add', () => {
1515
const command = massarg(opts)
1616
expect(command.command).toBeInstanceOf(Function)
@@ -36,149 +36,6 @@ describe('command', () => {
3636
})
3737
})
3838

39-
describe('option', () => {
40-
test('add', () => {
41-
const command = massarg(opts)
42-
expect(command.option).toBeInstanceOf(Function)
43-
expect(
44-
command.option({ name: 'test2', description: 'test2', aliases: [], defaultValue: '' }),
45-
).toBeInstanceOf(MassargCommand)
46-
})
47-
test('validate', () => {
48-
expect(() =>
49-
massarg(opts).option({
50-
name: 'test2',
51-
description: 123 as any,
52-
aliases: [],
53-
defaultValue: '',
54-
}),
55-
).toThrow('Expected string, received number')
56-
})
57-
test('add duplicate', () => {
58-
expect(() =>
59-
massarg(opts)
60-
.option({
61-
name: 'test2',
62-
description: 'test2',
63-
aliases: [],
64-
defaultValue: '',
65-
})
66-
.option({
67-
name: 'test2',
68-
description: 'test2',
69-
aliases: [],
70-
defaultValue: '',
71-
}),
72-
).toThrow('Option "test2" already exists')
73-
})
74-
test('add 2 defaults', () => {
75-
expect(() =>
76-
massarg(opts)
77-
.option({
78-
name: 'test',
79-
description: 'test2',
80-
aliases: [],
81-
isDefault: true,
82-
})
83-
.option({
84-
name: 'test2',
85-
description: 'test2',
86-
aliases: [],
87-
isDefault: true,
88-
}),
89-
).toThrow(
90-
'Option "test2" cannot be set as default because option "test" is already set as default',
91-
)
92-
})
93-
})
94-
95-
describe('flag', () => {
96-
test('add', () => {
97-
const command = massarg(opts)
98-
expect(command.flag).toBeInstanceOf(Function)
99-
expect(command.flag({ name: 'test2', description: 'test2', aliases: [] })).toBeInstanceOf(
100-
MassargCommand,
101-
)
102-
})
103-
test('add duplicate', () => {
104-
expect(() =>
105-
massarg(opts)
106-
.flag({ name: 'test2', description: 'test2', aliases: [] })
107-
.flag({ name: 'test2', description: 'test2', aliases: [] }),
108-
).toThrow('Flag "test2" already exists')
109-
})
110-
test('validate', () => {
111-
expect(() =>
112-
massarg(opts).flag({
113-
name: 'test2',
114-
description: 123 as any,
115-
aliases: [],
116-
}),
117-
).toThrow('Expected string, received number')
118-
})
119-
})
120-
121-
describe('example', () => {
122-
test('example', () => {
123-
const command = massarg(opts)
124-
expect(command.example).toBeInstanceOf(Function)
125-
expect(command.example({ description: 'test', input: '', output: '' })).toBeInstanceOf(
126-
MassargCommand,
127-
)
128-
})
129-
})
130-
131-
describe('help', () => {
132-
test('default value', () => {
133-
const command = massarg(opts)
134-
expect(command.helpConfig).toEqual(defaultHelpConfig)
135-
})
136-
137-
test('init', () => {
138-
const command = massarg(opts).help({
139-
bindOption: true,
140-
optionOptions: {
141-
namePrefix: '__',
142-
},
143-
})
144-
expect(command.help).toBeInstanceOf(Function)
145-
expect(command.helpConfig).toHaveProperty('bindOption', true)
146-
expect(command.helpConfig).toHaveProperty('optionOptions.namePrefix', '__')
147-
expect(command.helpConfig).toHaveProperty('optionOptions.aliasPrefix', '-')
148-
expect(command.helpConfig).toHaveProperty('optionOptions.nameStyle.color', 'yellow')
149-
})
150-
151-
test('binds command', () => {
152-
const command = massarg(opts).help({
153-
bindCommand: true,
154-
})
155-
expect(command.help).toBeInstanceOf(Function)
156-
expect(command.helpConfig).toHaveProperty('bindCommand', true)
157-
expect(command.commands.find((o) => o.name === 'help')).toBeTruthy()
158-
})
159-
160-
test('binds option', () => {
161-
const command = massarg(opts).help({
162-
bindOption: true,
163-
})
164-
expect(command.help).toBeInstanceOf(Function)
165-
expect(command.helpConfig).toHaveProperty('bindOption', true)
166-
expect(command.options.find((o) => o.name === 'help')).toBeTruthy()
167-
})
168-
169-
test('help string', () => {
170-
const command = massarg(opts)
171-
expect(command.helpString()).toContain(`Usage:`)
172-
})
173-
174-
test('print help', () => {
175-
const log = jest.spyOn(console, 'log').mockImplementation(() => {})
176-
const command = massarg(opts)
177-
command.printHelp()
178-
expect(log).toHaveBeenCalled()
179-
})
180-
})
181-
18239
describe('getArgs', () => {
18340
test('basic', () => {
18441
expect(massarg(opts).getArgs([])).toEqual({})

test/example.test.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { MassargCommand } from '../src/command'
2+
import { massarg } from '../src/index'
3+
4+
const opts = {
5+
name: 'test',
6+
description: 'test',
7+
}
8+
test('example', () => {
9+
const command = massarg(opts)
10+
expect(command.example).toBeInstanceOf(Function)
11+
expect(command.example({ description: 'test', input: '', output: '' })).toBeInstanceOf(
12+
MassargCommand,
13+
)
14+
})

0 commit comments

Comments
 (0)