Skip to content

Commit da9fed9

Browse files
AmirAmir
authored andcommitted
feat(commander): support positional argument completions
1 parent e5fb3df commit da9fed9

4 files changed

Lines changed: 202 additions & 8 deletions

File tree

examples/demo.commander.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ program
1818
])
1919
);
2020

21+
program.argument('[project]', 'Project name');
22+
2123
// Add commands
2224
const devCommand = program
2325
.command('dev')
@@ -81,6 +83,14 @@ program
8183
console.log('Linting files...');
8284
});
8385

86+
// Command with multiple required positional arguments
87+
program
88+
.command('copy <source> <destination>')
89+
.description('Copy files')
90+
.action((source, destination) => {
91+
console.log(`Copying ${source} to ${destination}...`);
92+
});
93+
8494
// Initialize tab completion
8595
const completion = tab(program);
8696

@@ -129,5 +139,48 @@ if (devCommandInstance) {
129139
}
130140
}
131141

142+
// Positional argument on root command
143+
const projectArg = completion.arguments.get('project');
144+
if (projectArg) {
145+
projectArg.handler = (complete) => {
146+
complete('my-app', 'My application');
147+
complete('my-lib', 'My library');
148+
complete('my-tool', 'My tool');
149+
};
150+
}
151+
152+
// Positional arguments on lint command
153+
const lintCommandInstance = completion.commands.get('lint');
154+
if (lintCommandInstance) {
155+
const filesArg = lintCommandInstance.arguments.get('files');
156+
if (filesArg) {
157+
filesArg.handler = (complete) => {
158+
complete('main.ts', 'Main file');
159+
complete('index.ts', 'Index file');
160+
};
161+
}
162+
}
163+
164+
// Positional arguments on copy command
165+
const copyCommandInstance = completion.commands.get('copy');
166+
if (copyCommandInstance) {
167+
const sourceArg = copyCommandInstance.arguments.get('source');
168+
if (sourceArg) {
169+
sourceArg.handler = (complete) => {
170+
complete('src/', 'Source directory');
171+
complete('dist/', 'Distribution directory');
172+
complete('public/', 'Public assets');
173+
};
174+
}
175+
const destinationArg = copyCommandInstance.arguments.get('destination');
176+
if (destinationArg) {
177+
destinationArg.handler = (complete) => {
178+
complete('build/', 'Build output');
179+
complete('release/', 'Release directory');
180+
complete('backup/', 'Backup location');
181+
};
182+
}
183+
}
184+
132185
// Parse command line arguments
133186
program.parse();

src/commander.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { Command as CommanderCommand } from 'commander';
2-
import t, { type RootCommand } from './t';
2+
import t, { Command as TabCommand, type RootCommand } from './t';
33

44
// rawArgs is available on (just) the Commander root command, but is not included in the TypeScript types.
55
interface CommandWithRawArgs extends CommanderCommand {
@@ -165,6 +165,25 @@ function processRootCommand(command: CommanderCommand): void {
165165
registerOption(t, flags, longFlag, option.description || '', shortFlag);
166166
}
167167
}
168+
169+
processArguments(t, command);
170+
}
171+
172+
function processArguments(tabCommand: TabCommand, cmd: CommanderCommand): void {
173+
for (const arg of cmd.registeredArguments) {
174+
const choices = arg.argChoices;
175+
if (choices?.length) {
176+
tabCommand.argument(
177+
arg.name(),
178+
(complete) => {
179+
for (const choice of choices) complete(choice, '');
180+
},
181+
arg.variadic
182+
);
183+
} else {
184+
tabCommand.argument(arg.name(), undefined, arg.variadic);
185+
}
186+
}
168187
}
169188

170189
function processSubcommands(rootCommand: CommanderCommand): void {
@@ -198,6 +217,8 @@ function processSubcommands(rootCommand: CommanderCommand): void {
198217
);
199218
}
200219
}
220+
221+
processArguments(command, cmd);
201222
}
202223
}
203224

