Skip to content

Commit 44a2993

Browse files
committed
feat: add short flag support for CLI options and enhance completion handling
1 parent 4595b27 commit 44a2993

File tree

6 files changed

+152
-15
lines changed

6 files changed

+152
-15
lines changed

demo.cac.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ cli
1010

1111
cli
1212
.command('dev', 'Start dev server')
13-
.option('--host [host]', `Specify hostname`)
14-
.option('--port <port>', `Specify port`)
13+
.option('-H, --host [host]', `Specify hostname`)
14+
.option('-p, --port <port>', `Specify port`)
1515
.action((options) => {});
1616

1717
cli.command('dev build', 'Build project').action((options) => {});

src/cac.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,15 @@ export default function tab(instance: CAC): Completion {
3939

4040
// Add command options
4141
for (const option of [...instance.globalCommand.options, ...cmd.options]) {
42+
// Extract short flag from the name if it exists (e.g., "-c, --config" -> "c")
43+
const shortFlag = option.name.match(/^-([a-zA-Z]), --/)?.[1];
44+
4245
completion.addOption(
4346
commandName,
44-
`--${option.name}`,
47+
`--${option.name.replace(/^-[a-zA-Z], --/, '')}`, // Remove the short flag part if it exists
4548
option.description || '',
46-
async () => []
49+
async () => [],
50+
shortFlag
4751
);
4852
}
4953
}

src/citty.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,13 +65,19 @@ async function handleSubCommands<T extends ArgsDef = ArgsDef>(
6565
if (conf.type === 'positional') {
6666
continue;
6767
}
68+
// Extract alias from the config if it exists
69+
const shortFlag = typeof conf === 'object' && 'alias' in conf ?
70+
(Array.isArray(conf.alias) ? conf.alias[0] : conf.alias) :
71+
undefined;
72+
6873
completion.addOption(
6974
name,
7075
`--${argName}`,
7176
conf.description ?? '',
7277
async (previousArgs, toComplete, endsWithSpace) => {
7378
return [];
74-
}
79+
},
80+
shortFlag
7581
);
7682
}
7783
}

