Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -644,6 +644,67 @@ Available theme color slots:
> [!TIP]
> You don't need to specify all color slots. Missing colors fall back to the default theme.

## Shell Completion

**bargs** can generate shell completion scripts for bash, zsh, and fish. Enable it with the `completion` option:

```typescript
bargs('my-cli', {
completion: true,
version: '1.0.0',
});
```

Then generate and install the completion script for your shell:

### Bash

```bash
# Add to ~/.bashrc (or ~/.bash_profile on macOS)
my-cli --completion-script bash >> ~/.bashrc
source ~/.bashrc
```

### Zsh

```bash
# Add to ~/.zshrc
my-cli --completion-script zsh >> ~/.zshrc
source ~/.zshrc

# Or save to a file in your $fpath
my-cli --completion-script zsh > ~/.zsh/completions/_my-cli
```

### Fish

```bash
# Save to completions directory
my-cli --completion-script fish > ~/.config/fish/completions/my-cli.fish
```

### What Gets Completed

Once installed, pressing `Tab` will complete:

- **Commands and subcommands** (including nested commands and aliases)
- **Options** (`--verbose`, `-v`, `--no-verbose` for booleans)
- **Enum values** for options and positionals with defined choices
- **Global options** at any command level

```shell
$ my-cli <TAB>
build test lint

$ my-cli build --target <TAB>
dev staging prod

$ my-cli --<TAB>
--verbose --config --help --version
```

See `examples/completion.ts` for a complete example.

## Advanced Usage