tests/__snapshots__/cli.test.ts.snap

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -717,6 +717,35 @@ exports[`cli completion tests for commander > cli option value handling > should
717717
"
718718
`;
719719

720+
exports[`cli completion tests for commander > copy command argument handlers > should complete destination argument with build suggestions 1`] = `
721+
"build/ Build output
722+
release/ Release directory
723+
backup/ Backup location
724+
:4
725+
"
726+
`;
727+
728+
exports[`cli completion tests for commander > copy command argument handlers > should complete source argument with directory suggestions 1`] = `
729+
"src/ Source directory
730+
dist/ Distribution directory
731+
public/ Public assets
732+
:4
733+
"
734+
`;
735+
736+
exports[`cli completion tests for commander > copy command argument handlers > should filter destination suggestions when typing partial input 1`] = `
737+
"build/ Build output
738+
backup/ Backup location
739+
:4
740+
"
741+
`;
742+
743+
exports[`cli completion tests for commander > copy command argument handlers > should filter source suggestions when typing partial input 1`] = `
744+
"src/ Source directory
745+
:4
746+
"
747+
`;
748+
720749
exports[`cli completion tests for commander > edge case completions for end with space > should keep suggesting the --port option if user typed partial but didn't end with space 1`] = `
721750
"--port Specify port
722751
:4
@@ -737,6 +766,96 @@ exports[`cli completion tests for commander > edge case completions for end with
737766
"
738767
`;
739768

769+
exports[`cli completion tests for commander > lint command argument handlers > should complete files argument with file suggestions 1`] = `
770+
"main.ts Main file
771+
index.ts Index file
772+
:4
773+
"
774+
`;
775+
776+
exports[`cli completion tests for commander > lint command argument handlers > should continue completing variadic files argument after first file 1`] = `
777+
"main.ts Main file
778+
index.ts Index file
779+
:4
780+
"
781+
`;
782+
783+
exports[`cli completion tests for commander > lint command argument handlers > should continue completing variadic suggestions after first file 1`] = `
784+
"index.ts Index file
785+
:4
786+
"
787+
`;
788+
789+
exports[`cli completion tests for commander > lint command argument handlers > should filter file suggestions when typing partial input 1`] = `
790+
"main.ts Main file
791+
:4
792+
"
793+
`;
794+
795+
exports[`cli completion tests for commander > positional argument completions > should complete multiple positional arguments when ending with part of the value 1`] = `
796+
"index.ts Index file
797+
:4
798+
"
799+
`;
800+
801+
exports[`cli completion tests for commander > positional argument completions > should complete multiple positional arguments when ending with space 1`] = `
802+
"main.ts Main file
803+
index.ts Index file
804+
:4
805+
"
806+
`;
807+
808+
exports[`cli completion tests for commander > positional argument completions > should complete single positional argument when ending with space 1`] = `
809+
"main.ts Main file
810+
index.ts Index file
811+
:4
812+
"
813+
`;
814+
815+
exports[`cli completion tests for commander > root command argument tests > should complete root command project argument 1`] = `
816+
"dev Start dev server
817+
serve Start the server
818+
build Build the project
819+
deploy Deploy the application
820+
lint Lint source files
821+
copy Copy files
822+
my-app My application
823+
my-lib My library
824+
my-tool My tool
825+
:4
826+
"
827+
`;
828+
829+
exports[`cli completion tests for commander > root command argument tests > should complete root command project argument after options 1`] = `
830+
"dev Start dev server
831+
serve Start the server
832+
build Build the project
833+
deploy Deploy the application
834+
lint Lint source files
835+
copy Copy files
836+
my-app My application
837+
my-lib My library
838+
my-tool My tool
839+
:4
840+
"
841+
`;
842+
843+
exports[`cli completion tests for commander > root command argument tests > should complete root command project argument with options and partial input 1`] = `
844+
"my-app My application
845+
my-lib My library
846+
my-tool My tool
847+
:4
848+
"
849+
`;
850+
851+
exports[`cli completion tests for commander > root command argument tests > should complete root command project argument with partial input 1`] = `
852+
"my-app My application
853+
my-lib My library
854+
my-tool My tool
855+
:4
856+
"
857+
`;
858+
740859
exports[`cli completion tests for commander > root command option tests > should complete root command --logLevel option values 1`] = `
741860
"info Info level
742861
warn Warn level
@@ -829,6 +948,10 @@ serve Start the server
829948
build Build the project
830949
deploy Deploy the application
831950
lint Lint source files
951+
copy Copy files
952+
my-app My application
953+
my-lib My library
954+
my-tool My tool
832955
:4
833956
"
834957
`;

tests/cli.test.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,6 @@ function runCommand(command: string): Promise<string> {
1616
const cliTools = ['t', 'citty', 'cac', 'commander'];
1717

1818
describe.each(cliTools)('cli completion tests for %s', (cliTool) => {
19-
// Commander does not have custom completions for arguments yet.
20-
const shouldSkipTest = cliTool === 'commander';
21-
2219
const commandPrefix = `pnpm tsx examples/demo.${cliTool}.ts complete --`;
2320

2421
it('should complete cli options', async () => {
@@ -230,7 +227,7 @@ describe.each(cliTools)('cli completion tests for %s', (cliTool) => {
230227
});
231228
});
232229

233-
describe.runIf(!shouldSkipTest)('root command argument tests', () => {
230+
describe('root command argument tests', () => {
234231
it('should complete root command project argument', async () => {
235232
const command = `${commandPrefix} ""`;
236233
const output = await runCommand(command);
@@ -352,7 +349,7 @@ describe.each(cliTools)('cli completion tests for %s', (cliTool) => {
352349
});
353350
});
354351

355-
describe.runIf(!shouldSkipTest)('positional argument completions', () => {
352+
describe('positional argument completions', () => {
356353
it('should complete multiple positional arguments when ending with space', async () => {
357354
const command = `${commandPrefix} lint ""`;
358355
const output = await runCommand(command);
@@ -372,7 +369,7 @@ describe.each(cliTools)('cli completion tests for %s', (cliTool) => {
372369
});
373370
});
374371

375-
describe.runIf(!shouldSkipTest)('copy command argument handlers', () => {
372+
describe('copy command argument handlers', () => {
376373
it('should complete source argument with directory suggestions', async () => {
377374
const command = `${commandPrefix} copy ""`;
378375
const output = await runCommand(command);
@@ -398,7 +395,7 @@ describe.each(cliTools)('cli completion tests for %s', (cliTool) => {
398395
});
399396
});
400397

401-
describe.runIf(!shouldSkipTest)('lint command argument handlers', () => {
398+
describe('lint command argument handlers', () => {
402399
it('should complete files argument with file suggestions', async () => {
403400
const command = `${commandPrefix} lint ""`;
404401
const output = await runCommand(command);

0 commit comments

Comments
 (0)