Bash – Confusing Behavior of Emacs-Style Keybindings in Bash

bashemacskeyboard shortcutsreadline

Bash offers many useful emacs-style keybindings for simple commandline editing. For example, Ctrl+w deletes ("kills") word left from the cursor.

Another keybinding, Alt+d is supposed to be a "mirror" of the first one. It is supposed to delete a word right from the cursor.

However, I have noticed, these two keybindings do not act completely symetricaly. Whereas Ctrl+w treats foo.bar as one word, Alt+d treats it as two words

Even more annoyingly, # echo are two words for Ctrl+w, but one word for Alt+d.

Is there some logic in this? Is there some reason why they don't treat words in the same way?

Is there any way for me to change this?

I am using bash on Debian Wheezy

Best Answer

Different bash commands use different notions of word. Check the description of each command in the manual.

C-w kills to the previous whitespace. M-DEL (usually Alt+BackSpace) kills to the previous word boundary where words contain only letters and digits (the same as M-b and M-f), and M-d kills forward similarly.

Bash uses the Readline library to process user input, and can be configured either via ~/.inputrc or via the bind builtin in ~/.bashrc. You can bind a key to a different readline command if you wish. You can also use bind -x to bind a key to a bash functions that modifies the READLINE_LINE variable.

For example, to make M-d kill a shell word, bind it to shell-kill-word in your .bashrc:

bind '"\M-d": shell-kill-word'

To make M-d delete a whitespace-delimited word, there is no built-in function, so you need to write either a macro or a shell function. Since there is no motion command that goes by whitespace-delimited words, you need a function at least for that part.

delete_whitespace_word () {
  local suffix="${READLINE_LINE:$READLINE_POINT}"
  if [[ $suffix =~ ^[[:space:]]*[^[:space:]]+ ]]; then
    local -i s=READLINE_POINT+${#BASH_REMATCH[0]}
    READLINE_LINE="${READLINE_LINE:0:$READLINE_POINT}${READLINE_LINE:$s}"
  fi
}
bind -x '"\ed": delete_whitespace_word'

To make M-d kill a whitespace-delimited word is more complicated because as far as I know, there is no way to access the kill ring from bash code. So this requires a function to find the end of the portion to kill, and a macro to follow this by the actual killing.

forward_whitespace_word () {
  local suffix="${READLINE_LINE:$READLINE_POINT}" 
  if [[ $suffix =~ ^[[:space:]]*[^[:space:]]+ ]]; then
    ((READLINE_POINT += ${#BASH_REMATCH[0]}))
  else
    READLINE_POINT=${#READLINE_LINE}
  fi
}
bind -x '"\C-xF": forward_whitespace_word'
bind '"\C-x\C-w": kill-region'
bind '"\ed": "\e \C-xF\C-x\C-w"'

All of this would be a lot easier in zsh.

Related Question