在 zsh 中,Home、End 和 Del 在脚本内部的行为与在外部的行为不同

在 zsh 中,Home、End 和 Del 在脚本内部的行为与在外部的行为不同

经过多次挫折后,我了解到$EDITOR最初设置为vim不会影响主 shell,但会影响脚本,并且似乎没有什么可以撤消此初始状态。取消设置环境变量,将其设置为空字符串,使用bindkey -e不会传播到脚本。当我bindkey -v在主 shell 中使用 和 返回切换模式时,所有键在两种模式下都按预期运行。

在脚本中, vi 编辑工作正常,与主 shell 相同,但执行后bindkey -e,输入行为奇怪。 Home并且End什么也不做,但会干扰后续的击键和Del打印~

如果我测试按键发送的内容,并在每个按键前面加上Ctrl+ V,我会[OH^[OF^[[3~进入主 shell,但[[H^[[F^[[3~在脚本中。因此,两个不同,行为不同,一个相同,但行为不同。

当然,我不必仅仅为了脚本的可移植性而定义键绑定。他们为什么会破裂?

Zsh 是 v5.9,脚本如下:

#!/usr/bin/zsh
bindkey -e
vared -p 'x: ' -c x

我想提供一个提示来编辑带有预填充建议的字符串,并且人们希望这些键在文本输入中以某种方式工作。其他 Emacs 序列也可以工作,但最好不要向非技术用户教授这些序列。

答案1

HomeEndInsert、 、Delete、对于那些拥有它们的键盘以及对于那些按下时发送一些唯一字符或字符序列的终端,默认情况下不会在 zsh 的任何键盘映射(emacs、vi-insert、vi-command)中绑定PageUpPageDown..,请参阅bindkey -l$keymaps数组以获取完整列表)。

当与or或任一组合时,功能键或上面的功能键或箭头键或Tab,没有任何绑定(同样对于那些确实为这些键发送唯一序列的终端)。BackspaceEscapeShiftControlAlt

默认情况下,箭头键 ( UpDownLeftRight) 是绑定的,因为大多数终端都会在这些键上发送相同的转义序列。这几乎总是^[[A......^[[D^[OA......^[OD取决于终端和/或它是否在键盘传输模式(见smkxterminfo(5)或不。

您可以通过使用选项运行(跳过系统或用户初始化文件(zshenv 除外))并运行来查看各种键zsh映射中的默认键绑定:zsh-f

for m ($keymaps) bindkey -M $m | grep -H --label=$m .

手册还将向您显示在键盘映射中的哪些键找到了哪些emacsvicmd部件viins

如果将上面的循环通过管道传递给grep '\^\[[[O][A-D]',您会看到,无论 的值如何$TERM

% (for m ($keymaps) bindkey -M $m | grep -H --label=$m .) | grep '\^\[[[O][A-D]'
visual:"^[OA" up-line
visual:"^[OB" down-line
visual:"^[[A" up-line
visual:"^[[B" down-line
viopp:"^[OA" up-line
viopp:"^[OB" down-line
viopp:"^[[A" up-line
viopp:"^[[B" down-line
vicmd:"^[OA" up-line-or-history
vicmd:"^[OB" down-line-or-history
vicmd:"^[OC" vi-forward-char
vicmd:"^[OD" vi-backward-char
vicmd:"^[[A" up-line-or-history
vicmd:"^[[B" down-line-or-history
vicmd:"^[[C" vi-forward-char
vicmd:"^[[D" vi-backward-char
main:"^[OA" up-line-or-history
main:"^[OB" down-line-or-history
main:"^[OC" forward-char
main:"^[OD" backward-char
main:"^[[A" up-line-or-history
main:"^[[B" down-line-or-history
main:"^[[C" forward-char
main:"^[[D" backward-char
viins:"^[OA" up-line-or-history
viins:"^[OB" down-line-or-history
viins:"^[OC" vi-forward-char
viins:"^[OD" vi-backward-char
viins:"^[[A" up-line-or-history
viins:"^[[B" down-line-or-history
viins:"^[[C" vi-forward-char
viins:"^[[D" vi-backward-char
emacs:"^[OA" up-line-or-history
emacs:"^[OB" down-line-or-history
emacs:"^[OC" forward-char
emacs:"^[OD" backward-char
emacs:"^[[A" up-line-or-history
emacs:"^[[B" down-line-or-history
emacs:"^[[C" forward-char
emacs:"^[[D" backward-char

在大多数键盘映射中,这 4 个箭头键的两种转义序列都与您通常期望它们在给定上下文中执行的操作绑定在一起。

这些小部件也绑定到常用的 emacs/vi 键(例如^Bemacs模式下的^F, , ^P,或vi-cmd 模式下的, , , )。^Nhjkl

当然, 您还会发现EscTab、 的绑定,所有终端都会发送众所周知的单个控制字符(尽管对于 ,有发送 BS 的终端和发送 DEL 的终端)。BackspaceEnterBackspace

但您不会找到任何有关任何其他功能键的信息。

例如,有一种方法可以获取 terminfo 数据库已知的终端在按下 时发送的内容的摘要End

$ (typeset -A count; for TERM (/usr/share/terminfo/*/*(.:t)) (( count[\$terminfo[kend]]++ )); typeset -p1 count)
typeset -A count=(
  [$'\M-\C-@O']=8
  ['']=1341
  [$'\M-\C-?\M-(']=6
  [$'\C-Ak\C-M']=1
  [$'\C-SI']=3
  [$'\C-[)4\C-M']=4
  [$'\C-[0']=8
  [$'\C-[F']=1
  [$'\C-[K']=4
  [$'\C-[OF']=99
  [$'\C-[T']=13
  [$'\C-[Y']=1
  [$'\C-[[146q']=23
  [$'\C-[[1~']=11
  [$'\C-[[220z']=21
  [$'\C-[[24;1H']=3
  [$'\C-[[4~']=139
  [$'\C-[[5~']=9
  [$'\C-[[8~']=28
  [$'\C-[[F']=49
  [$'\C-[[K']=1
  [$'\C-[[OF']=1
  [$'\C-[[U']=15
  [$'\C-[[Y']=16
  [$'\C-[[d']=1
  [$'\C-[_1\C-[\\']=1
  [$'\C-[k']=3
  [$'\C-[z']=13
  ['- @']=1
  ['-45~']=1
  ['-4~']=5
  [1!]=1
)

您会看到,对于大多数终端,它们发送的内容(如果有)是未知的。$'\C-[[4~'是最常见的。

Delete现在通常发送\e[3~,但有时会发送 DEL ( ^?) (目前最常在退格键上发送),并且对于许多人来说,可以在终端仿真器设置中进行配置。某些终端仿真器还可以被告知要仿真什么类型的键盘,以及将为这些功能键发送不同的序列。例如,参见 xterm,引用其手册:

-kt 键盘类型
该选项设置keyboardType资源。可能的值包括:“未知”、“默认”、“传统”、“hp”、“sco”、“sun”、“tcap”和“vt220”。

现在,用户将知道他们将使用什么类型的键盘、什么终端仿真器以及在什么系统上。操作系统供应商还可以做出比 zsh 更有根据的猜测(zsh 已在数千个不同的系统上使用了 30 多年)。

例如,用于 x86 PC 的 Debian GNU/Linux 发行版试图维护一个 terminfo 数据库,该数据库相对忠实于它包含在其软件包中的几十个终端仿真器(其中大多数是xterm类似的),可以合理地知道发送的转义序列只要用户不决定更改终端仿真器的默认配置并且不从外来操作系统远程登录,就可以使用 PC 键盘上常见的几个功能键。

所以你会发现 Debian 将其添加到/etc/zsh/zshrc(交互式调用的系统自定义文件zsh)中:

# /etc/zsh/zshrc: system-wide .zshrc file for zsh(1).
#
# This file is sourced only for interactive shells. It
# should contain commands to set up aliases, functions,
# options, key bindings, etc.
#
# Global Order: zshenv, zprofile, zshrc, zlogin

READNULLCMD=${PAGER:-/usr/bin/pager}

# An array to note missing features to ease diagnosis in case of problems.
typeset -ga debian_missing_features

if [[ -z "${DEBIAN_PREVENT_KEYBOARD_CHANGES-}" ]] &&
   [[ "$TERM" != 'emacs' ]]
then

    typeset -A key
    key=(
        BackSpace  "${terminfo[kbs]}"
        Home       "${terminfo[khome]}"
        End        "${terminfo[kend]}"
        Insert     "${terminfo[kich1]}"
        Delete     "${terminfo[kdch1]}"
        Up         "${terminfo[kcuu1]}"
        Down       "${terminfo[kcud1]}"
        Left       "${terminfo[kcub1]}"
        Right      "${terminfo[kcuf1]}"
        PageUp     "${terminfo[kpp]}"
        PageDown   "${terminfo[knp]}"
    )

    function bind2maps () {
        local i sequence widget
        local -a maps

        while [[ "$1" != "--" ]]; do
            maps+=( "$1" )
            shift
        done
        shift

        sequence="${key[$1]}"
        widget="$2"

        [[ -z "$sequence" ]] && return 1

        for i in "${maps[@]}"; do
            bindkey -M "$i" "$sequence" "$widget"
        done
    }

    bind2maps emacs             -- BackSpace   backward-delete-char
    bind2maps       viins       -- BackSpace   vi-backward-delete-char
    bind2maps             vicmd -- BackSpace   vi-backward-char
    bind2maps emacs             -- Home        beginning-of-line
    bind2maps       viins vicmd -- Home        vi-beginning-of-line
    bind2maps emacs             -- End         end-of-line
    bind2maps       viins vicmd -- End         vi-end-of-line
    bind2maps emacs viins       -- Insert      overwrite-mode
    bind2maps             vicmd -- Insert      vi-insert
    bind2maps emacs             -- Delete      delete-char
    bind2maps       viins vicmd -- Delete      vi-delete-char
    bind2maps emacs viins vicmd -- Up          up-line-or-history
    bind2maps emacs viins vicmd -- Down        down-line-or-history
    bind2maps emacs             -- Left        backward-char
    bind2maps       viins vicmd -- Left        vi-backward-char
    bind2maps emacs             -- Right       forward-char
    bind2maps       viins vicmd -- Right       vi-forward-char

    # Make sure the terminal is in application mode, when zle is
    # active. Only then are the values from $terminfo valid.
    if (( ${+terminfo[smkx]} )) && (( ${+terminfo[rmkx]} )); then
        function zle-line-init () {
            emulate -L zsh
            printf '%s' ${terminfo[smkx]}
        }
        function zle-line-finish () {
            emulate -L zsh
            printf '%s' ${terminfo[rmkx]}
        }
        zle -N zle-line-init
        zle -N zle-line-finish
    else
        for i in {s,r}mkx; do
            (( ${+terminfo[$i]} )) || debian_missing_features+=($i)
        done
        unset i
    fi

    unfunction bind2maps

fi # [[ -z "$DEBIAN_PREVENT_KEYBOARD_CHANGES" ]] && [[ "$TERM" != 'emacs' ]]
[...]

正如您在上面看到的,它定义了一个关联数组,将键名称映射到相应的转义序列。这些转义序列是从 Debian 默认情况下附带的 terminfo 数据库中检索的(尽管默认情况下条目列表有限)。

但是因为 terminfo 数据库只给出终端在 in 时发送的转义序列键盘传输模式,您会看到 zshrc 还告诉 ZLE 在启动时进入该模式并在退出时离开该模式。

zshrc这就解释了为什么在交互式 zsh shell(运行该 shell 的地方)的提示符下(在 ZLE 内)和没有提示符时,您会看到不同的序列。

zsh 的脚本和更常见的非交互式调用不会读取 zshrc。由于 zshrc 是您放置所有自定义、别名、函数的地方......如果脚本读取它们,那肯定会破坏它们。

现在vared还使用 ZLE,并且当在不读取 zshrc 的脚本中调用时,这意味着它不会在 /etc/zsh/zshrc 中获取操作系统提供的绑定。

因此,如果您喜欢这些绑定,并且希望这些脚本的用户可以使用它们,无论他们是否使用与您相同的操作系统,您都需要自己将它们包含在您的脚本中。

另请注意,ZLE默认处于emacs或模式,具体取决于或是否以或开头,并且包含或不包含,如果您希望脚本的用户具有一致的键绑定,您可能还需要强制它们进入一种或另一种模式。默认情况下,配置键盘映射的绑定,它是 或 的别名,具体取决于是否选择了 emacs 或 vi 模式(使用或)。vi$EDITOR$VISUALvi/vibindkeymainemacsviinsbindkey -ebindkey -v

除了 Debian 之外,您还会发现许多其他有关绑定功能键的自定义建议,包括与Shift/ Alt/组合Ctrl以模仿其他编辑器的行为(在为这些编辑器发送唯一序列的终端上)。

参见例如zsh zle 移位选择关于堆栈溢出oh-my-zsh的键绑定您可以从中获得灵感。


1 查看 csh 脚本通常如何有一个#! /bin/csh -fshebang,其中-f跳过阅读~/.cshrc以避免遇到此类问题。

相关内容