2

I learned after much frustration that having $EDITOR initially set to vim doesn't affect the main shell but does affect scripts and nothing seems to undo this initial state. Unsetting the env var, setting it to the empty string, using bindkey -e doesn't propagate to scripts. When I switch modes with bindkey -v and back, in the main shell, all keys behave as expected in both modes.

In a script, vi editing works fine, the same as the main shell, but after executing bindkey -e, input behaves oddly. Home and End do nothing but can interfere with subsequent keystrokes and Del prints ~.

If I test what's being sent by the keys, prefacing each with Ctrl + V, I get [OH^[OF^[[3~ in the main shell but [[H^[[F^[[3~ in the script. So two are different and behave differently, and one is the same but behaves differently.

Surely I shouldn't have to define keybindings just for a script to work portably. Why are they breaking?

Zsh is v5.9 and the script is just this:

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

I want to offer a prompt to edit a string with a pre-filled suggestion, and people expect those keys to work a certain way in a text input. Other Emacs sequences work, but it would be better not to have to teach non-technical users those.

5
  • Those keys are not mapped by default maybe your OS binds them for interactive use in /etc/zsh/zshrc Commented Feb 5, 2024 at 10:03
  • See also .zshrc shortcut working on Arch but not Ubuntu and zsh: Where is the `key` -> `terminfo` dictionary defined Commented Feb 5, 2024 at 17:01
  • Thanks, @StéphaneChazelas. Those links explain a lot but seem to be about solving binding issues in the default interactive mode. Are you saying it's normal for zsh scripts to not inherit key bindings, and that each script is required to define its own? Commented Feb 6, 2024 at 2:03
  • You get zsh's key bindings, if you want additional key bindings you need to define them. If you want key bindings to affect all interactive or non-interactive invocations of zsh by any user of the system, you can put them in /etc/zsh/zshenv. Or ~/.zshenv for only your invocations. Commented Feb 6, 2024 at 15:15
  • @StéphaneChazelas I assumed I'd get zsh's key bindings but that's not the behaviour I'm seeing. In the zle of vared in that script, those three keys (and possibly others I'm unaware of) behave differently to how they do in the parent zsh, from which I run the script. I really don't want to make permanent changes to a system as the script is intended to be portable. Commented Feb 7, 2024 at 3:24

1 Answer 1

2

Home, End, Insert, Delete, PageUp, PageDown for those keyboards that have them and for those terminals that send some unique characters or sequences of characters when they're pressed are not bound by default in any of zsh's keymaps (emacs, vi-insert, vi-command..., see bindkey -l or the $keymaps array for the full list).

There's nothing bound to function keys, or those above or arrow keys or Tab, Backspace, Escape when combined with Shift or Control or Alt either (again for those terminals that do send unique sequences for those).

Arrow keys (UpDownLeftRight) are bound by default as most terminals send the same escape sequences upon those. That's almost always either ^[[A...^[[D or ^[OA...^[OD depending on the terminal and/or whether it's in keypad transmit mode (see smkx in terminfo(5)) or not.

You can see zsh's default key bindings in the various keymaps, by running zsh with the -f option (which skips system or user initialisation files except the zshenv ones) and run:

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

The manual will also show you what widgets are found to which keys in the emacs/vicmd/viins keymaps.

If you pipe that loop above to grep '\^\[[[O][A-D]', you see, regardless of the value of $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 

Both flavours of escape sequences for those 4 arrow keys are bound in most keymaps to the actions you usually expect them to have in the given contexts.

Those widgets are also bound to the usual emacs/vi keys as well (like ^B, ^F, ^P, ^N in emacs mode or h, j, k, l in vi-cmd mode).

You'll also find bindings of course for Esc, Tab, Backspace, Enter for which all terminals send very well known single control characters (though for Backspace, there are those that send BS and those that send DEL).

But you won't find anything about any other function key.

Here's a way for instance to get a summary of what is sent by the terminals known to the terminfo database upon pressing 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 ) 

You'll see that for most terminals, what they send if any is not known. $'\C-[[4~' is the most common.

Delete nowadays commonly sends \e[3~, but sometimes DEL (^?) instead (the one most commonly sent upon backspace these days) and for many, that can be configured in the terminal emulator settings. Some terminal emulators can also be told what type keyboard to emulate and different sequences would be sent for those function keys. See for instance for xterm, quoting its manual:

-kt keyboardtype
This option sets the keyboardType resource. Possible values include: “unknown”, “default”, “legacy”, “hp”, “sco”, “sun”, “tcap” and “vt220”.

Now, a user will know what kind a keyboard, what terminal emulators and on what system they will use. An operating system vendor can also make a more educated guess than zsh (which has been used on thousands of different systems for over 30 years).

For instance, a distribution of Debian GNU/Linux for x86 PCs that tries to maintain a terminfo database relatively faithful to the few dozen terminal emulators it includes among its packages (most of them being xterm-like) can reasonably know what escape sequences are sent upon those few function keys commonly found on PC keyboards as long as users don't decide to change the configuration of their terminal emulators from the default and don't login remotely from alien operating systems.

So you'll find that Debian adds this to /etc/zsh/zshrc (the system customisation file for the interactive invocations of 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' ]] [...] 

As you can see above, it defines an associative array that maps key names to corresponding escape sequences. Those escape sequences are retrieved from the terminfo database which Debian happens to ship by default (though with a limited list of entries by default).

But because the terminfo database only gives the escape sequences that terminal send when in keypad transmit mode, you'll see that zshrc also tells ZLE to enter that mode when starting and leave it upon exiting.

That explains why you see different sequences when at the prompt (within ZLE) of an interactive zsh shell (where that zshrc is run) and when not.

Scripts and more generally non-interactive invocations of zsh don't read zshrc. Since zshrc is where you put all your customisations, aliases, functions... if scripts read them, that would be sure to break them¹.

Now vared also uses the ZLE, and when invoked in scripts where zshrc is not read, that means it won't get the bindings supplied by your operating system in /etc/zsh/zshrc.

So, if you like those bindings and you want them to be available to users of those scripts whether they are using the same operating system as you or not, you'll need to include them in your script by yourself.

Also beware that ZLE is in emacs or vi mode by default depending on whether $EDITOR or $VISUAL starts with vi or contains /vi or not, and you may also want to force them into one mode or another if you want consistent key bindings for users of your script. bindkey by default configures the bindings of the main keymap which is an alias to either emacs or viins depending on whether the emacs or vi mode is selected (with bindkey -e or bindkey -v).

Besides the Debian ones, you'll find many other suggestions of customisations around to bind the function keys including in combinations with Shift/Alt/Ctrl to mimic the behaviour of some other editors (on terminals that send unique sequences for those).

See for instance Zsh zle shift selection on Stack Overflow of oh-my-zsh's key bindings which you could get inspiration from.


¹ see how csh scripts usually have a #! /bin/csh -f shebang, where -f skips reading the ~/.cshrc to avoid them running into this kind of problem.

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.