为什么我的 zsh tab 在命令上完成速度很慢,但在目录上却没有?

jav*_*vex 5 zsh zsh-completion

我正在使用 zsh 并打开完成功能。当我尝试制表符完成时,有时命令会挂起很长时间。几秒钟后,它完成并正确呈现我的选项。另一方面,如果我用 Ctrl-C 中断它,我会收到以下消息:

Killed by signal in _path_commands after 2s
Run Code Online (Sandbox Code Playgroud)

如果我尝试使用制表符完成目录(例如在 中ls),它工作得很好,没有延迟。

请注意,我正在使用 WSL2 在 Windows 上运行,尽管我依稀记得它也发生在其他系统上。还没有回去确认,但是当我刚刚在我的服务器上测试时,我无法在那里重现它,所以这与环境有关。

jav*_*vex 5

提供我自己的问题的答案以分享我的发现。如果其他人有更好的想法,我很乐意接受他们的答案。然而,当谷歌搜索时,我找不到任何关于这个错误的信息(zsh\ 晦涩的语法没有帮助,使它像 perl 表达式一样容易谷歌搜索)。

\n

tl ;dr解决方案如下:运行unsetopt pathdirs,问题就会消失。将其放入您的中~/.zshrc,应该可以解决。接下来是解释。

\n

打开跟踪以_path_commands查看它挂在哪里autoload -t _path_commands::

\n
+_path_commands:46> ret=0\n+_path_commands:51> [[ -o path_dirs ]]\n+_path_commands:52> local -a path_dirs\n
Run Code Online (Sandbox Code Playgroud)\n

因此,让我们通过以下方式查看该函数which _path_commands(请注意,您需要执行一次补全操作,以便 zsh 加载它)。我将提供相关片段:

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

当它挂起时我们得到的最后一行local -a path_dirs只是定义了一个空数组。可能不是这样,但是如果我执行下一个命令,它会挂起很长时间:path_dirs=(${^path}/*(/N:t))。如果您不熟悉该语言,祝您好运。我会解释一下:

\n
    \n
  • 我们正在创建一个数组( ... )
  • \n
  • 我们引用参数$path
  • \n
  • RC_EXPAND_PARAM 我们用^字符来开启${^path},参见14.3参数扩展。这不是我们的罪魁祸首,所以我将跳过解释。唯一需要理解的是我们这里有一个数组。
  • \n
  • 通过 . 在每个目录中进行通配/*。这与您在命令行上执行此操作相同:ls *例如。但这里除外,它对数组的所有元素执行此操作,就像循环一样。一个很好的罪魁祸首,但如果我们尝试的echo ${^path}/*话,速度仍然很快。
  • \n
  • 最后,我们添加三个glob 限定符,本质上是对扩展结果的过滤:\n
      \n
    • /只返回目录
    • \n
    • N sets nullglob, basically "remove empty elements"
    • \n
    • :t sets the modifier to remove the full path and leave only the basename output.
    • \n
    \n
  • \n
\n

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:

\n
for item in $path; do echo "${item}: " ${item}/*(/); done\n
Run Code Online (Sandbox Code Playgroud)\n

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).

\n

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:

\n
\n

PATH_DIRS (-Q)

\n
\n
\n

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

\n
\n

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.

\n