Why is bash completion being loaded so slow on OS X?

I don't understand why bash completion is loaded so slow on my MacBook Pro.

I did the following in my ~/.bash_profile:

echo "Loading BashCompletion..."
if [ -f /opt/local/etc/bash_completion ]; then
    . /opt/local/etc/bash_completion
fi
echo "BashCompletion loaded."

the execution time for bash_completion typically is > 2 seconds.

I find that really annoying when I am working on the terminal which requires me to constantly open new tabs.

Is there a way I can cache this or something?

(Note I am using iTerm2 and this is equally slow on the original terminal in Mac as well).


For anyone coming to the conclusion that startup times for new shells on MacOS are too slow for them, this is the solution.

I just discovered that there are in fact two packages that can be installed via brew. I've been installing the bash-completion package for years, and never bothered to question it, even though in that time I've gone from Bash 3, to 4, to now 5. From time to time, though, I'd revisit the problem, often stumbling upon this very StackOverflow discussion.

There's another package, bash-completion@2!

What's the difference? bash-completion is for Bash version 3.2. bash-completion@2 is for Bash version 4.1+, and 5.

In removing the old bash-completion package, and installing bash-completion@2, my shell startup times have dropped from 605ms to 244ms. That's a huge speed improvement.

I suspect many of us are making this same mistake, as the brew info stats show that the former has tonnes of installs, while the latter has so few:

enter image description here

It should be noted that the current chosen answer mentions commenting out some lines, which provides only a slight improvement to startup times (if using the old bash-completion package, which many probably are), but has no impact whatsoever on the new bash-completion@2 package: this new package is fast no matter what. That means no hacks required.

TL;DR:

brew uninstall bash-completion && brew install bash-completion@2

Remember to update the source path to the completion file in your .bashrc or .bash_profile file.

Sources:

  • https://formulae.brew.sh/formula/bash-completion
  • https://formulae.brew.sh/formula/bash-completion@2

As a topic somewhat related, I use the rclone utility a lot, so it's installed. It also happens to have the largest completion file I've ever seen. Removing it reduces my shell startup times to ~120ms, which is very fast.


Edit:

For anyone who wants the technical details that explain this problem, I've written about it at length on the Homebrew forums. To summarize, the reason that bash-completion@2 is so much faster is because it was written so that it no longer eagerly loads all the completion files; instead it loads a completion file on-demand, or as the author describes it, it loads them in a non-eager manner.


Short version: Removing a single line from /usr/local/etc/bash_completion reduced the time to open a new tab from ten seconds to a quarter of a second. Read on for details.

I'm using bash-completion from homebrew and encountered the same problem. It was taking over ten seconds to load the bash completion scripts each time I opened a terminal.

Most of that time, it seems is taken up by a single line in the have() function: a call to type to determine if a command-line program is installed.

With the default have() function and all of the provided bash completion scripts in place, it would take 10.561s to load the scripts (reported by prefixing time to the . /opt/local/etc/bash_completion line in my .bash_profile file.

After commenting out the PATH=$PATH:/sbin:/usr/sbin:/usr/local/sbin type $1 &>/dev/null && line of my /usr/local/etc/bash_completion script (leaving the have=yes line, opening a new terminal takes only 0.258s. This time could be reduced further by removing unnecessary completion scripts (symlinks) from the /usr/local/etc/bash_completion.d directory.

I don't know why the call to type is taking so long. I'm investigating that next.

One potential downside to this approach is that it will cause bash completion functions to be loaded into memory even though you have no use for them. The have() function checks to see if a command or application is installed. If it's not, the completion script generally decides not to bother loading itself because it will be of no use.

At the moment, I'm happy with the tradeoff but I will continue to explore the type problem as I get time. I'll update my answer if I find a better solution.


With the idea that godbyk answer gave me, I found that my PATH variable had a few directories that didn't have any binaries or didn't exists, removing them sped it up significantly. In other words, this is the PATH I had in my bashrc:

PATH="$GOPATH/bin:/some/directory/not/existing:/some/empty/directory:/some/directory/without/binaries:$PATH"

And then I changed it to:

PATH="$GOPATH/bin:$PATH"

This was because have function in that bash completion, looked for each command, and I had too much of useless directories that was going to be visited for each of those binaries, removing them sped it up.