### Error Handling
Expand Down
6 changes: 5 additions & 1 deletion cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"msuccess",
"mwarning",
"mycli",
"mytool",
"barg",
"argumentis",
"mhello",
Expand All @@ -62,7 +63,10 @@
"realfavicongenerator",
"frickin",
"TSES",
"ghostty"
"ghostty",
"CWORD",
"fpath",
"compdef"
],
"words": [
"bupkis",
Expand Down
154 changes: 154 additions & 0 deletions examples/completion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
#!/usr/bin/env npx tsx
/**
* Shell completion example
*
* Demonstrates how to enable shell completion for a CLI.
*
* To enable completions for this example:
*
* # Bash (add to ~/.bashrc)
*
* Npx tsx examples/completion.ts --completion-script bash >> ~/.bashrc source
* ~/.bashrc
*
* # Zsh (add to ~/.zshrc)
*
* Npx tsx examples/completion.ts --completion-script zsh >> ~/.zshrc source
* ~/.zshrc
*
* # Fish (save to completions directory)
*
* Npx tsx examples/completion.ts --completion-script fish >
* ~/.config/fish/completions/completion-demo.fish
*
* Then try pressing TAB after typing partial commands or options.
*
* Usage: npx tsx examples/completion.ts build --target prod npx tsx
* examples/completion.ts test --coverage npx tsx examples/completion.ts lint
* --fix
*/
import { bargs, opt, pos } from '../src/index.js';

// ═══════════════════════════════════════════════════════════════════════════════
// GLOBAL OPTIONS
// ═══════════════════════════════════════════════════════════════════════════════

const globalOptions = opt.options({
config: opt.string({
aliases: ['c'],
description: 'Path to config file',
}),
verbose: opt.boolean({
aliases: ['v'],
default: false,
description: 'Enable verbose output',
}),
});

// ═══════════════════════════════════════════════════════════════════════════════
// BUILD COMMAND
// ═══════════════════════════════════════════════════════════════════════════════

const buildParser = opt.options({
minify: opt.boolean({
aliases: ['m'],
default: false,
description: 'Minify output',
}),
// Enum option - completions will suggest these choices
target: opt.enum(['dev', 'staging', 'prod'], {
aliases: ['t'],
default: 'dev',
description: 'Build target environment',
}),
});

// ═══════════════════════════════════════════════════════════════════════════════
// TEST COMMAND
// ═══════════════════════════════════════════════════════════════════════════════

const testParser = pos.positionals(
// Enum positional - completions will suggest these choices
pos.enum(['unit', 'integration', 'e2e'], {
description: 'Test type to run',
name: 'type',
}),
)(
opt.options({
coverage: opt.boolean({
default: false,
description: 'Collect coverage',
}),
watch: opt.boolean({
aliases: ['w'],
default: false,
description: 'Watch for changes',
}),
}),
);

// ═══════════════════════════════════════════════════════════════════════════════
// LINT COMMAND
// ═══════════════════════════════════════════════════════════════════════════════

const lintParser = opt.options({
fix: opt.boolean({
default: false,
description: 'Auto-fix issues',
}),
// Enum option - completions will suggest these choices
format: opt.enum(['stylish', 'json', 'compact'], {
default: 'stylish',
description: 'Output format',
}),
});

// ═══════════════════════════════════════════════════════════════════════════════
// CLI
// ═══════════════════════════════════════════════════════════════════════════════

await bargs('completion', {
// Enable shell completion support!
completion: true,
description: 'Example CLI with shell completion support',
version: '1.0.0',
})
.globals(globalOptions)
.command(
'build',
buildParser,
({ values }) => {
console.log('Building for:', values.target);
console.log('Minify:', values.minify);
if (values.verbose) {
console.log('Config:', values.config ?? '(default)');
}
},
{ aliases: ['b'], description: 'Build the project' },
)
.command(
'test',
testParser,
({ positionals, values }) => {
console.log('Running tests:', positionals[0] ?? 'all');
console.log('Coverage:', values.coverage);
console.log('Watch:', values.watch);
if (values.verbose) {
console.log('Config:', values.config ?? '(default)');
}
},
{ aliases: ['t'], description: 'Run tests' },
)
.command(
'lint',
lintParser,
({ values }) => {
console.log('Linting with format:', values.format);
console.log('Fix:', values.fix);
if (values.verbose) {
console.log('Config:', values.config ?? '(default)');
}
},
{ aliases: ['l'], description: 'Lint source files' },
)
.parseAsync();
71 changes: 70 additions & 1 deletion src/bargs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ import type {
ParseResult,
} from './types.js';

import {
generateCompletionScript,
getCompletionCandidates,
validateShell,
} from './completion.js';
import { BargsError, HelpError } from './errors.js';
import { generateCommandHelp, generateHelp } from './help.js';
import { parseSimple } from './parser.js';
Expand Down Expand Up @@ -527,6 +532,7 @@ const isCommand = (x: unknown): x is Command<unknown, readonly unknown[]> => {

// Internal type for CliBuilder with internal methods
type InternalCliBuilder<V, P extends readonly unknown[]> = CliBuilder<V, P> & {
__getState: () => InternalCliState;
__parseWithParentGlobals: (
args: string[],
parentGlobals: ParseResult<unknown, readonly unknown[]>,
Expand All @@ -545,6 +551,11 @@ const createCliBuilder = <V, P extends readonly unknown[]>(
state: InternalCliState,
): CliBuilder<V, P> => {
const builder: InternalCliBuilder<V, P> = {
// Internal method for completion support - not part of public API
__getState(): InternalCliState {
return state;
},

// Internal method for nested command support - not part of public API
__parseWithParentGlobals(
args: string[],
Expand Down Expand Up @@ -852,6 +863,52 @@ const parseCore = (
process.exit(0);
}
}

// Handle shell completion (when enabled)
if (options.completion) {
// Handle --completion-script <shell>
const completionScriptIndex = args.indexOf('--completion-script');
if (completionScriptIndex >= 0) {
const shellArg = args[completionScriptIndex + 1];
if (!shellArg) {
console.error(
'Error: --completion-script requires a shell argument (bash, zsh, or fish)',
);
process.exit(1);
}
try {
const shell = validateShell(shellArg);
console.log(generateCompletionScript(state.name, shell));
process.exit(0);
} catch (err) {
console.error(`Error: ${(err as Error).message}`);
process.exit(1);
}
}

// Handle --get-bargs-completions <shell> <...words>
const getCompletionsIndex = args.indexOf('--get-bargs-completions');
if (getCompletionsIndex >= 0) {
const shellArg = args[getCompletionsIndex + 1];
if (!shellArg) {
// No shell specified, output nothing
process.exit(0);
}
try {
const shell = validateShell(shellArg);
// Words are everything after the shell argument
const words = args.slice(getCompletionsIndex + 2);
const candidates = getCompletionCandidates(state, shell, words);
if (candidates.length > 0) {
console.log(candidates.join('\n'));
}
process.exit(0);
} catch {
// Invalid shell, output nothing
process.exit(0);
}
}
}
/* c8 ignore stop */

// If we have commands, dispatch to the appropriate one
Expand Down Expand Up @@ -947,6 +1004,18 @@ const generateCommandHelpNew = (
*/
/* c8 ignore start -- only called from help paths that call process.exit() */
const generateHelpNew = (state: InternalCliState, theme: Theme): string => {
// Build options schema, adding --completion-script if completion is enabled
let options = state.globalParser?.__optionsSchema;
if (state.options.completion) {
options = {
...options,
'completion-script': {
description: 'Output shell completion script (bash, zsh, fish)',
type: 'string' as const,
},
};
}

// Delegate to existing help generator with config including aliases
const config = {
commands: Object.fromEntries(
Expand All @@ -959,7 +1028,7 @@ const generateHelpNew = (state: InternalCliState, theme: Theme): string => {
),
description: state.options.description,
name: state.name,
options: state.globalParser?.__optionsSchema,
options,
version: state.options.version,
};
return generateHelp(config as Parameters<typeof generateHelp>[0], theme);
Expand Down
Loading