[READ-ONLY] Mirror of https://github.com/bombshell-dev/tab. shell autocompletions for javascript CLIs
Something went wrong. Try again.
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508// Shell directive constantsexport const ShellCompDirective = { ShellCompDirectiveError: 1 << 0, ShellCompDirectiveNoSpace: 1 << 1, ShellCompDirectiveNoFileComp: 1 << 2, ShellCompDirectiveFilterFileExt: 1 << 3, ShellCompDirectiveFilterDirs: 1 << 4, ShellCompDirectiveKeepOrder: 1 << 5, shellCompDirectiveMaxValue: 1 << 6, ShellCompDirectiveDefault: 0,};
export type OptionsMap = Map<string, Option>;
export type Complete = (value: string, description: string) => void;
export type OptionHandler = ( this: Option, complete: Complete, options: OptionsMap) => void;
export interface Completion { description?: string; value: string;}
export type ArgumentHandler = ( this: Argument, complete: Complete, options: OptionsMap) => void;
export class Argument { name: string; variadic: boolean; command: Command; handler?: ArgumentHandler;
constructor( command: Command, name: string, handler?: ArgumentHandler, variadic: boolean = false ) { this.command = command; this.name = name; this.handler = handler; this.variadic = variadic; }}
export class Option { value: string; description: string; command: Command; handler?: OptionHandler; alias?: string; isBoolean?: boolean;
constructor( command: Command, value: string, description: string, handler?: OptionHandler, alias?: string, isBoolean?: boolean ) { this.command = command; this.value = value; this.description = description; this.handler = handler; this.alias = alias; this.isBoolean = isBoolean; }}
export class Command { value: string; description: string; options = new Map<string, Option>(); arguments = new Map<string, Argument>(); parent?: Command;
constructor(value: string, description: string) { this.value = value; this.description = description; }
option( value: string, description: string, handlerOrAlias?: OptionHandler | string, alias?: string ): Command { let handler: OptionHandler | undefined; let aliasStr: string | undefined; let isBoolean: boolean;
if (typeof handlerOrAlias === 'function') { handler = handlerOrAlias; aliasStr = alias; isBoolean = false; } else if (typeof handlerOrAlias === 'string') { handler = undefined; aliasStr = handlerOrAlias; isBoolean = true; } else { handler = undefined; aliasStr = undefined; isBoolean = true; }
const option = new Option( this, value, description, handler, aliasStr, isBoolean ); this.options.set(value, option); return this; }
argument(name: string, handler?: ArgumentHandler, variadic: boolean = false) { const arg = new Argument(this, name, handler, variadic); this.arguments.set(name, arg); return this; }}
import * as zsh from './zsh';import * as bash from './bash';import * as fish from './fish';import * as powershell from './powershell';import assert from 'node:assert';
export class RootCommand extends Command { commands = new Map<string, Command>(); completions: Completion[] = []; directive = ShellCompDirective.ShellCompDirectiveDefault;
constructor() { super('', ''); }
command(value: string, description: string) { const c = new Command(value, description); this.commands.set(value, c); return c; }
private stripOptions(args: string[]): string[] { const parts: string[] = []; let i = 0;
while (i < args.length) { const arg = args[i];
if (arg.startsWith('-')) { i++;
let isBoolean = false;
const rootOption = this.findOption(this, arg); if (rootOption) { isBoolean = rootOption.isBoolean ?? false; } else { // subcommand options for (const [, command] of this.commands) { const option = this.findOption(command, arg); if (option) { isBoolean = option.isBoolean ?? false; break; } } }
// skip the next argument if this is not a boolean option and the next arg doesn't start with - if (!isBoolean && i < args.length && !args[i].startsWith('-')) { i++; } } else { parts.push(arg); i++; } }
return parts; }
private matchCommand(args: string[]): [Command, string[]] { args = this.stripOptions(args); const parts: string[] = []; let remaining: string[] = []; let matchedCommand: Command | null = null;
for (let i = 0; i < args.length; i++) { const k = args[i]; parts.push(k); const potential = this.commands.get(parts.join(' '));
if (potential) { matchedCommand = potential; } else { remaining = args.slice(i, args.length); break; } }
return [matchedCommand || this, remaining]; }
private shouldCompleteFlags( lastPrevArg: string | undefined, toComplete: string ): boolean { if (toComplete.startsWith('-')) { return true; }
// previous argument was an option, check if it expects a value if (lastPrevArg?.startsWith('-')) { let option = this.findOption(this, lastPrevArg); if (!option) { // subcommand options for (const [, command] of this.commands) { option = this.findOption(command, lastPrevArg); if (option) break; } }
// boolean option, don't try to complete its value if (option && option.isBoolean) { return false; }
// non-boolean options expect values return true; }
return false; }
private shouldCompleteCommands(toComplete: string): boolean { return !toComplete.startsWith('-'); }
// flag completion (names and values) private handleFlagCompletion( command: Command, previousArgs: string[], toComplete: string, lastPrevArg: string | undefined ) { // Handle flag value completion let optionName: string | undefined;
if (toComplete.includes('=')) { const [flag] = toComplete.split('='); optionName = flag; } else if (lastPrevArg?.startsWith('-')) { const option = this.findOption(command, lastPrevArg); if (option && !option.isBoolean) { optionName = lastPrevArg; } }
if (optionName) { const option = this.findOption(command, optionName); if (option?.handler) { const suggestions: Completion[] = []; option.handler.call( option, (value: string, description: string) => suggestions.push({ value, description }), command.options );
this.completions = suggestions; } return; }
if (toComplete.startsWith('-')) { const isShortFlag = toComplete.startsWith('-') && !toComplete.startsWith('--'); const cleanToComplete = toComplete.replace(/^-+/, '');
for (const [name, option] of command.options) { if ( isShortFlag && option.alias && `-${option.alias}`.startsWith(toComplete) ) { this.completions.push({ value: `-${option.alias}`, description: option.description, }); } else if (!isShortFlag && name.startsWith(cleanToComplete)) { this.completions.push({ value: `--${name}`, description: option.description, }); } } } }
// find option by name or alias private findOption(command: Command, optionName: string): Option | undefined { let option = command.options.get(optionName); if (option) return option;
option = command.options.get(optionName.replace(/^-+/, '')); if (option) return option;
// short alias for (const [_name, opt] of command.options) { if (opt.alias && `-${opt.alias}` === optionName) { return opt; } }
return undefined; }
// command completion private handleCommandCompletion(previousArgs: string[], toComplete: string) { const commandParts = this.stripOptions(previousArgs);
for (const [k, command] of this.commands) { if (k === '') continue;
const parts = k.split(' '); const match = parts .slice(0, commandParts.length) .every((part, i) => part === commandParts[i]);
if (match && parts[commandParts.length]?.startsWith(toComplete)) { this.completions.push({ value: parts[commandParts.length], description: command.description, }); } } }
// positional argument completion private handlePositionalCompletion(command: Command, previousArgs: string[]) { // current argument position (subtract command name) const commandParts = command.value.split(' ').length; const currentArgIndex = Math.max(0, previousArgs.length - commandParts); const argumentEntries = Array.from(command.arguments.entries());
if (argumentEntries.length > 0) { let targetArgument: Argument | undefined;
if (currentArgIndex < argumentEntries.length) { const [_argName, argument] = argumentEntries[currentArgIndex]; targetArgument = argument; } else { const lastArgument = argumentEntries[argumentEntries.length - 1][1]; if (lastArgument.variadic) { targetArgument = lastArgument; } }
if ( targetArgument && targetArgument.handler && typeof targetArgument.handler === 'function' ) { const suggestions: Completion[] = []; targetArgument.handler.call( targetArgument, (value: string, description: string) => suggestions.push({ value, description }), command.options ); this.completions.push(...suggestions); } } }
private complete(toComplete: string) { this.directive = ShellCompDirective.ShellCompDirectiveNoFileComp;
const seen = new Set<string>(); this.completions .filter((comp) => { if (seen.has(comp.value)) return false; seen.add(comp.value); return true; }) .filter((comp) => { if (toComplete.includes('=')) { // for --option=value format, extract the value part after = const [, valueToComplete] = toComplete.split('='); return comp.value.startsWith(valueToComplete); } return comp.value.startsWith(toComplete); }) .forEach((comp) => console.log(`${comp.value}\t${comp.description ?? ''}`) ); console.log(`:${this.directive}`); }
parse(args: string[]) { this.completions = [];
const endsWithSpace = args[args.length - 1] === '';
if (endsWithSpace) { args.pop(); }
let toComplete = args[args.length - 1] || ''; const previousArgs = args.slice(0, -1);
if (endsWithSpace) { if (toComplete !== '') { previousArgs.push(toComplete); } toComplete = ''; }
const [matchedCommand] = this.matchCommand(previousArgs); const lastPrevArg = previousArgs[previousArgs.length - 1];
if (this.shouldCompleteFlags(lastPrevArg, toComplete)) { this.handleFlagCompletion( matchedCommand, previousArgs, toComplete, lastPrevArg ); } else { if (lastPrevArg?.startsWith('-') && toComplete === '' && endsWithSpace) { let option = this.findOption(this, lastPrevArg); if (!option) { for (const [, command] of this.commands) { option = this.findOption(command, lastPrevArg); if (option) break; } }
if (option && option.isBoolean) { this.complete(toComplete); return; } }
if (this.shouldCompleteCommands(toComplete)) { this.handleCommandCompletion(previousArgs, toComplete); } if (matchedCommand && matchedCommand.arguments.size > 0) { this.handlePositionalCompletion(matchedCommand, previousArgs); } }
this.complete(toComplete); }
setup(name: string, executable: string, shell: string) { assert( shell === 'zsh' || shell === 'bash' || shell === 'fish' || shell === 'powershell', 'Unsupported shell' );
switch (shell) { case 'zsh': { const script = zsh.generate(name, executable); console.log(script); break; } case 'bash': { const script = bash.generate(name, executable); console.log(script); break; } case 'fish': { const script = fish.generate(name, executable); console.log(script); break; } case 'powershell': { const script = powershell.generate(name, executable); console.log(script); break; } } }}
const t = new RootCommand();
export function script(shell: string, name: string, executable: string) { t.setup(name, executable, shell);}
export default t;