Best Practices
This guide covers best practices for implementing effective autocompletions with Tab, including performance optimization, user experience considerations, and common patterns.
General Best Practices
Section titled “General Best Practices”1. Provide Meaningful Descriptions
Section titled “1. Provide Meaningful Descriptions”Always include descriptive text for your completions to help users understand what each option does:
// GooddevCmd.option('--port', 'Port number', function(complete) { complete('3000', 'Development port (default)'); complete('8080', 'Production port'); complete('9000', 'Alternative port');}, 'p');
// AvoiddevCmd.option('--port', 'Port number', function(complete) { complete('3000', ''); complete('8080', '');}, 'p');
2. Use Context-Aware Completions
Section titled “2. Use Context-Aware Completions”Make your completions responsive to what the user is typing:
devCmd.option('--mode', 'Build mode', function(complete) { // If user is typing 'dev', suggest development if (this.toComplete?.startsWith('dev')) { complete('development', 'Development mode'); return; }
// If user is typing 'prod', suggest production if (this.toComplete?.startsWith('prod')) { complete('production', 'Production mode'); return; }
// Default suggestions complete('development', 'Development mode'); complete('production', 'Production mode'); complete('staging', 'Staging mode');});
3. Handle Errors Gracefully
Section titled “3. Handle Errors Gracefully”Always handle errors in your completion handlers to prevent crashes:
devCmd.option('--config', 'Config file', async function(complete) { try { const files = await fs.readdir('.'); const configFiles = files.filter(f => f.includes('config')); configFiles.forEach(file => complete(file, `Config file: ${file}`)); } catch (error) { // Provide fallback completions instead of failing complete('vite.config.ts', 'Vite config file'); complete('vite.config.js', 'Vite config file'); }});
4. Optimize Performance
Section titled “4. Optimize Performance”Cache expensive operations and limit results to maintain responsiveness:
let cachedScripts: string[] | null = null;
t.command('run', 'Run scripts') .argument('script', async function(complete) { if (cachedScripts) { cachedScripts.forEach(script => complete(script, `Run ${script} script`)); return; }
try { const packageJson = JSON.parse(await fs.readFile('package.json', 'utf8')); const scripts = Object.keys(packageJson.scripts || {}); cachedScripts = scripts.slice(0, 20); // Limit to 20 scripts cachedScripts.forEach(script => complete(script, `Run ${script} script`)); } catch (error) { // Fallback completions complete('dev', 'Start development server'); complete('build', 'Build for production'); } });
Command Structure Best Practices
Section titled “Command Structure Best Practices”1. Use Consistent Naming
Section titled “1. Use Consistent Naming”Follow consistent naming conventions for commands and options:
// Good - consistent with common CLI patternst.command('dev', 'Start development server');t.command('build', 'Build for production');t.command('deploy', 'Deploy application');
// Avoid - inconsistent namingt.command('start-dev', 'Start development server');t.command('build-prod', 'Build for production');t.command('deploy-app', 'Deploy application');
2. Group Related Commands
Section titled “2. Group Related Commands”Organize related commands logically:
// Development commandst.command('dev', 'Start development server');t.command('dev build', 'Build in development mode');t.command('dev test', 'Run tests in development mode');
// Production commandst.command('build', 'Build for production');t.command('deploy', 'Deploy to production');t.command('deploy staging', 'Deploy to staging');
3. Use Short Aliases Sparingly
Section titled “3. Use Short Aliases Sparingly”Provide short aliases only for commonly used options:
// Good - short aliases for common optionsdevCmd.option('--port', 'Port number', handler, 'p');devCmd.option('--host', 'Host address', handler, 'h');devCmd.option('--verbose', 'Enable verbose logging', 'v');
// Avoid - too many short aliasesdevCmd.option('--config', 'Config file', handler, 'c');devCmd.option('--mode', 'Build mode', handler, 'm');devCmd.option('--output', 'Output directory', handler, 'o');devCmd.option('--source', 'Source directory', handler, 's');
Option Design Best Practices
Section titled “Option Design Best Practices”1. Use Boolean Flags Appropriately
Section titled “1. Use Boolean Flags Appropriately”Use boolean flags for simple on/off options:
// Good - boolean flags for simple optionsdevCmd.option('--verbose', 'Enable verbose logging', 'v');devCmd.option('--quiet', 'Suppress output', 'q');devCmd.option('--watch', 'Watch for changes', 'w');
// Good - value options for complex datadevCmd.option('--port', 'Port number', function(complete) { complete('3000', 'Development port'); complete('8080', 'Production port');}, 'p');
2. Provide Sensible Defaults
Section titled “2. Provide Sensible Defaults”When possible, provide sensible default values in your suggestions:
devCmd.option('--port', 'Port number', function(complete) { complete('3000', 'Development port (default)'); complete('8080', 'Production port'); complete('9000', 'Alternative port');}, 'p');
3. Use Descriptive Option Names
Section titled “3. Use Descriptive Option Names”Choose option names that clearly indicate their purpose:
// Good - descriptive namesdevCmd.option('--output-dir', 'Output directory', handler);devCmd.option('--source-map', 'Generate source maps', handler);
// Avoid - ambiguous namesdevCmd.option('--output', 'Output directory', handler);devCmd.option('--map', 'Generate source maps', handler);
Argument Design Best Practices
Section titled “Argument Design Best Practices”1. Use Variadic Arguments for File Lists
Section titled “1. Use Variadic Arguments for File Lists”Use variadic arguments when users might want to specify multiple files:
// Good - variadic argument for multiple filest.command('lint', 'Lint files') .argument('files', function(complete) { complete('src/', 'Source directory'); complete('tests/', 'Tests directory'); complete('*.ts', 'TypeScript files'); }, true); // true = variadic argument
2. Provide Contextual Suggestions
Section titled “2. Provide Contextual Suggestions”Make argument suggestions contextual to the command:
t.command('copy', 'Copy files') .argument('source', function(complete) { // Suggest source locations complete('src/', 'Source directory'); complete('dist/', 'Distribution directory'); complete('public/', 'Public assets'); }) .argument('destination', function(complete) { // Suggest destination locations complete('build/', 'Build output'); complete('release/', 'Release directory'); complete('backup/', 'Backup location'); });
Package Manager Integration Best Practices
Section titled “Package Manager Integration Best Practices”1. Test CLI Compatibility
Section titled “1. Test CLI Compatibility”When building CLI tools, test that they work with Tab’s package manager integration:
# Test if your CLI supports Tab completionsmy-cli complete -- --help
# Expected output format:--help Show help information:0
2. Follow the Tab Protocol
Section titled “2. Follow the Tab Protocol”Ensure your CLI follows the Tab completion protocol:
// In your CLI entry pointif (process.argv[2] === 'complete') { const shell = process.argv[3]; if (shell && ['zsh', 'bash', 'fish', 'powershell'].includes(shell)) { t.setup('my-cli', process.execPath, shell); } else { const separatorIndex = process.argv.indexOf('--'); const completionArgs = separatorIndex !== -1 ? process.argv.slice(separatorIndex + 1) : []; t.parse(completionArgs); }}
3. Provide Comprehensive Completions
Section titled “3. Provide Comprehensive Completions”For package manager integration, provide completions for all major commands:
// Example: Comprehensive pnpm completionsconst addCmd = t.command('add', 'Install packages');addCmd.option('--save-dev', 'Save to devDependencies', 'D');addCmd.option('--save-optional', 'Save to optionalDependencies', 'O');addCmd.option('--global', 'Install globally', 'g');
const runCmd = t.command('run', 'Run scripts');runCmd.argument('script', async function(complete) { try { const packageJson = JSON.parse(await fs.readFile('package.json', 'utf8')); const scripts = Object.keys(packageJson.scripts || {}); scripts.forEach(script => complete(script, `Run ${script} script`)); } catch (error) { // Fallback completions complete('dev', 'Start development server'); complete('build', 'Build for production'); }}, true);
Performance Best Practices
Section titled “Performance Best Practices”1. Cache Expensive Operations
Section titled “1. Cache Expensive Operations”Cache results of expensive operations to improve responsiveness:
let cachedDependencies: string[] | null = null;
t.command('remove', 'Remove packages') .argument('package', async function(complete) { if (cachedDependencies) { cachedDependencies.forEach(dep => complete(dep, 'Installed package')); return; }
try { const packageJson = JSON.parse(await fs.readFile('package.json', 'utf8')); const deps = { ...packageJson.dependencies, ...packageJson.devDependencies, }; cachedDependencies = Object.keys(deps); cachedDependencies.forEach(dep => complete(dep, 'Installed package')); } catch (error) { // Fallback completions complete('react', 'React library'); complete('typescript', 'TypeScript compiler'); } });
2. Limit Result Sets
Section titled “2. Limit Result Sets”Limit the number of completions to maintain performance:
devCmd.option('--config', 'Config file', async function(complete) { try { const files = await fs.readdir('.'); const configFiles = files.filter(f => f.includes('config')); // Limit to 10 results for performance configFiles.slice(0, 10).forEach(file => complete(file, `Config file: ${file}`)); } catch (error) { complete('vite.config.ts', 'Vite config file'); }});
3. Use Async Operations Sparingly
Section titled “3. Use Async Operations Sparingly”Only use async operations when necessary:
// Good - sync operations for simple completionsdevCmd.option('--mode', 'Build mode', function(complete) { complete('development', 'Development mode'); complete('production', 'Production mode'); complete('staging', 'Staging mode');});
// Good - async operations for dynamic datadevCmd.option('--config', 'Config file', async function(complete) { const files = await fs.readdir('.'); const configFiles = files.filter(f => f.includes('config')); configFiles.forEach(file => complete(file, `Config file: ${file}`));});
User Experience Best Practices
Section titled “User Experience Best Practices”1. Provide Progressive Disclosure
Section titled “1. Provide Progressive Disclosure”Start with common options and provide more specific ones as users type:
devCmd.option('--mode', 'Build mode', function(complete) { if (this.toComplete?.startsWith('dev')) { complete('development', 'Development mode'); return; }
if (this.toComplete?.startsWith('prod')) { complete('production', 'Production mode'); return; }
// Show all options initially complete('development', 'Development mode'); complete('production', 'Production mode'); complete('staging', 'Staging mode'); complete('test', 'Test mode');});
2. Use Consistent Descriptions
Section titled “2. Use Consistent Descriptions”Maintain consistent description formatting:
// Good - consistent formattingdevCmd.option('--port', 'Port number', function(complete) { complete('3000', 'Development port (default)'); complete('8080', 'Production port'); complete('9000', 'Alternative port');}, 'p');
// Avoid - inconsistent formattingdevCmd.option('--port', 'Port number', function(complete) { complete('3000', 'dev port'); complete('8080', 'Production port'); complete('9000', 'alt port');}, 'p');
3. Provide Helpful Defaults
Section titled “3. Provide Helpful Defaults”Include default values in descriptions when helpful:
devCmd.option('--port', 'Port number', function(complete) { complete('3000', 'Development port (default)'); complete('8080', 'Production port');}, 'p');
devCmd.option('--host', 'Host address', function(complete) { complete('localhost', 'Localhost (default)'); complete('0.0.0.0', 'All interfaces');}, 'h');
Testing Best Practices
Section titled “Testing Best Practices”1. Test Completions Manually
Section titled “1. Test Completions Manually”Regularly test your completions to ensure they work correctly:
# Test command completionsmy-cli complete -- "dev"
# Test option completionsmy-cli complete -- "dev --port"
# Test argument completionsmy-cli complete -- "copy src/"
# Test with package managerspnpm my-cli complete -- "dev --port"
2. Test Error Scenarios
Section titled “2. Test Error Scenarios”Test how your completions handle error conditions:
// Test with missing filesdevCmd.option('--config', 'Config file', async function(complete) { try { const files = await fs.readdir('.'); const configFiles = files.filter(f => f.includes('config')); configFiles.forEach(file => complete(file, `Config file: ${file}`)); } catch (error) { // Ensure fallback completions work complete('vite.config.ts', 'Vite config file'); complete('vite.config.js', 'Vite config file'); }});
3. Test Performance
Section titled “3. Test Performance”Monitor completion performance, especially for async operations:
// Add timing for performance monitoringdevCmd.option('--config', 'Config file', async function(complete) { const start = Date.now(); try { const files = await fs.readdir('.'); const configFiles = files.filter(f => f.includes('config')); configFiles.forEach(file => complete(file, `Config file: ${file}`)); } catch (error) { complete('vite.config.ts', 'Vite config file'); } const duration = Date.now() - start; if (duration > 100) { console.warn(`Slow completion: ${duration}ms`); }});
Next Steps
Section titled “Next Steps”- Check out Examples for practical use cases
- Explore the API Reference for advanced usage
- Learn about Package Manager Integration for enhanced completions