How Tab Completion Actually Works in Bash and Zsh
Pressing Tab and getting a sensible list of filenames, commands, or flags looks simple from the outside. Underneath, it's a programmable system matching the word you're typing against rules specific to the command you're running.
Tab completion feels like a simple, built-in convenience — press Tab, get a sensible suggestion — but underneath, both Bash and Zsh implement it as a genuinely programmable system, where different commands can define entirely different completion rules.
The default behavior: filename completion
Without any specific configuration, both shells fall back to completing filenames and directory names in the current context — typing cd doc and pressing Tab checks the current directory for entries starting with “doc” and completes as far as it can unambiguously.
Where command-specific completion actually comes from
complete -F _git git
Bash’s complete builtin registers a specific completion function for a specific command — _git, in this example, is a function that knows about git’s subcommands, flags, and branch names, and gets invoked automatically whenever you’re completing arguments to git specifically, rather than falling back to generic filename completion.
Where these completion functions actually come from
The bash-completion project provides pre-written completion functions for hundreds of common commands, installed as a package on most Linux distributions and installable via Homebrew on macOS — this is why git checkout <Tab> suggests actual branch names on a properly configured system, rather than just listing files in the current directory.
Zsh’s more structurally rich completion system
Zsh’s completion system (compsys, initialized via compinit) is built around a more structured description format than Bash’s function-based approach — completion definitions can describe argument types, mutual exclusivity between flags, and multi-level subcommand structures more richly, which is part of why Zsh’s completion is often perceived as more capable out of the box.
Why completion sometimes seems to “not know about” a command
complete -p git
# shows the currently registered completion function for git, if any
If Tab completion for a specific command falls back to generic filenames instead of command-aware suggestions, no completion function is currently registered for it — either the relevant completion package isn’t installed, or it hasn’t been sourced in the current shell session yet.
Why completion can behave differently across a script and an interactive shell
Completion configuration is loaded as part of interactive shell startup (from .bashrc or Zsh’s completion initialization), not from the files a non-interactive script reads — this is exactly why completion “works in my terminal” but is irrelevant inside a script, since scripts never trigger interactive Tab-completion behavior at all.
Why understanding this as programmable, not fixed, behavior matters
Once you know a specific completion function is what makes git checkout <Tab> smart about branch names, it stops looking like unexplainable shell magic — you can reasonably ask “does this command have a completion function installed,” troubleshoot why one might be missing, and even understand that writing a custom completion function for your own scripts and tools is genuinely possible, not something reserved for shell internals alone.