5

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.

javex
  • 7,198
  • 7
  • 41
  • 60
  • 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`? – user1934428 Jul 24 '23 at 06:36

1 Answers1

4

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.

javex
  • 7,198
  • 7
  • 41
  • 60
  • 1
    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. – Kurtis Rader Jul 23 '23 at 03:56
  • 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 ;-) – javex Jul 24 '23 at 11:40