From 7f7fc32b76b393f82d0e96f97e6568cd48ba67bc Mon Sep 17 00:00:00 2001 From: Tim Oelkers Date: Tue, 10 Nov 2026 09:52:21 +0100 Subject: [PATCH] fix: keep the shell directive set by an option or argument handler `complete()` reset `directive` to `ShellCompDirectiveNoFileComp` after the handlers had run, so a handler could not choose file or directory completion. Reset it at the start of `parse()` instead: the default stays `:4`, and a handler can set e.g. `ShellCompDirectiveDefault` or `ShellCompDirectiveFilterDirs` for its completion request. --- .changeset/quiet-files-wait.md | 5 +++ README.md | 13 +++++++ src/t.ts | 3 +- tests/directive.test.ts | 63 ++++++++++++++++++++++++++++++++++ 4 files changed, 82 insertions(+), 2 deletions(-) create mode 100644 .changeset/quiet-files-wait.md create mode 100644 tests/directive.test.ts diff --git a/.changeset/quiet-files-wait.md b/.changeset/quiet-files-wait.md new file mode 100644 index 0000000..b137058 --- /dev/null +++ b/.changeset/quiet-files-wait.md @@ -0,0 +1,5 @@ +--- +'@bomb.sh/tab': patch +--- + +fix: keep the shell directive set by an option or argument handler, so handlers can enable file and directory completion diff --git a/README.md b/README.md index c019b23..3c6da7a 100644 --- a/README.md +++ b/README.md @@ -353,6 +353,19 @@ my-cli complete -- install --port="" :4 ``` +The last line is the shell directive. `:4` (`ShellCompDirectiveNoFileComp`) is the default and stops the shell from falling back to file completion. A handler can set another directive for its completion request, for example to let the shell complete file or directory paths: + +```typescript +import t, { ShellCompDirective } from '@bomb.sh/tab'; + +devCmd.option('config', 'Config file', () => { + t.directive = ShellCompDirective.ShellCompDirectiveDefault; // files +}); +devCmd.argument('dir', () => { + t.directive = ShellCompDirective.ShellCompDirectiveFilterDirs; // directories +}); +``` + ## Documentation See [bombshell docs](https://bomb.sh/docs/tab/). diff --git a/src/t.ts b/src/t.ts index b22cd9c..f211323 100644 --- a/src/t.ts +++ b/src/t.ts @@ -387,8 +387,6 @@ export class RootCommand extends Command { } private complete(toComplete: string) { - this.directive = ShellCompDirective.ShellCompDirectiveNoFileComp; - const seen = new Set(); this.completions .filter((comp) => { @@ -412,6 +410,7 @@ export class RootCommand extends Command { parse(args: string[]) { this.completions = []; + this.directive = ShellCompDirective.ShellCompDirectiveNoFileComp; const endsWithSpace = args[args.length - 1] === ''; diff --git a/tests/directive.test.ts b/tests/directive.test.ts new file mode 100644 index 0000000..f0840d1 --- /dev/null +++ b/tests/directive.test.ts @@ -0,0 +1,63 @@ +import { describe, it, expect, vi, afterEach } from 'vitest'; +import { RootCommand, ShellCompDirective } from '../src/t'; + +function parse(root: RootCommand, args: string[]): string[] { + const log = vi.spyOn(console, 'log').mockImplementation(() => {}); + root.parse(args); + return log.mock.calls.map(([line]) => String(line)); +} + +describe('completion directive', () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('disables file completion by default', () => { + const root = new RootCommand(); + root.command('build', 'Build').option('out', 'Output file', () => {}); + + expect(parse(root, ['build', '--out', ''])).toEqual([ + `:${ShellCompDirective.ShellCompDirectiveNoFileComp}`, + ]); + }); + + it('keeps a directive set by an option handler', () => { + const root = new RootCommand(); + root.command('build', 'Build').option('out', 'Output file', () => { + root.directive = ShellCompDirective.ShellCompDirectiveDefault; + }); + + expect(parse(root, ['build', '--out', ''])).toEqual([ + `:${ShellCompDirective.ShellCompDirectiveDefault}`, + ]); + }); + + it('keeps a directive set by an argument handler', () => { + const root = new RootCommand(); + root.command('serve', 'Serve').argument('dir', () => { + root.directive = ShellCompDirective.ShellCompDirectiveFilterDirs; + }); + + expect(parse(root, ['serve', ''])).toEqual([ + `:${ShellCompDirective.ShellCompDirectiveFilterDirs}`, + ]); + }); + + it('resets the directive for the next completion request', () => { + const root = new RootCommand(); + const build = root.command('build', 'Build'); + build.option('out', 'Output file', () => { + root.directive = ShellCompDirective.ShellCompDirectiveDefault; + }); + build.option('mode', 'Build mode', (complete) => { + complete('production', ''); + }); + + parse(root, ['build', '--out', '']); + + expect(parse(root, ['build', '--mode', ''])).toEqual([ + 'production\t', + `:${ShellCompDirective.ShellCompDirectiveNoFileComp}`, + ]); + }); +});