1. 29

  2. 5

    I’ll add something from my old zshrc – build the missing vim text objects when bind -v is turned on (stuff like ca"):

    autoload -U select-quoted
    zle -N select-quoted
    for m in visual viopp; do
        for c in {a,i}{\',\",\`}; do
            bindkey -M $m $c select-quoted

    link to all my zshrc vim stuff here (note: cursorStyle is a function for setting the cursor display kind, here)

    Nowadays I get my vim bindings in a shell context with emacs shell-mode (so I get all my vim/evil plugins there as well).

    1. 4

      select-quoted is also useful with a number of other characters, all of the following are useful {a,i}"${(s..):-\'\"\|,./:;-=+@}`

      There’s also a select-bracketed for things like i(, i{, i< which you don’t seem to use. And surround is also available.

      1. 1

        ooo nice, thanks for the tips

    2. 3

      Nice post, there’s some great stuff here!

      I’ve got a few random things that might be interesting that I’ve learned in my time maintaining prezto and zsh-utils.

      Exists could also be written with the plus operator, the commands array, and the arithmetic parens. This should be slightly faster as it doesn’t have to match the given arg against a pattern - it just uses the associative array that’s already there. Note that there are also arrays for $functions and I thought there was one that was a combined array of functions, commands, and aliases but I can’t seem to find it.

      _exists() { (( $+commands[$1] )) }

      This is a modification to _postpath which uses a zsh builtin to handle the readlink portion which allows it to work on more platforms (it’s especially nice because it should work with both macOS, BSD, and Linux). It could obviously be used for _prepath as well.


      _postpath() {
          for dir in "$@"; do
              [[ ! -d "$dir" ]] && return
              path=($path[@] "$dir")

      One really good example of where ZLE can be useful is prepending sudo on a keybind. https://github.com/sorin-ionescu/prezto/blob/master/modules/editor/init.zsh#L232

      I assume you could do this with zle -U, but the nice thing about this is that it doesn’t move the cursor.

      prepend-sudo() {
        if [[ "$BUFFER" != su(do|)\ * ]]; then
          BUFFER="sudo $BUFFER"
          (( CURSOR += 5 ))
      zle -N prepend-sudo
      1. 3

        Ah, thanks; I’ll update the _exists and _{pre,post}path functions.

        To be honest I never really looked much at zle, and always found it a bit confusing. I need to sit down and properly look at it one day. There’s some other useful stuff in that zshrc as well btw (like using terminfo instead of bindkey '^[OA').

        1. 2

          Yeah there’s a ton in zle. It’s super powerful but there are also a lot of pitfalls. If you’re looking for something simpler than presto or oh-my-zsh take a look at zsh-utils or zsh4humans. They’re both really solid places to start.

          1. 1

            I love zsh and all but confused why you made the _exists function? Is command -v not enough? Thats also portable to any bourne shell as well.

            1. 1

              It’s just so much work to type!

              command -v vim >/dev/null || alias vi=vim
          2. 1

            For sudo, I am using this version:

            # Meta-S will toggle sudo
            function vbe-sudo-command-line() {
                [[ -z $BUFFER ]] && zle up-history
                if [[ $BUFFER == sudo\ * ]]; then
                    LBUFFER="${LBUFFER#sudo }"
                    LBUFFER="sudo $LBUFFER"
            zle -N vbe-sudo-command-line
            bindkey "\es" vbe-sudo-command-line

            For _postpath, it could be further simplified to (relying on Zsh not doing word-splitting by default and arrays are not nested:

            _postpath() {
                for dir in "$@"; do
                    [[ ! -d $dir ]] && return
                    path=($path $dir)

            I don’t rely on typeset -U myself as it can be fooled with symlinks (and I want to keep symlinks in path, notably for the path to Nix profile). So, I do the dupe checks manually: https://github.com/vincentbernat/zshrc/blob/master/zshenv#L28

          3. 3

            Define directory shortcuts

            I just have cdpath=(~/src/github.com/myfreeweb) so I can just type e.g. dotf<Tab><Enter> to cd to ~/src/github.com/myfreeweb :)

            (not having to type cd is setopt auto_cd)

            Filter history completion with what you typed

            https://github.com/zsh-users/zsh-history-substring-search — why limit yourself to “entries that start with ls” when you can search for “entries that contain ls” this way!

            Some helper functions to prepend or append to PATH which also checks if the path exists

            noooooo this functionality is built in!! Just use (N) when adding paths:

            path=(~/.local/bin(N) ~/bin(N) … )

            P.S. my config: https://github.com/myfreeweb/dotfiles/tree/master/zsh

            1. 3

              I’m not a huge fan of cdpath myself; I tried it for a while a few years ago and found it too “magic”, especially once you start adding a whole bunch of directories in there. Same with autocd btw; I toggled between having that on and off way back in my tcsh days, and eventually decided that explicitly typing commands isn’t too much of a burden. But yeah, if that works for you: 👍 One of the nice things of zsh is that it gives you plenty of tools to do things the way you want; these are just some things I find useful personally but hadn’t seen much in other articles (which is why it excludes e.g. “show git branch in prompt”, as there are already 600 articles on that).

              Personally I find “start with ” almost always a much more useful way to search. Searching for ls also matches alsamixer, tools, tls, totals, docker ls, and probably a bunch of other things. Usually I know what I want (history for a specific command), and the computer is generally dumber than me for these kind of things on account of not being able to read my mind. I never quite understood how anyone can deal with things like “fuzzy searches” btw.

              What does that (N) do? I can’t seem to find any docs on it, and it doesn’t seem to behave quite the same?

              [~]% ls -ld /bin /usr/bin
              lrwxrwxrwx 1 root root     7 Apr  6 15:36 /bin -> usr/bin/
              drwxr-xr-x 2 root root 57344 May 22 15:38 /usr/bin/
              [~]% path=(/bin(N) /usr/bin(N))
              [~]% echo $path
              /bin /usr/bin
              [~]% path=() 
              [~]% _postpath /bin
              [~]% _postpath /usr/bin
              [~]% echo $path
              1. 2

                When using history-incremental-search-backward (bound to ^R in emacs mode), note that you can start the pattern with ^ to get an anchored search.

                1. 2

                  I haven’t bothered to actually read the docs before :) turns out this is called glob qualifiers and you can use them just normally in shell expansion:

                  % echo /bin(N) /nonexistent(N)

                  The documentation is not really discoverable: you’d never find “sets the NULL_GLOB option for the current pattern” with Ctrl-F “exist” :D

                  If you combine / (directories) with N (existing), looks like that does what you want:

                  % ln -s /bin /tmp/symlink
                  % echo /bin(/N) /tmp/symlink(/N)

                  Turns out this feature is very powerful:

                  % ls /var/cache/pkg/*(m+365)
                  /var/cache/pkg/docbook-1.5-a2e793cdbf.txz      /var/cache/pkg/lua52-json-1.3.4-cf2cea4c71.txz

                  (yes, that’s files last written over 365 days ago)

              2. 3

                Cc @arp242 this looks like it 404s?

                1. 3

                  Oops, should be fixed now; forgot to add a line when linking to this thread. “I’ll just make this simple change real quick, no need to test locally”.

                  1. 1

                    Lol, the best kind of bug

                2. 2

                  I’ll give all the zsh users my favorite global aliases i’ve used for years:

                  alias -g silent="> /dev/null 2>&1"
                  alias -g noerr="2> /dev/null"
                  alias -g stdboth="2>&1"
                  # mainly for firefox, because it can suck up the cycles, but not when
                  # in a SIGSTOP state you can't use any power then firefox.
                  alias -g sigcont='kill -CONT '
                  alias -g sigstop='kill -STOP '

                  And my fave keybind to save what I typed and get it back after i run the thing i just forgot:

                  bindkey '^P' push-line # Saves the current line and returns to it afterwards.

                  I use them ALL THE TIME, aka: running a chatty command that you don’t want output for:

                  terraform apply -auto-approve silent

                  Or just filter out stderr for reasons:

                  chatty_stderr_command noerr

                  Or snagging stdout/err to a file:

                  docker build -t zomg . stdboth | tee whatever.log

                  Most of my zsh shenanigans are for just getting in/out of the shell as fast as I can and back into the warm embrace of emacs, so my shell stuff is pretty boring.

                  1. 1

                    Note that push-line is already mapped to M-q by default.

                    1. 1

                      push-line is pretty neat and didn’t know about it. I ended up doing it like this:

                      remember() {
                      	# Nothing in buffer: get previous command.
                      	if [[ $#BUFFER -eq 0 ]]; then
                      		print -ln "${stored:-}"
                      	# Store current input.
                      zle -N remember
                      bindkey '^Q' remember

                      It’s essentially the same, except that you need to explicitly put the pushed value back with a second ^Q press, which seems a bit nicer to me.

                      A related thing I found/added btw:

                      # Run command but don't clear commandline and preserve cursor position.
                      bindkey '^\'    accept-and-hold

                      Which seems pretty useful when mucking about with longer curl command etc.

                    2. 1

                      Great post. I picked up a few news zsh configs. Thanks!

                      A sidenote: From what I understand, readlink -f is not entirely portable. For example, on macos readlink does not support the -f flag, but on FreeBSD (as well are gnu/coreutils) it does.

                      1. 1

                        Oh yeah, you’re right. readlink is not always installed by default on Linux either (on some distros it’s a separate package, I forgot which one, and perhaps that changed now since I encountered this >5 years ago). It’s worked on all systems I’ve used in the last ~2 years though (including busybox), so it’s “good enough” for me 😅

                        1. 2

                          Yeah, if it works, it works! Good enough. :)

                          My zsh tip to share.. on macos, if using the os provided zsh (and not homebrew), make sure to set HELPDIR! The default helpdir is pretty wonky in the build for some reason.

                          ~% grep -m 1 HELPDIR /usr/share/zsh/${ZSH_VERSION}/functions/run-help
                          local HELPDIR="${HELPDIR:-/AppleInternal/BuildRoot/Library/Caches/com.apple.xbs/Binaries/zsh/install/TempContent/Root/usr/share/zsh/5.7.1/help}"

                          Setting HELPDIR to /usr/share/zsh/${ZSH_VERSION}/help fixed a frustration with nonfunctional run-help I was struggling with.

                      2. 1

                        could someone explain any preference over the wd plugin in contrast to the hash -d functionality ?

                        1. 1

                          As I understand it, wd is a “super-charged cd”, whereas with hash -d it works everywhere and you can do stuff like vim ~x/foo and cp ~x/foo . etc.

                          1. 2

                            And as you stated in your article, the short form can also be displayed in the prompt easily. Zsh is quite powerful here. In addition to the static named directories feature you describe, there is a dynamic one. An old post of mine around that: https://vincent.bernat.ch/en/blog/2015-zsh-directory-bookmarks.

                        2. 1

                          Ah, this is great. I can finally stop using oh-my-zsh now that I know how to replicate the features I care about.