[READ-ONLY] Mirror of https://github.com/bombshell-dev/tab. shell autocompletions for javascript CLIs
4

Configure Feed

Select the types of activity you want to include in your feed.

feat: add short flag support for CLI options and enhance completion handling (#9)

* feat: add short flag support for CLI options and enhance completion handling

authored by

Paul Valladares and committed by
GitHub
(Feb 23, 2025, 10:21 PM +0330) 978fb7b9 4595b27c

+156 -15
+2 -2
demo.cac.ts
··· 10 10 11 11 cli 12 12 .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`) 15 15 .action((options) => {}); 16 16 17 17 cli.command('dev build', 'Build project').action((options) => {});
+6 -2
src/cac.ts
··· 39 39 40 40 // Add command options 41 41 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 + 42 45 completion.addOption( 43 46 commandName, 44 - `--${option.name}`, 47 + `--${option.name.replace(/^-[a-zA-Z], --/, '')}`, // Remove the short flag part if it exists 45 48 option.description || '', 46 - async () => [] 49 + async () => [], 50 + shortFlag 47 51 ); 48 52 } 49 53 }
+10 -1
src/citty.ts
··· 65 65 if (conf.type === 'positional') { 66 66 continue; 67 67 } 68 + // Extract alias from the config if it exists 69 + const shortFlag = 70 + typeof conf === 'object' && 'alias' in conf 71 + ? Array.isArray(conf.alias) 72 + ? conf.alias[0] 73 + : conf.alias 74 + : undefined; 75 + 68 76 completion.addOption( 69 77 name, 70 78 `--${argName}`, 71 79 conf.description ?? '', 72 80 async (previousArgs, toComplete, endsWithSpace) => { 73 81 return []; 74 - } 82 + }, 83 + shortFlag 75 84 ); 76 85 } 77 86 }
+41 -9
src/index.ts
··· 75 75 type Option = { 76 76 description: string; 77 77 handler: Handler; 78 + alias?: string; 78 79 }; 79 80 80 81 type Command = { ··· 117 118 command: string, 118 119 option: string, 119 120 description: string, 120 - handler: Handler 121 + handler: Handler, 122 + alias?: string 121 123 ) { 122 124 const cmd = this.commands.get(command); 123 125 if (!cmd) { 124 126 throw new Error(`Command ${command} not found.`); 125 127 } 126 - cmd.options.set(option, { description, handler }); 128 + cmd.options.set(option, { description, handler, alias }); 127 129 return option; 128 130 } 129 131 ··· 233 235 toComplete: string, 234 236 endsWithSpace: boolean 235 237 ): boolean { 236 - return lastPrevArg?.startsWith('--') || toComplete.startsWith('--'); 238 + return ( 239 + lastPrevArg?.startsWith('--') || 240 + lastPrevArg?.startsWith('-') || 241 + toComplete.startsWith('--') || 242 + toComplete.startsWith('-') 243 + ); 237 244 } 238 245 239 246 private shouldCompleteCommands( ··· 255 262 let valueToComplete = toComplete; 256 263 257 264 if (toComplete.includes('=')) { 258 - // Handle --flag=value case 265 + // Handle --flag=value or -f=value case 259 266 const parts = toComplete.split('='); 260 267 flagName = parts[0]; 261 268 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 264 271 flagName = lastPrevArg; 265 272 } 266 273 267 274 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 + 269 288 if (option) { 270 289 const suggestions = await option.handler( 271 290 previousArgs, ··· 286 305 } 287 306 288 307 // Handle flag name completion 289 - if (toComplete.startsWith('--')) { 308 + if (toComplete.startsWith('-')) { 309 + const isShortFlag = 310 + toComplete.startsWith('-') && !toComplete.startsWith('--'); 311 + 290 312 for (const [name, option] of command.options) { 291 - if (name.startsWith(toComplete)) { 313 + // For short flags (-), only show aliases 314 + if (isShortFlag) { 315 + if (option.alias && `-${option.alias}`.startsWith(toComplete)) { 316 + this.completions.push({ 317 + value: `-${option.alias}`, 318 + description: option.description, 319 + }); 320 + } 321 + } 322 + // For long flags (--), show the full names 323 + else if (name.startsWith(toComplete)) { 292 324 this.completions.push({ 293 325 value: name, 294 326 description: option.description,
+66
tests/__snapshots__/cli.test.ts.snap
··· 6 6 " 7 7 `; 8 8 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 + 9 19 exports[`cli completion tests for cac > cli option exclusion tests > should not suggest already specified option '{ specified: '--config', shouldNotContain: '--config' }' 1`] = ` 10 20 ":4 11 21 " ··· 70 80 exports[`cli completion tests for cac > positional argument completions > should complete single positional argument when ending with space 1`] = ` 71 81 "main.ts Main file 72 82 index.ts Index file 83 + :4 84 + " 85 + `; 86 + 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 73 106 :4 74 107 " 75 108 `; ··· 87 120 " 88 121 `; 89 122 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 + 90 133 exports[`cli completion tests for citty > cli option exclusion tests > should not suggest already specified option '{ specified: '--config', shouldNotContain: '--config' }' 1`] = ` 91 134 ":4 92 135 " ··· 131 174 exports[`cli completion tests for citty > edge case completions for end with space > should suggest port values if user typed \`--port=\` and hasn't typed a space or value yet 1`] = ` 132 175 "--port=3000 Development server port 133 176 --port=8080 Alternative port 177 + :4 178 + " 179 + `; 180 + 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 134 200 :4 135 201 " 136 202 `;
+31 -1
tests/cli.test.ts
··· 25 25 }); 26 26 27 27 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 + ]; 29 33 30 34 test.each(optionTests)( 31 35 "should complete option for partial input '%s'", ··· 95 99 96 100 it("should suggest port values if user typed `--port=` and hasn't typed a space or value yet", async () => { 97 101 const command = `${commandPrefix} dev --port=`; 102 + const output = await runCommand(command); 103 + expect(output).toMatchSnapshot(); 104 + }); 105 + }); 106 + 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 --`; 98 128 const output = await runCommand(command); 99 129 expect(output).toMatchSnapshot(); 100 130 });