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.
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:
( ... )
$path
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./*
. 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./
only returns directoriesN
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.