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 -y zsh{,-doc} git virtualenvwrapper jq source-highlight python3-pip cargo snapd lsd bat dust duf dog gping
sudo snap install hub --classic

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.

pip3 install --user powerline-shell

cargo install bat # alternative to cat
cargo install yq  # yaml version of jq


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.

. ~/.profile

  if [ -e "`which tmux 2>/dev/null`" -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 2>/dev/null` ]; 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='lsd'      # nicer output
alias l='ls -lah'   # Long view, show hidden
alias la='ls -AF'   # Compact view, show hidden
alias ll='ls -lFh'  # 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='gping'             # Pings with 5 packets, not unlimited
alias df='duf'                 # Disk free, but a nicer version
alias du='dust'                # Calculate total disk usage for a folder
alias dig='dog'                # It's a nicer version of dig.
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 k='kubectl'              # shorthand for working with kubernetes
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
alias cat="bat" # nicer alternative

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         ;;
            *.tar.xz)         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 2>/dev/null}" ]; 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
}

wmget () {
    # input: rails/rails
    USER=$(echo $@ | tr "/" " " | awk '{print $1}')
    REPO=$(echo $@ | tr "/" " " | awk '{print $2}')

    mcd "$HOME/src/walmart/$USER" && \
    GITHUB_HOST=gecgithub01.walmart.com hub clone -p $@ && \
    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
}

Adding things to .gitignore more easily. Can only be run from root of repo.

gitignore() {
  if [ -e .gitignore ];then
    echo "$@" >> .gitignore
  else
    echo ".gitignore doesn't exist here"
    return 1
  fi
}

I need to remember the fibonacci series for story estimating. Instead of calculating it, just print them out.

fib() {
  echo "0, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144"
}

I run often my terminal emulator in emacs, using libvterm. This function allows it to setup directory tracking, among other things.

function vterm_printf(){
    if [ -n "$TMUX" ]; then
        # Tell tmux to pass the escape sequences through
        # (Source: http://permalink.gmane.org/gmane.comp.terminal-emulators.tmux.user/1324)
        printf "\ePtmux;\e\e]%s\007\e\\" "$1"
    elif [ "${TERM%%-*}" = "screen" ]; then
        # GNU screen (screen, screen-256color, screen-256color-bce)
        printf "\eP\e]%s\007\e\\" "$1"
    else
        printf "\e]%s\e\\" "$1"
    fi
}

.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/bin:$HOME/.local/bin:node_modules/.bin:$HOME/.cabal/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:/usr/local/go/bin:$HOME/.cargo/bin:/snap/bin:$PATH
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 XDG_DATA_HOME=$HOME/.local/share/

export GPG_TTY=$(tty) # without this, I get complaints from gpg w/r/t pinentry.
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=firefox

export GOPATH=$HOME

export HISTCONTROL=erasedups  # Ignore duplicate entries in history
export HISTFILE=~/.histfile
export HISTTIMEFORMAT="%m/%d/%y %T"
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 2>/dev/null`
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 2>/dev/null`" ]; then
    $(boot2docker shellinit)
fi

if [ -e "`which luarocks 2>/dev/null`" ]; then
    eval `luarocks path`
fi

.shell/hostspecific

I have a hostspecific file here for the case where I need something in my config that I don't want checked in. These are usually not common across machine to machine. As such, we don't create it here so it won't tangle improperly.

.shell/completions

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

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

# Kubernetes
if which kubectl >/dev/null; then source <(kubectl completion zsh); fi

if [ -e "`which autojump >/dev/null`" ]; then
. $(nix-store --query `which autojump`)/share/autojump/autojump.`basename $SHELL`
fi

.shell/prompt

We special case term types here in order to make emacs's tramp mode (which has a $TERM of "dumb") not have to evaluate prompt craziness.

case "$TERM" in
    "dumb")
        export PS1="> "
        ;;
esac

I like 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 find myself writing more and more nodejs these days. To make that easier, I use nvm which allows moving between different versions of node/npm. This is the setup for it.

export NVM_DIR="$HOME/.nvm"
if [ -d $NVM_DIR ]; then
  [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"  # This loads nvm
  [ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"  # This loads nvm bash_completion
fi

To setup directory and prompt integration with vterm, some small additions to $PROMPT need to happen.

vterm_prompt_end() {
    vterm_printf "51;A$(whoami)@$(hostname):$(pwd)";
}
setopt PROMPT_SUBST
PROMPT=$PROMPT'%{$(vterm_prompt_end)%}'

.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 inc_append_history autocd nomatch autopushd pushdignoredups promptsubst hist_ignore_dups
unsetopt beep
bindkey -e  # setup emacs-like key bindings.
autoload -Uz compinit && compinit # setup completion

zstyle :compinstall filename '/home/jlilly/.zshrc'
# end automatic options

# Setup oh-my-zsh
OMZSH_FILE='~/.oh-my-zsh/zshrc.abrahms'
if [ -f $OMZSH_FILE ]; then
    . $OMZSH_FILE
fi

# Make prompt prettier
autoload -U promptinit
promptinit

. ~/.profile
. ~/.shell/prompt

if [ -f /usr/local/bin/virtualenvwrapper.sh ]; then
    . /usr/local/bin/virtualenvwrapper.sh
fi
if [ -f ~/.bash_local ]; then
    . ~/.bash_local
fi

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

NIX_FILE="~/.nix-profile/etc/profile.d/nix.sh"
if [ -e $NIX_FILE ]; then
    . $NIX_FILE
fi

Variables should be loaded for non-interactive shells too, so we stick that in .profile

. ~/.shell/aliases
. ~/.shell/completions
. ~/.shell/functions
. ~/.shell/variables
[ -f ~/.shell/host_specific ] &&   . ~/.shell/host_specific
© 2012 - 2023 · Home — Theme Simpleness