Why is my zsh tab completion slow on commands but not directories?
Asked Answered
G

2

7

I am using zsh with completion turned on. When I try to tab-complete, sometimes the command hangs for a long time. After a few seconds it completes and correctly presents my options. On the other hand, if I interrupt it with Ctrl-C, I get the following message:

Killed by signal in _path_commands after 2s

If I try to tab-complete directories (e.g. in ls) it works just fine, there is no lag.

Note that I am running on Windows using WSL2, though I can vaguely recall it happening on other systems, too. Haven't gone back to confirm, but when I just tested on my server, I couldn't reproduce it there, so it's something about the environment.

Gibb answered 23/7, 2023 at 1:51 Comment(1)
Could it be that one of the elements in your path points to a Network share? In particular on Windows. this could explain the delay. Another point worth to check: Do you also see the delay immediately after doing a rehash?Ichthyosaur
G
7

Providing an answer for my own question to share what I found. If others have a better idea, I would love to accept their answer. However, when googling I could not find anything on this error (not helped by zsh's obscure syntax making it about as easy to google as perl expressions).

The tl;dr solution is as follows: Run unsetopt pathdirs and the issue should go away. Put it in your ~/.zshrc and it should be resolved. What follows is the explanation.

Turning on tracing for _path_commands to see where it hangs: autoload -t _path_commands:

+_path_commands:46> ret=0
+_path_commands:51> [[ -o path_dirs ]]
+_path_commands:52> local -a path_dirs

So let's have a look at that function via which _path_commands (note you need to do a completion once for zsh to load it). I'll provide the relevant snippet:

        if [[ -o path_dirs ]]
        then
                local -a path_dirs
                path_dirs=(${^path}/*(/N:t))
                (( ${#path_dirs} )) && _wanted path-dirs expl 'directory in path' compadd "$@" -a path_dirs && ret=0
                if [[ $PREFIX$SUFFIX = */* ]]
                then
                        _wanted commands expl 'external command' _path_files -W path -g '*(*)' && ret=0
                fi
        fi

The last line we get when it hangs is local -a path_dirs which just defines an empty array. That's likely not it, but if I execute the next command it hangs for a long time: path_dirs=(${^path}/*(/N:t)). Good luck googling that if you're not familiar with the language. I'll explain:

  • We are creating an array with ( ... )
  • We reference the parameter $path
  • We turn on RC_EXPAND_PARAM with the ^ chracter ${^path}, see 14.3 Parameter Expansion. It's not our culprit so I'll skip the explanation. The only bit to understand is that we have an array here.
  • Do globbing inside each directory via /*. This is the same as if you did this on your command line: ls *, for example. Except here it does it for all elements of the array, like a loop. A good culprit, but if we try echo ${^path}/* it's still very quick.
  • Lastly we add three glob qualifiers, essentially filters on the results of that expansion:
    • / only returns directories
    • N sets nullglob, basically "remove empty elements"
    • :t sets the modifier to remove the full path and leave only the basename output.

If we play around with the full expression e.g. ${^path}/*(/N:t) we notice that it's only slow if the / character is present. Removing it makes everything fast. With some additional debugging you can even find what's slow, e.g. write a loop and see when it hangs:

for item in $path; do echo "${item}: " ${item}/*(/); done

In my case I notice it hanging on a lot of Windows paths (/mnt/c/Windows/system32, for example). At this point I gave up: I don't know why this expansion is so slow for Windows paths and I don't know how to debug it or do some form of "caching" that speeds it up (it might just be slow due to WSL filesystem issues).

Instead, notice how there is a condition: if [[ -o path_dirs ]] before entering this code path? The conditional test -o checks for an option, i.e. if path_dirs is set. This is described in the options manual:

PATH_DIRS (-Q)

Perform a path search even on command names with slashes in them. Thus if ‘/usr/local/bin’ is in the user’s path, and he or she types ‘X11/xinit’, the command ‘/usr/local/bin/X11/xinit’ will be executed (assuming it exists). Commands explicitly beginning with ‘/’, ‘./’ or ‘../’ are not subject to the path search. This also applies to the ‘.’ and source builtins.

If we can live without this feature (I think I can), we can stop here: Simply turn it off, e.g. via unsetopt pathdirs and call it a day. Once that's done, this code branch is no longer executed and the problem goes away.

Gibb answered 23/7, 2023 at 1:51 Comment(2)
Excellent analysis. FWIW, This type of problem (especially that god awful glob expansion) is why I stopped using Zsh over a decade ago. All POSIX shells suck but Zsh sucks hard. I've written a couple of blog articles highlighting similar problems I have encountered. Do yourself a favor and switch to a sane, modern alternative, shell like Fish or Elvish.Intuitivism
Just read your blog posts, you make good points. Maybe I'll consider another shell in the future. For now though I'll hopefully be spared too many such adventures ;-)Gibb
G
0

I also use WSL2 and ran to this exact problem. Thanks to @javex's answers, I also narrowed it down to the Windows paths, which can be expanded to a lot of different paths.

I solved it by disabling Windows path sharing in WSL since I don't need any of them. You can do it by adding these 2 lines to /etc/wsl.conf:

[interop]
appendWindowsPath = false

There are a lot of good answers on Stackoverflow about disabling it.

My autocompletion becomes snappy again after. If you do need some Windows paths in wsl, you can add the exact paths in your .zshrc and hopefully it won't slow the autocompletion down.

Geneviegenevieve answered 15/8 at 23:23 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.