Custom Backward Word Deletion in Zsh

Custom Backward Word Deletion In ZSH

1. Intro

Do you use ctrl+w keyboard shortcut in Zsh? It’s a handy function for deleting “words”. Some of you may be wondering, how does Zsh know that a “word” is? Can I change what a “word” means? If neither of those questions sound interesting, this post isn’t for you. If they do, stay tuned. I’ll show you how ctrl+w works along with a basic and an advanced example of customizing it.

2. What is a “word”?

What do you think a “word” means? You may think of the words that you are currently reading. A sequence of symbols containing [a-z] and [A-Z]. Commands typed out in your terminal contain more than letters. They contain brackets, punctuation, and even mathematical symbols.

Zsh needs to know which special characters you want to consider part of a “word”. It uses an environmental variable called WORDCHARS. You can inspect the variable with the echo command:

$ echo $WORDCHARS
*?_-.[]~=/&;!#$%^(){}<>

It’s defined as:

WORDCHARS A list of non-alphanumeric characters considered part of a word by the line editor.

When you press ctrl+w, Zsh will delete all letters plus any symbols in WORDCHARS. If you want ctrl+w to stop deleting up to certain symbol, we remove it from WORDCHARS.

3. Basic Example

Let’s walk through a basic example example. The default WORDCHARS variable does not include colons (:). To test this you can:

  1. Type a string with colons on you command line (e.g 1234🔢1234)
  2. Hit ctrl+w
  3. Watch as the cursor stops at each colon

Let’s update WORDCHARS to include colons. You can update your ~/.zshrc to add the colon to the $WORDCHARS variable by running this command:

echo "export WORDCHARS='${WORDCHARS}:'" >> ~/.zshrc

To see the change you will need to reload your ~/.zshrc or open a new terminal window. If your run the same experiment above you’ll see you only need to hit ctrl+w once.

Being able to update WORDCHARS like this may give you more than enough mileage. Or, if you like to tinker, you may want even want more customization. Let’s ramp it up.

4. Advanced Example

I like Vim. I like Vim’s visual mode motions. Vim defines both a word and a WORD, which you can check out with :help word:

word

A word consists of a sequence of letters, digits and underscores, or a sequence of other non-blank characters, separated with white space (spaces, tabs, ). This can be changed with the ‘iskeyword’ option. An empty line is also considered to be a word.

WORD

A WORD consists of a sequence of non-blank characters, separated with white space. An empty line is also considered to be a WORD.

We’re going to build similar functionality into Zsh. Our default ctrl+w will be for deleting a word. We’ll also define ctrl+alt+w to delete a WORD. Why? Because we hate holding the backspace key down.

4.1 ZSH Widgets

Widgets are how zsh performs actions in your terminal. Everything from moving the cursor to command completion to executing commands.

First we need to figure out which widget we are trying to extend. We know that the key command is ctrl+w. This is where the bindkey command comes in. Browsing the commands output helps us figure out what zsh widget we are trying to extend:

$ bindkey
...
"^W" backward-kill-word
...

4.2 All Together Now

We know which widget we’re extending, backward-kill-word. We also know how to customize the widget, with WORDCHARS. Combining these, we can make our own custom widgets and then bind them:

# This will be our new default `ctrl+w` command
my-backward-delete-word() {
    # Copy the global WORDCHARS variable to a local variable. That way any
    # modifications are scoped to this function only
    local WORDCHARS=$WORDCHARS
    # Use bash string manipulation to remove `:` so our delete will stop at it
    WORDCHARS="${WORDCHARS//:}"
    # Use bash string manipulation to remove `/` so our delete will stop at it
    WORDCHARS="${WORDCHARS//\/}"
    # Use bash string manipulation to remove `.` so our delete will stop at it
    WORDCHARS="${WORDCHARS//.}"
    # zle <widget-name> will run an existing widget.
    zle backward-delete-word
}
# `zle -N` will create a new widget that we can use on the command line
zle -N my-backward-delete-word
# bind this new widget to `ctrl+w`
bindkey '^W' my-backward-delete-word

# This will be our `ctrl+alt+w` command
my-backward-delete-whole-word() {
    # Copy the global WORDCHARS variable to a local variable. That way any
    # modifications are scoped to this function only
    local WORDCHARS=$WORDCHARS
    # Use bash string manipulation to add `:` to WORDCHARS if it's not present
    # already.
    [[ ! $WORDCHARS == *":"* ]] && WORDCHARS="$WORDCHARS"":"
    # zle <widget-name> will run that widget.
    zle backward-delete-word
}
# `zle -N` will create a new widget that we can use on the command line
zle -N my-backward-delete-whole-word
# bind this new widget to `ctrl+alt+w`
bindkey '^[^w' my-backward-delete-whole-word

After you add this to your ~/.zshrc you can test it against the following strings:

asdf/asdf/asdf/
asdf:asdf:asdf:
asdf.asdf.asdf

5. Conclusion

Zsh has some powerful mechanisms for customizing your command line experience. This post only scratches a small surface. Hope you enjoy your new widgets! Happy hacking!

Additional Reading: