Things I Use: Zsh

While the majority of my shell work these days is done from within emacs using eshell, there are remote servers where its nice to have things setup in a familiar way. There's also always launching emacs. ;)

Dependencies

I have some dependencies for my config.

sudo apt-get install aptitude zsh{,-doc} git virtualenvwrapper git-hub

Run chsh to set your prompt to /usr/bin/zsh. Download hub at https://github.com/github/hub/releases/latest Download dropbox at https://www.dropbox.com/install?os=lnx

There are also some post-install dependencies.

ghget milkbikis/powerline-shell
cp config.py{.dist,}
./install
ghget powerline/fonts
./install.sh

Why I like it

Better completion

Zsh is basically bash with better completion mechanisms. This isn't to say the method of providing the list of possible completions is any different. I don't actually know much about that, if I'm honest. The reason I like zsh's completion may seem trivial, but when you finish completing something, it doesn't leave little tracks along the way.

In bash, when you tab complete, the possible completions are listed in your terminal and you are provided again with your prompt with your command filled in as you had it. If you hit tab again, you get another screen full of text, and the prompt again. This is fairly spammy and pollutes your scrollback. Zsh on the other hand, will display possible completions below your prompt. When you find one you want, the list just disappears and you have an otherwise clean scrollback.

Like I said, its basically bash, so the small things are the difference.

Oh-my-zsh

Another fun thing about zsh is the oh-my-zsh project. Its a framework for zsh configs, including themeing and plugin support. I don't have much experience with the plugin system, which seems like a way to overly organize your customizations (as if this isn't one). The themeing support, though, is top notch. They have support for lots of colors and general nice looks, but also with git support and other things. Very neat.

How I customize it

This is a mix of my bash configs and my zsh configs. The zsh configs use almost the same syntax as bash. There were a few differences (I think setopt being one of them), but they were minor and quickly fixed with a bit of searching.

.bashrc

This is my central .bashrc, which loads a few handy libraries, and optionally some machine specific configurations I don't want in source control. As .bashrc is evaluated in interactive shells only, I have it start up a copy of tmux, so I'm never in a situation where I wished I had the session started after I've started some process. I also have it dump a fortune to the screen, which an old unix command for displaying pithy sayings and jokes as the login message.

  . ~/.shell/aliases
  . ~/.shell/functions
  . ~/.shell/variables
  . ~/.shell/host_specific
  [[ -s "$HOME/.bash_local" ]] && . ~/.bash_local
  
  if [ -e "`which tmux`" -a "$PS1" != "" -a "$TMUX" == "" -a "${SSH_TTY:-x}" != x ]; then
          sleep 1
          ( (tmux has-session -t remote && tmux attach-session -t remote) || (tmux new-session -s remote) ) && exit 0
          echo "tmux failed to start"
  fi
  
  # Run on new shell
  if [ `which fortune` ]; then
      echo ""
      fortune
      echo ""
  fi

.shell/aliases

I have a few traversal and directory navigation shortcuts that make my life easier. One of my favorites is .. as an alias for cd ...

# Filesystem
alias ..='cd ..'            # Go up one directory
alias ...='cd ../..'        # Go up two directories
alias ....='cd ../../..'    # And for good measure
alias ls='ls --color=auto'  # gimmie colors
alias l='ls -lah --color=auto'   # Long view, show hidden
alias la='ls -AF --color=auto'   # Compact view, show hidden
alias ll='ls -lFh --color=auto'  # Long view, no hidden

The default of hidden files not being around in OS X is both a blessing and a curse. It would make finding things more difficult if you rely on browsing, but at least you can see interesting files. As I don't always want it on, I have simple aliases to turn off showing these hidden files in Finder.app. These sort of OS X tweaks are amazingly difficult to remember.

# Mac Helpers
alias show_hidden="defaults write com.apple.Finder AppleShowAllFiles YES && killall Finder"
alias hide_hidden="defaults write com.apple.Finder AppleShowAllFiles NO && killall Finder"

I like to have a fancy looking emacs session in my terminal. According to this stack-overflow question, tmux suggests not altering the TERM in your shell init. Instead, we'll alias tmux to set the variable. The backslash here makes it so we don't risk looping on the alias.

alias tmux='TERM=xterm-256color \tmux'

These helpers are mostly defaults I want for these programs, but for whatever reason the commands themselves don't support rc files.

# Helpers
alias grep='grep --color=auto' # Always highlight grep search term
alias ping='ping -c 5'      # Pings with 5 packets, not unlimited
alias df='df -h'            # Disk free, in gigabytes, not bytes
alias du='du -h -c'         # Calculate total disk usage for a folder
alias sgi='sudo gem install' # Install ruby stuff
alias be='bundle exec'       # shortcut for ruby environment activation
alias dc='docker-compose'    # invoke docker-compose, which takes too long to type.
alias clj='clj-env-dir'      # Clojure helper
alias clr='clear;echo "Currently logged in on $(tty), as $(whoami) in directory $(pwd)."'
alias tt='tt++ $HOME/.ttconf'
alias svim="sudo vim" # Run vim as super user
alias emc="emacsclient -n" # no blocking terminal waiting for edit

Here we get to some of the more interesting aliases. servethis will spawn a simple HTTP server on port 8000 serving the current directory. Very helpful if you want to serve a few small static files.

pypath will print your python path, minus all the egg files littering it. pycclean will recursively clean out all of the pyc files littering your current directory.

I alias ssh to open a window to the my origin server, so I can pass files back and forth. From within an ssh connection, I can scp files to localhost:10999:~ and they'll be in my home directory on the host machine. Quite handy.

Its always a pain to remember to install nethack when you want to play a quick game (to say nothing of remembering to install telnet on recent ubuntus), so I just connect instead to a communal nethack server. This has the benefit of having a much more interesting bones file, for random goodies in the dungeon.

# Nifty extras
alias servethis="python -c 'import SimpleHTTPServer; SimpleHTTPServer.test()'"
alias pypath='python -c "import sys; print sys.path" | tr "," "\n" | grep -v "egg"'
alias pycclean='find . -name "*.pyc" -exec rm {} \; && find . -name "__pycache__" -exec rm -rf {} \;'
alias ssh='ssh -R 10999:localhost:22'
alias nethack='telnet nethack.alt.org'

I've been hit a few times with sites that block the curl user agent, so I have a pair of simple aliases which will masquerade as IE6 or Firefox to get around it.

# curl for useragents
alias iecurl="curl -H \"User-Agent: Mozilla/5.0 (Windows; U; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 2.0.50727)\""
alias ffcurl="curl -H \"User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.0.8) Gecko/2009032609 Firefox/3.0.0 (.NET CLR 3.5.30729)\""

These are more or less self explanitory aliases. The stand outs are gst which adds the -sb option to git status, making the output very small and nice to look at. Also, gho stands for github open and will open the current repository on github.

# GIT ALIASES
alias g=git
alias ga='git add'
alias gb='git branch'
alias gba='git branch -a'
alias gc='git commit -v'
alias gl='git pull'
alias gp='git push'
alias gst='git status -sb'
alias gsd='git svn dcommit'
alias gsr='git svn rebase'
alias gs='git stash'
alias gsa='git stash apply'
alias gr='git stash && git svn rebase && git svn dcommit && git stash pop' # git refresh
alias gd='git diff | $GIT_EDITOR -'
alias gmv='git mv'
alias gho='$(git remote -v 2> /dev/null | grep github | sed -e "s/.*git\:\/\/\([a-z]\.\)*/\1/" -e "s/\.git.*//g" -e "s/.*@\(.*\)$/\1/g" | tr ":" "/" | tr -d "\011" | sed -e "s/^/open http:\/\//g" | uniq)'

# HG ALIASES
alias hgst='hg status'
alias hgd='hg diff | $GIT_EDITOR -'
alias hgo='hg outgoing'

.shell/functions

One of the most interesting things about shell customization are functions. I have a mix of functions that actually do interesting things, and others which are effectively glorified aliases. The main reason in choosing a function over an alias is when you need to alter the order of arguments passed in.

The first function is a handy one. It will upload your public key to the .ssh/authorized_keys file, so you don't have to type in a password for that machine when attempting to SSH. (note: I've been told that this is built in to the command ssh-copy-id)

## Functions
add_auth_key () {
    ssh-copy-id $@
}

This is a handy little script I stole from somewhere which determines what type of archive you have (based on file extension) and executes the correct incantation to unarchive it. It doesn't support additional flags, however.

extract () {
    if [ -f $1 ] ; then
        case $1 in
            *.tar.bz2)        tar xjf $1        ;;
            *.tar.gz)         tar xzf $1        ;;
            *.bz2)            bunzip2 $1        ;;
            *.rar)            unrar x $1        ;;
            *.gz)             gunzip $1         ;;
            *.tar)            tar xf $1         ;;
            *.tbz2)           tar xjf $1        ;;
            *.tgz)            tar xzf $1        ;;
            *.zip)            unzip $1          ;;
            *.Z)              uncompress $1     ;;
            *.7z)             7zr e $1          ;;
            *)                echo "'$1' cannot be extracted via extract()" ;;
        esac
    else
        echo "'$1' is not a valid file"
    fi
}

dict is a small utility which I used when cheating at IRC games. The game was effectively "guess the word" using more or less binary search on a word space. Once you got it down to something like "Wah - Water" and you had to guess all the words in between there, it got really difficult. If no one could guess the right word, I'd do a search for something like dict ^wa and try those words which occurred between the two.

This is a perfect example of when you want to use a function instead of an alias. If this were an alias, we couldn't insert the term before the file name. The $@ syntax means "Take the arguments that were passed to this function and put them here."

dict() {
    grep "$@" /usr/share/dict/words
}

dls will list directories instead of files in the current working directory. dgrep will grep everything under the current directory and dfgrep does the same as dgrep save that it filters out to only have unique filenames. To complete the grep triad, I have psgrep which is similar to pgrep in that it is a process grep. Unlike pgrep, it shows the entire line of ps rather than just the PID.

dls () {
 # directory LS
 echo `ls -l | grep "^d" | awk '{ print $9 }' | tr -d "/"`
}
dgrep() {
    # A recursive, case-insensitive grep that excludes binary files
    grep -iR "$@" * | grep -v "Binary"
}
dfgrep() {
    # A recursive, case-insensitive grep that excludes binary files
    # and returns only unique filenames
    grep -iR "$@" * | grep -v "Binary" | sed 's/:/ /g' | awk '{ print $1 }' | sort | uniq
}
psgrep() {
    if [ ! -z $1 ] ; then
        echo "Grepping for processes matching $1..."
        ps aux | grep $1 | grep -v grep
    else
        echo "!! Need name to grep for"
    fi
}

When I used to run a local copy of postgres, it would occasionally get into a weird state where killing it was the only way to proceed. Unfortunately, there were 5-10 postgres processes and I could never remember which was the correct one to kill. This function will basically let you kill all processes that match a regex. Very handy for "postgres" or "java".

killit() {
    # Kills any process that matches a regexp passed to it
    ps aux | grep -v "grep" | grep "$@" | awk '{print $2}' | xargs sudo kill
}

If this computer doesn't have an implementation of tree, then let's make a simple one with find and sed. Tree basically outputs a directory layout in a tree form.

if [ -z "\${which tree}" ]; then
  tree () {
      find $@ -print | sed -e 's;[^/]*/;|____;g;s;____|; |;g'
  }
fi

# make a dir and cd into it
mcd () {
    mkdir -p "$@" && cd "$@"
}

If you need to kill a process on a particular port, but you don't know the process, portslay handles that.

portslay () {
    kill -9 `lsof -i tcp:$1 | tail -1 | awk '{ print $2;}'`
}

I download a bunch of github repos. I put them in $HOME/src/github.com/github_user/project_name. This makes that a bit easier.

ghget () {
    # input: rails/rails
    USER=$(echo $@ | tr "/" " " | awk '{print $1}')
    REPO=$(echo $@ | tr "/" " " | awk '{print $2}')
    mcd "$HOME/src/github.com/$USER" && \
    hub clone $@ && \
    cd $REPO
}

A few debugging tools for IP addresses. exip will list your external IP (as determined from myip.dk) and ips will list what your NIC things your IP addresses are.

exip () {
    # gather external ip address
    echo -n "Current External IP: "
    curl -s -m 5 http://myip.dk | grep "ha4" | sed -e 's/.*ha4">//g' -e 's/<\/span>.*//g'
}

ips () {
    # determine local IP address
    ifconfig | grep "inet " | awk '{ print $2 }'
}

The parse_git_branch and parse_svn_rev functions are used primarily for bash prompt use, so I can display interesting information whenever I'm in a directory that supports it.

parse_git_branch(){
    git branch 2> /dev/null | sed -e '/^[^*]/d' -e 's/* \(.*\)/[\1] /';
}

parse_svn_rev(){
    svn info 2> /dev/null | grep "Revision" | sed 's/Revision: \(.*\)/[r\1] /';
}

