Zsh Tips 4: General Helpers

Esperanto • English
Last updated: March 19, 2022

A change in perspective is worth 80 IQ points.
—Alan Kay

adam-hornyak-Cm187aESg0k-unsplash

Table of contents

Introduction

Last time, I talked about helper functions to assist in managing chroot environments. In this article, I’ll talk about general helpers for working with the command line. I will also talk about helpful keybindings to speed up typing.

Functions

A beautiful thing about functions is that they’re so easy to create and use. Here are some functions that I use often.

map

When you have a command that takes only a single argument, you can simulate multiple usage of that command through this function. It is defined as:

function map () {
  for i (${argv[2,-1]}) { ${(ps: :)${1}} $i }
}

For example, you can use map to fetch multiple git repositories, serially:

% map 'git clone' git@github.com:nixos/nixpkgs.git git@github.com:tmux/tmux.git

rmap

As the name implies, rmap operates as the reverse of map—the rest of the arguments are applied as commands to the first argument. It is defined as:

function rmap () {
  for i (${argv[2,-1]}) { ${(ps: :)${i}} $1 }
}

You may use it, for example, to check disk usage, view file information, and view the open file descriptors of a file or directory:

% rmap somedir 'du -h' stat 'sudo lsof'

fp

I want a quick way to quickly determine the real and absolute path of a given file or directory. This helps me a lot in scripting. I have fp which is defined as:

function fp () {
  echo "${1:A}"
}

If I am in a symlinked directory, and I want do determine the real path of ., I can run:

% fp .

This function will be of important use in the next sections.

d

Most of the time, when I change to a directory, I need to perform a series of commands. I want to save time typing, so instead of running two commands, I can run only one. I also want a way to quickly switch through the directory stack created by pushd. I have it defined as:

function d () {
  if (( ! $#@ )); then
      builtin cd
  elif [[ -d $argv[1] ]]; then
      builtin cd "$(fp $argv[1])"
      $argv[2,-1]
  elif [[ "$1" = -<-> ]]; then
      builtin cd $1 > /dev/null 2>&1
      $argv[2,-1]
  else
    echo "$0: no such file or directory: $1"
  fi
}

When I run d alone:

% d

I go back to my home directory, just like a standalone cd would do:

When I run d with a directory and a command:

% d ~/Downloads ls -l

I change directory to ~/Downloads then I run ls -l display the directory listing of that directory.

If the output of dirs -v is:

0       /usr/local
1       /tmp

Then, when I run d with the second entry as its argument plus a command:

% d -1 date

I change directory to /tmp/, then I execute the date command.

d!

As the name implies, d! is like its cousin, only that, when the directory does not exist, it creates it for you, switches to it, then behaves as d. It is defined as:

function d! () {
  mkdir -p $argv[1]
  d "$@"
}

For example, I can use d! to stage a directory before downloading an ISO:

% d! ~/Downloads/iso https://www.foo.bar/baz/meh/meh.iso

rm!

When I am certain that I want to delete a file or directory, I don’t want to be bothered by prompts, while at the same time I want to make an exception not to accidentally delete my home directory. I defined it as:

function rm! () {
  if [[ "$1" == "$HOME" || "$1" == "$HOME/" || "$1" == "~" || "$1" == "~/" ]]; then
      return 1
  else
    command rm -rf $@
  fi
}

The command command ensures that I am calling the system binary rm instead of any shell alias or function.

rm+

When I want to quickly remove a tree containing a lot of files and directories, I use the parallel command to run the deletions in parallel, instead of serially. I have a helper function defined as:

function rm+ () {
  parallel 'rm -rf {}' ::: $@
}

Consult your package management system on how to install parallel.

rm@

From time to time, I need to delete a file or directory without the chances of recovery. For that I use the shred command. I have a helper function defined as:

function rm@ () {
  if [[ -d $1 ]]; then
      find $1 -type f -exec shred -vfzun 10 {} \;
      command rm -rf $1
  else
    shred -vfzun 10 $1
  fi
}

Consult your package management system on how to install shred.

def_mk

This helper generates helpers. It allows us to create functions that create a staging directory before the actual command is executed. It is defined as:

function def_mk () {
  eval "function ${argv[1]} () {
            if [[ \$# -ge 2 ]]; then
                if [[ ! -e \${@: -1} ]]; then
                     mkdir -p \${@: -1}
                fi

                command ${argv[2,-1]} \$@
            fi
        }"
}

To use it, supply the name of the function that will be used as a command, and the expansion itself. These invocations should ideally be in your config file.

cp!

To use def_mk with cp invoke it as:

def_mk cp! cp -rf

which expands to:

function cp! () {
  if [[ $# -ge 2 ]]; then
    if [[ ! -e ${@: -1} ]]; then
      mkdir -p ${@: -1}
    fi
    command cp -rf $@
  fi
}

The cp! command will allow us to copy files to a directory, creating that directory as necessary:

%  tree
.
├── bar.txt
└── foo.txt

0 directories, 2 files

%  cp! * a

%  tree
.
├── a
│   ├── bar.txt
│   └── foo.txt
├── bar.txt
└── foo.txt

1 directory, 4 files

mv!

To use def_mk with mv invoke it as:

def_mk mv! mv -f

which expands to:

function mv! () {
  if [[ $# -ge 2 ]]; then
    if [[ ! -e ${@: -1} ]]; then
      mkdir -p ${@: -1}
    fi
    command mv -f $@
  fi
}

The mv! command will allow us to move files to a directory, creating that directory as necessary:

%  tree
.
├── bar.txt
└── foo.txt

0 directories, 2 files

%  mv! * b

%  tree
.
└── b
    ├── bar.txt
    └── foo.txt

1 directory, 2 files

Keybindings

Aside from commands that are typed, one can also invoke keyboard shortcuts to perform arbitrary commands. Here are some that I use, a lot:

insert-last-word

When I want to insert the last word of the last command, I call insert-last-word. For example, if you have the following:

% dig foo12345.bar.baz.com
% mtr
      ^

When I execute M-x insert-last-word RET, Zsh inserts the last word of the last command, turning it to:

% dig foo12345.bar.baz.com
% mtr foo12345.bar.baz.com

This ensures that that arguments gets copied accurately.

This key is bound by default to M-.. If you want to make sure that you have it, put the following in your config:

bindkey "\e." insert-last-word

copy-prev-shell-word

When I want to repeat the last word in the current command line, I call copy-prev-shell-word. For example, if you have the following:

% cp this.is.a.file.with.a.very.long.name
                                          ^

When I execute M-x copy-prev-shell-word RET, Zsh inserts the last word, turning it to:

% cp this.is.a.file.with.a.very.long.name this.is.a.file.with.a.very.long.name
                                                                              ^

I bound it to M-=. To bind it in your config file:

bindkey "\e=" copy-prev-shell-word

Substitutions

In addition to executing M-x commands, Zsh also permits us to define keybindings that insert arbitrary text to the command line, including control characters.

I frequently have the need to process the output of a command. Usually I would do the following:

% foo `some command`

or

% foo $(some command)

The former is easier to type but it can’t be nested; the latter is too cumbersome to type. For that, I bound the key M-` as:

% bindkey -s '\e`' '$()\C-b'

So, when I have the following:

% foo
      ^

When I hit M-`, I get:

% foo $()
        ^

Quotes

I frequently have the need to quote the argument of a command, especially if it has metacharacters. A frequent case is with YouTube URLs, which contain the ? character.

For that I bound M-' as:

% bindkey -s "\e'" "''\C-b"

So, when I have the following:

% youtube-dl
             ^

When I hit M-', I get:

% youtube-dl ''
              ^

Instead of pressing three keys on my keyboard, I only get to press two, plus it ensures that I get a pair of quotes.

Putting them all together

Here are all the definitions, along with some more helpers, all in one place:

function map () {
  for i (${argv[2,-1]}) { ${(ps: :)${1}} $i }
}

function rmap () {
  for i (${argv[2,-1]}) { ${(ps: :)${i}} $1 }
}

function fp () {
  echo "${1:A}"
}

function d () {
  if (( ! $#@ )); then
      builtin cd
  elif [[ -d $argv[1] ]]; then
      builtin cd "$(fp $argv[1])"
      $argv[2,-1]
  elif [[ "$1" = -<-> ]]; then
      builtin cd $1 > /dev/null 2>&1
      $argv[2,-1]
  else
    echo "$0: no such file or directory: $1"
  fi
}

function d! () {
  mkdir -p $argv[1]
  d "$@"
}

function rm! () {
  if [[ "$1" == "$HOME" || "$1" == "$HOME/" || "$1" == "~" || "$1" == "~/" ]]; then
      return 1
  else
    command rm -rf $@
  fi
}

function rm+ () {
  parallel 'rm -rf {}' ::: $@
}

function rm@ () {
  if [[ -d $1 ]]; then
      find $1 -type f -exec shred -vfzun 10 {} \;
      command rm -rf $1
  else
    shred -vfzun 10 $1
  fi
}

function def_mk () {
  eval "function ${argv[1]} () {
            if [[ \$# -ge 2 ]]; then
                if [[ ! -e \${@: -1} ]]; then
                     mkdir -p \${@: -1}
                fi

                command ${argv[2,-1]} \$@
            fi
        }"
}

def_mk cp! cp -rf
def_mk mv! mv -f

function def_key () {
  while [[ $# -ge 2 ]]; do
    bindkey "$1" "$2"
    shift 2
  done
}

function def_keys () {
  def_key $keys
  unset keys
}

function def_out_key () {
  while [[ $# -ge 2 ]]; do
    bindkey -s "$1" "$2"
    shift 2
  done
}

function def_out_keys () {
  def_out_key $out_keys
  unset out_keys
}

keys=(
  "\e." insert-last-word
  "\e=" copy-prev-shell-word
); def_keys

out_keys=(
  '\e`' '$()\C-b'
  "\e'" "''\C-b"
); def_out_keys

Closing remarks

When using the command line, especially with a shell as powerful as Zsh, it becomes mandatory to be aware what your shell can do. Do not blindly use packages that customize your shell, without understanding what they do.

Happy Zsh’ng!