This guide explains how to implement custom tab completions for your scripts, providing a seamless command-line experience.
- Overview
- Basic Script Completion
- Implementing --complete
- Completion Format
- Examples by Language
- Advanced Patterns
- Debugging Completions
tome-cli provides two levels of completion:
-
Automatic completions - Completed by tome-cli itself:
- Built-in commands (
exec,help,completion,alias) - Directory names in your scripts folder
- Script names in your scripts folder
- Built-in commands (
-
Script-level completions - Provided by individual scripts:
- Script-specific flags and options
- Dynamic argument values
- Context-aware suggestions
This guide focuses on implementing script-level completions.
To enable your script to provide its own completions:
Add TOME_COMPLETION anywhere in your script (typically in a comment):
#!/usr/bin/env bash
# TOME_COMPLETION
# USAGE: my-script [options]
# Rest of your script...When tome-cli detects a script with TOME_COMPLETION, it will call your script with a --complete flag (actually --completion based on the code). Handle this flag to output completions:
#!/usr/bin/env bash
# TOME_COMPLETION
case "${1:-}" in
--complete)
# Output your completions here
echo -e "--help\tShow help message"
echo -e "--verbose\tEnable verbose mode"
echo -e "start\tStart the service"
echo -e "stop\tStop the service"
exit 0
;;
*)
# Normal script logic
echo "Running script..."
;;
esaccase "${1:-}" in
--complete)
# Output completions
# Format: value<TAB>description
# Use echo -e for tab character (\t)
echo -e "completion1\tDescription of completion1"
echo -e "completion2\tDescription of completion2"
exit 0
;;
esac- Each completion is on its own line
- Format:
value<TAB>description(tab-separated) - The description is optional but recommended
- Exit after outputting completions (don't run normal script logic)
- Use
echo -eto properly interpret the\ttab character
completion-value<TAB>description
Examples:
echo -e "--help\tShow help message"
echo -e "--verbose\tEnable verbose output"
echo -e "production\tProduction environment"Different shells render completions differently:
- Bash: Shows completion values (descriptions may not be visible)
- Zsh: Shows both values and descriptions
- Fish: Shows values with descriptions as hints
- Keep values short and memorable:
start,stop, notstart-the-service - Make descriptions helpful: Explain what the option does
- Order matters: Put most common options first
- Use consistent naming: Follow CLI conventions (e.g.,
--flag-name)
#!/usr/bin/env bash
# TOME_COMPLETION
# USAGE: bash-example <command> [options]
case "${1:-}" in
--complete)
# Static completions
echo -e "start\tStart the service"
echo -e "stop\tStop the service"
echo -e "restart\tRestart the service"
echo -e "status\tCheck service status"
echo -e "--force\tForce the operation"
echo -e "--quiet\tSuppress output"
exit 0
;;
start)
echo "Starting service..."
;;
stop)
echo "Stopping service..."
;;
*)
echo "Usage: $0 <command>" >&2
exit 1
;;
esac#!/usr/bin/env python3
# TOME_COMPLETION
# USAGE: python-example <command> [options]
import sys
if len(sys.argv) > 1 and sys.argv[1] == '--complete':
# Output completions
completions = [
("list", "List all items"),
("add", "Add a new item"),
("remove", "Remove an item"),
("--format", "Output format (json, yaml, text)"),
("--verbose", "Enable verbose output"),
]
for value, description in completions:
print(f"{value}\t{description}")
sys.exit(0)
# Normal script logic
command = sys.argv[1] if len(sys.argv) > 1 else None
if command == "list":
print("Listing items...")
elif command == "add":
print("Adding item...")
else:
print("Unknown command", file=sys.stderr)
sys.exit(1)#!/usr/bin/env -S deno run --allow-all
// TOME_COMPLETION
// USAGE: deno-example <command> [options]
if (Deno.args[0] === '--complete') {
// Output completions
const completions = [
['deploy', 'Deploy the application'],
['rollback', 'Rollback to previous version'],
['status', 'Check deployment status'],
['--environment', 'Target environment'],
['--dry-run', 'Preview without executing'],
];
completions.forEach(([value, description]) => {
console.log(`${value}\t${description}`);
});
Deno.exit(0);
}
// Normal script logic
const command = Deno.args[0];
console.log(`Running command: ${command}`);#!/usr/bin/env ruby
# TOME_COMPLETION
# USAGE: ruby-example <action> [options]
if ARGV[0] == '--complete'
# Output completions
completions = [
['backup', 'Create a backup'],
['restore', 'Restore from backup'],
['list', 'List available backups'],
['--destination', 'Backup destination path'],
['--compress', 'Compress the backup'],
]
completions.each do |value, description|
puts "#{value}\t#{description}"
end
exit 0
end
# Normal script logic
action = ARGV[0]
puts "Performing action: #{action}"Generate completions based on current system state:
#!/usr/bin/env bash
# TOME_COMPLETION
# USAGE: deploy <environment>
case "${1:-}" in
--complete)
# List available environments from a config file
if [ -f "$TOME_ROOT/.config/environments.txt" ]; then
while IFS= read -r env; do
echo -e "$env\tDeploy to $env environment"
done < "$TOME_ROOT/.config/environments.txt"
else
# Fallback to static list
echo -e "development\tDevelopment environment"
echo -e "staging\tStaging environment"
echo -e "production\tProduction environment"
fi
exit 0
;;
*)
environment="${1:-}"
echo "Deploying to $environment..."
;;
esacProvide different completions based on previous arguments:
#!/usr/bin/env bash
# TOME_COMPLETION
# USAGE: db <command> [options]
case "${1:-}" in
--complete)
# If no arguments yet, show commands
echo -e "backup\tBackup the database"
echo -e "restore\tRestore the database"
echo -e "migrate\tRun migrations"
echo -e "seed\tSeed the database"
# Common flags
echo -e "--database\tDatabase name"
echo -e "--dry-run\tPreview without executing"
exit 0
;;
backup)
shift
case "${1:-}" in
--complete)
# Completions specific to backup command
echo -e "--format\tBackup format (sql, dump)"
echo -e "--compress\tCompress the backup"
echo -e "--output\tOutput file path"
exit 0
;;
*)
echo "Backing up database..."
;;
esac
;;
*)
echo "Unknown command" >&2
exit 1
;;
esacSuggest files or directories:
#!/usr/bin/env bash
# TOME_COMPLETION
# USAGE: process-file <file>
case "${1:-}" in
--complete)
# List .json files in current directory
for file in *.json; do
[ -f "$file" ] && echo -e "$file\tProcess $file"
done
# Or list directories
for dir in */; do
[ -d "$dir" ] && echo -e "${dir%/}\tProcess directory: ${dir%/}"
done
exit 0
;;
*)
file="${1:-}"
echo "Processing file: $file"
;;
esacFetch completions from APIs or databases:
#!/usr/bin/env bash
# TOME_COMPLETION
# USAGE: cloud <resource> <action>
case "${1:-}" in
--complete)
# Fetch available resources from API
if command -v curl &> /dev/null; then
curl -s https://api.example.com/resources 2>/dev/null | \
jq -r '.[] | "\(.name)\t\(.description)"' 2>/dev/null
else
# Fallback if curl or jq not available
echo -e "vm\tVirtual machine"
echo -e "storage\tStorage bucket"
echo -e "network\tNetwork configuration"
fi
exit 0
;;
*)
echo "Managing cloud resource..."
;;
esacCache expensive completion computations:
#!/usr/bin/env bash
# TOME_COMPLETION
# USAGE: search <query>
CACHE_FILE="$TOME_ROOT/.cache/search-completions"
CACHE_TTL=3600 # 1 hour
case "${1:-}" in
--complete)
# Check if cache exists and is fresh
if [ -f "$CACHE_FILE" ]; then
cache_age=$(($(date +%s) - $(stat -f %m "$CACHE_FILE" 2>/dev/null || stat -c %Y "$CACHE_FILE")))
if [ $cache_age -lt $CACHE_TTL ]; then
cat "$CACHE_FILE"
exit 0
fi
fi
# Generate completions (expensive operation)
mkdir -p "$(dirname "$CACHE_FILE")"
{
echo -e "recent\tRecent searches"
echo -e "popular\tPopular searches"
# ... more completions
} | tee "$CACHE_FILE"
exit 0
;;
*)
query="${1:-}"
echo "Searching for: $query"
;;
esacCall your script with the --complete flag directly:
./my-script --completeExpected output:
option1 Description of option1
option2 Description of option2
--flag Description of flag
grep -l "TOME_COMPLETION" my-scriptEnsure you're using actual tabs, not spaces:
./my-script --complete | cat -AYou should see ^I between values and descriptions (that's a tab).
# Test in bash
bash -c 'complete -C "tome-cli completion bash" tome-cli && complete -p tome-cli'
# Test in zsh
zsh -c 'source <(tome-cli completion zsh) && compdef tome-cli'
# Test in fish
fish -c 'tome-cli completion fish | source && complete -C"tome-cli "'-
Completions not showing:
- Verify
TOME_COMPLETIONis present in the file - Check that script handles
--completeflag - Ensure script is executable
- Verify
-
Descriptions not visible:
- Some shells (bash) don't show descriptions
- Try in zsh or fish to see descriptions
- Ensure tab character (
\t) is used, not spaces
-
Completions not updating:
- Reload shell completions:
source ~/.bashrc - Some shells cache completions aggressively
- Try in a new shell session
- Reload shell completions:
-
Script execution instead of completions:
- Make sure you exit after outputting completions
- Don't run normal script logic when
--completeis passed
- Keep it fast: Completion generation should be quick (<100ms)
- Cache when possible: Cache expensive operations
- Fail gracefully: If completion generation fails, output nothing rather than errors
- Test thoroughly: Test completions in all supported shells
- Document your completions: Mention completion support in your script's help text
- Use meaningful descriptions: Help users understand what each option does
- Order by frequency: Put most common options first
- Handle errors silently: Redirect errors to
/dev/nullin completion mode
- See writing-scripts.md for general script-writing guidance
- Check out examples/foo for a working completion example
- Read about environment variables available in scripts