update_git_dirs() {
    # so what the below does is finds all files named .git in my home
    # directory, but excludes the .virtualenvs folder then strips the .git from
    # the end, cd's into the directory, pulls from the origin master, then
    # repeats

    OLD_DIR=`pwd`
    cd ~
    for i in `find . -type d -name ".virtualenvs" -prune -o -name ".git" | sed 's/\.git//'`; do
        echo "Going into $i"
        cd $i
        git pull origin master
        cd ~
    done
    cd $OLD_DIR
}

Its surprisingly hard to figure out what shell you're currently in, so the shell command will tell you. Note that the environment variable SHELL will tell you what you started in, but if you change it it doesn't update.

shell () {
  ps | grep `echo $$` | awk '{ print $4 }'
}

unegg and unpatch basically clean up crufty files. unegg will take a .egg file (which is actually a zip archive) and make it a directory. This will still be loadable by python. unpatch will clean up after some failed patches (for instance, when you get the wrong patch level when applying a diff) by recursing through the current directory removing any .orig or .rej files, as well as any directories named b.

unegg () {
    unzip $1 -d tmp
    rm $1
    mv tmp $1
}

unpatch () {
  find . -name "*.orig" -o -name "*.rej"  -type f -exec rm {} \;
  find . -name "b" -type d -exec rm -rf {} \;
}

So, I play counterstrike on my linux laptop. The gamma isn't set correctly for the game due to some issues in Steam v1. This script will update the gamma for the laptop to be at playable levels.

set_gamma () {
  xrandr --output eDP1 --gamma $1:$1:$1
}

cs_on() {
  set_gamma 1.7
}

cs_off()  {
  set_gamma 1.0
}

.shell/variables

This is more or less unexciting environment variables. Of interest, you can have a custom opener for less. The one I'm using below (from the source-highlight package in ubuntu) will syntax color anything it recognizes as highlightable. This is quite handy if you tend to open code things as I do.

I've found that naming the color escape codes something a bit more memorable has been a big help, especially when trying to build a nice looking prompt (though that effort is more or less gone out the window for me due to oh-my-zsh and eshell).

  export PATH=$HOME/.local/bin:$HOME/.rbenv/bin:$HOME/bin:$HOME/.gem/ruby/1.8/bin:/usr/local/git/bin:/Applications/Emacs.app/Contents/MacOS/bin:/usr/share/source-highlight:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/opt/X11/bin:/usr/local/git/bin:/Applications/Postgres.app/Contents/Versions/9.4/bin:/usr/games/:/usr/lib/go-1.6/bin
  export GDAL_DATA=/opt/local/share
  export MANPATH=/opt/local/share/man:$MANPATH
  export CLOJURE_EXT=$HOME/.clojure
  export P4CONFIG=$HOME/.p4config
  export P4EDITOR=$EDITOR
  export WORKON_HOME=$HOME/.virtualenvs
  export INFOPATH=$INFOPATH:/usr/share/info

  export GREP_COLOR='1;31'
  export LESS="-R"
  export LESSOPEN="| src-hilite-lesspipe.sh %s"
  export LESSHISTFILE=/dev/null
  export LESS_TERMCAP_mb=$'\E[01;32m'
  export LESS_TERMCAP_md=$'\E[01;32m'
  export LESS_TERMCAP_me=$'\E[0m'
  export LESS_TERMCAP_se=$'\E[0m'
  export LESS_TERMCAP_so=$'\E[01;44;33m'
  export LESS_TERMCAP_ue=$'\E[0m'
  export LESS_TERMCAP_us=$'\E[01;37m'
  export EDITOR='emacsclient'
  export OOO_FORCE_DESKTOP=gnome # For OpenOffice to look more gtk-friendly.
  export BROWSER=google-chrome

  export GOPATH=$HOME

  export HISTCONTROL=erasedups  # Ignore duplicate entries in history
  export HISTFILE=~/.histfile
  export HISTSIZE=10000         # Increases size of history
  export SAVEHIST=10000
  export HISTIGNORE="&:ls:ll:la:l.:pwd:exit:clear:clr:[bf]g"

  RED="\[\033[0;31m\]"
  PINK="\[\033[1;31m\]"
  YELLOW="\[\033[1;33m\]"
  GREEN="\[\033[0;32m\]"
  LT_GREEN="\[\033[1;32m\]"
  BLUE="\[\033[0;34m\]"
  WHITE="\[\033[1;37m\]"
  PURPLE="\[\033[1;35m\]"
  CYAN="\[\033[1;36m\]"
  BROWN="\[\033[0;33m\]"
  COLOR_NONE="\[\033[0m\]"

  SHOPT=`which shopt`
  if [ -z SHOPT ]; then
      shopt -s histappend        # Append history instead of overwriting
      shopt -s cdspell           # Correct minor spelling errors in cd command
      shopt -s dotglob           # includes dotfiles in pathname expansion
      shopt -s checkwinsize      # If window size changes, redraw contents
      shopt -s cmdhist           # Multiline commands are a single command in history.
      shopt -s extglob           # Allows basic regexps in bash.
  fi
  set ignoreeof on           # Typing EOF (CTRL+D) will not exit interactive sessions

  if [ -e "`which boot2docker`" ]; then
      $(boot2docker shellinit)
  fi