src/index.ts

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ type Handler = (
7575
type Option = {
7676
description: string;
7777
handler: Handler;
78+
alias?: string;
7879
};
7980

8081
type Command = {
@@ -117,13 +118,14 @@ export class Completion {
117118
command: string,
118119
option: string,
119120
description: string,
120-
handler: Handler
121+
handler: Handler,
122+
alias?: string
121123
) {
122124
const cmd = this.commands.get(command);
123125
if (!cmd) {
124126
throw new Error(`Command ${command} not found.`);
125127
}
126-
cmd.options.set(option, { description, handler });
128+
cmd.options.set(option, { description, handler, alias });
127129
return option;
128130
}
129131

@@ -233,7 +235,12 @@ export class Completion {
233235
toComplete: string,
234236
endsWithSpace: boolean
235237
): boolean {
236-
return lastPrevArg?.startsWith('--') || toComplete.startsWith('--');
238+
return (
239+
lastPrevArg?.startsWith('--') ||
240+
lastPrevArg?.startsWith('-') ||
241+
toComplete.startsWith('--') ||
242+
toComplete.startsWith('-')
243+
);
237244
}
238245

239246
private shouldCompleteCommands(
@@ -255,17 +262,29 @@ export class Completion {
255262
let valueToComplete = toComplete;
256263

257264
if (toComplete.includes('=')) {
258-
// Handle --flag=value case
265+
// Handle --flag=value or -f=value case
259266
const parts = toComplete.split('=');
260267
flagName = parts[0];
261268
valueToComplete = parts[1] || '';
262-
} else if (lastPrevArg?.startsWith('--')) {
263-
// Handle --flag value case
269+
} else if (lastPrevArg?.startsWith('-')) {
270+
// Handle --flag value or -f value case
264271
flagName = lastPrevArg;
265272
}
266273

267274
if (flagName) {
268-
const option = command.options.get(flagName);
275+
// Try to find the option by long name or alias
276+
let option = command.options.get(flagName);
277+
if (!option) {
278+
// If not found by direct match, try to find by alias
279+
for (const [name, opt] of command.options) {
280+
if (opt.alias && `-${opt.alias}` === flagName) {
281+
option = opt;
282+
flagName = name; // Use the long name for completion
283+
break;
284+
}
285+
}
286+
}
287+
269288
if (option) {
270289
const suggestions = await option.handler(
271290
previousArgs,
@@ -286,9 +305,21 @@ export class Completion {
286305
}
287306

288307
// Handle flag name completion
289-
if (toComplete.startsWith('--')) {
308+
if (toComplete.startsWith('-')) {
309+
const isShortFlag = toComplete.startsWith('-') && !toComplete.startsWith('--');
310+
290311
for (const [name, option] of command.options) {
291-
if (name.startsWith(toComplete)) {
312+
// For short flags (-), only show aliases
313+
if (isShortFlag) {
314+
if (option.alias && `-${option.alias}`.startsWith(toComplete)) {
315+
this.completions.push({
316+
value: `-${option.alias}`,
317+
description: option.description,
318+
});
319+
}
320+
}
321+
// For long flags (--), show the full names
322+
else if (name.startsWith(toComplete)) {
292323
this.completions.push({
293324
value: name,
294325
description: option.description,

tests/__snapshots__/cli.test.ts.snap

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,16 @@ exports[`cli completion tests for cac > cli option completion tests > should com
66
"
77
`;
88

9+
exports[`cli completion tests for cac > cli option completion tests > should complete option for partial input '{ partial: '-H', expected: '-H' }' 1`] = `
10+
":4
11+
"
12+
`;
13+
14+
exports[`cli completion tests for cac > cli option completion tests > should complete option for partial input '{ partial: '-p', expected: '-p' }' 1`] = `
15+
":4
16+
"
17+
`;
18+
919
exports[`cli completion tests for cac > cli option exclusion tests > should not suggest already specified option '{ specified: '--config', shouldNotContain: '--config' }' 1`] = `
1020
":4
1121
"
@@ -74,6 +84,29 @@ index.ts Index file
7484
"
7585
`;
7686

87+
exports[`cli completion tests for cac > short flag handling > should handle global short flags 1`] = `
88+
":4
89+
"
90+
`;
91+
92+
exports[`cli completion tests for cac > short flag handling > should handle short flag value completion 1`] = `
93+
":4
94+
"
95+
`;
96+
97+
exports[`cli completion tests for cac > short flag handling > should handle short flag with equals sign 1`] = `
98+
":4
99+
"
100+
`;
101+
102+
exports[`cli completion tests for cac > short flag handling > should not show duplicate options when short flag is used 1`] = `
103+
"--config Use specified config file
104+
--mode Set env mode
105+
--logLevel info | warn | error | silent
106+
:4
107+
"
108+
`;
109+
77110
exports[`cli completion tests for cac > should complete cli options 1`] = `
78111
"dev Start dev server
79112
lint Lint project
@@ -87,6 +120,16 @@ exports[`cli completion tests for citty > cli option completion tests > should c
87120
"
88121
`;
89122

123+
exports[`cli completion tests for citty > cli option completion tests > should complete option for partial input '{ partial: '-H', expected: '-H' }' 1`] = `
124+
":4
125+
"
126+
`;
127+
128+
exports[`cli completion tests for citty > cli option completion tests > should complete option for partial input '{ partial: '-p', expected: '-p' }' 1`] = `
129+
":4
130+
"
131+
`;
132+
90133
exports[`cli completion tests for citty > cli option exclusion tests > should not suggest already specified option '{ specified: '--config', shouldNotContain: '--config' }' 1`] = `
91134
":4
92135
"
@@ -135,6 +178,29 @@ exports[`cli completion tests for citty > edge case completions for end with spa
135178
"
136179
`;
137180

181+
exports[`cli completion tests for citty > short flag handling > should handle global short flags 1`] = `
182+
":4
183+
"
184+
`;
185+
186+
exports[`cli completion tests for citty > short flag handling > should handle short flag value completion 1`] = `
187+
":4
188+
"
189+
`;
190+
191+
exports[`cli completion tests for citty > short flag handling > should handle short flag with equals sign 1`] = `
192+
":4
193+
"
194+
`;
195+
196+
exports[`cli completion tests for citty > short flag handling > should not show duplicate options when short flag is used 1`] = `
197+
"--config Use specified config file
198+
--mode Set env mode
199+
--logLevel info | warn | error | silent
200+
:4
201+
"
202+
`;
203+
138204
exports[`cli completion tests for citty > should complete cli options 1`] = `
139205
"dev Start dev server
140206
lint Lint project

tests/cli.test.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,11 @@ describe.each(cliTools)('cli completion tests for %s', (cliTool) => {
2525
});
2626

2727
describe('cli option completion tests', () => {
28-
const optionTests = [{ partial: '--p', expected: '--port' }];
28+
const optionTests = [
29+
{ partial: '--p', expected: '--port' },
30+
{ partial: '-p', expected: '-p' }, // Test short flag completion
31+
{ partial: '-H', expected: '-H' }, // Test another short flag completion
32+
];
2933

3034
test.each(optionTests)(
3135
"should complete option for partial input '%s'",
@@ -100,6 +104,32 @@ describe.each(cliTools)('cli completion tests for %s', (cliTool) => {
100104
});
101105
});
102106

107+
describe('short flag handling', () => {
108+
it('should handle short flag value completion', async () => {
109+
const command = `${commandPrefix} dev -p `;
110+
const output = await runCommand(command);
111+
expect(output).toMatchSnapshot();
112+
});
113+
114+
it('should handle short flag with equals sign', async () => {
115+
const command = `${commandPrefix} dev -p=3`;
116+
const output = await runCommand(command);
117+
expect(output).toMatchSnapshot();
118+
});
119+
120+
it('should handle global short flags', async () => {
121+
const command = `${commandPrefix} -c `;
122+
const output = await runCommand(command);
123+
expect(output).toMatchSnapshot();
124+
});
125+
126+
it('should not show duplicate options when short flag is used', async () => {
127+
const command = `${commandPrefix} -c vite.config.js --`;
128+
const output = await runCommand(command);
129+
expect(output).toMatchSnapshot();
130+
});
131+
});
132+
103133
// single positional command: `lint [file]`
104134
// vite ""
105135
// -> src/

0 commit comments

Comments
 (0)