.shell/hostspecific

The only host specific configuration I have is to make my prompt super simple in the case where I'm using eterm (emacs terminal). This is mainly due to the fact that my emacs buffers tend to be rather narrow and having a large, information filled prompt makes actually using the terminal more difficult.

if [ $TERM = "eterm-color" ]; then
  # prompt for emacs (width sensitive)
  PS1='\u@\h:\w\$ '
fi

I also need to source the virtualenvwrapper stuff from ubuntu's installation location, so do that if we're on linux.

if [ `uname` = "Linux" ]; then
   # assuming ubuntu
   . /usr/share/virtualenvwrapper/virtualenvwrapper.sh 
fi

.shell/completions

Having additional shell completions is handy. Those go in their own file.

if which rbenv > /dev/null; then eval "$(rbenv init -)"; fi

.shell/prompt

BASEPATH=~/src/github.com/milkbikis/powerline-shell
if [ -f $BASEPATH/powerline-shell.py ]; then
    function powerline_precmd() {
        export PS1="$($BASEPATH/powerline-shell.py $? --shell zsh 2> /dev/null)"
    }

    function install_powerline_precmd() {
        for s in "${precmd_functions[@]}"; do
            if [ "$s" = "powerline_precmd" ]; then
                return
            fi
        done
        precmd_functions+=(powerline_precmd)
    }

    install_powerline_precmd
else
    echo "You should install powerline-shell from https://github.com/milkbikis/powerline-shell/"
fi

This isn't quite prompt related, but we hook into the prompt displaying to make this work. This snippet of code will auto-activate virtualenvs by looking for files named .venv and sourcing whatever virtualenv name is in there. You can read more about that here.

Thanks to GitHub users @scharron, @bsgreenb, @jessedhillon, @cjerdonek and @dorkitude for their comments on the original version.

# Support for bash
PROMPT_COMMAND='prompt'

# Mirrored support for zsh. See: https://superuser.com/questions/735660/whats-the-zsh-equivalent-of-bashs-prompt-command/735969#735969 
precmd() { eval "$PROMPT_COMMAND" }

function prompt()
{
    if [ "$PWD" != "$MYOLDPWD" ]; then
        MYOLDPWD="$PWD"
        test -e .venv && workon `cat .venv`
    fi
}

.zshrc

Very similar to my .bashrc, my .zshrc sets up a few simple variables, sources from a bunch of different locations, plays a fortune and lets me be on my way.

    # Automatic options added
    setopt appendhistory autocd nomatch autopushd pushdignoredups promptsubst
    unsetopt beep
    bindkey -e
    zstyle :compinstall filename '/home/jlilly/.zshrc'
    # end automatic options

    # Make prompt prettier
    autoload -U promptinit
    promptinit

    . ~/.shell/aliases
    . ~/.shell/completions
    . ~/.shell/functions
    . ~/.shell/prompt
    . ~/.shell/variables
    if [ -f /usr/local/bin/virtualenvwrapper.sh ]; then
      . /usr/local/bin/virtualenvwrapper.sh
    fi
    . ~/.shell/host_specific
    if [ -f ~/.bash_local ]; then
        . ~/.bash_local
    fi

    if [ -e "`which brew`" ]; then
        [[ -s $(brew --prefix)/etc/profile.d/autojump.sh ]] && . $(brew --prefix)/etc/profile.d/autojump.sh
    fi
    [[ -s /usr/share/autojump/autojump.zsh ]] && . /usr/share/autojump/autojump.zsh || \
      [[ -s /usr/share/autojump/autojump.sh ]] && . /usr/share/autojump/autojump.sh


    # Run on new shell
    have_fortune=`which fortune`
    if [ -e have_fortune ]; then
        echo ""
        fortune
        echo ""
    fi