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. ;)
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 [ `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 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"
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 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 {} \;' 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")' # 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 ;; *) 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 mcd () { mkdir "$@" && cd "$@" }
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 {} \; }
.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/.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 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_OPTIONS='--color=auto' 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 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
.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
.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 . ~/bin/virtualenvwrapper.sh . ~/.shell/host_specific if [ -f ~/.bash_local ]; then . ~/.bash_local fi # Run on new shell have_fortune=`which fortune` if [ -e have_fortune ]; then echo "" fortune echo "" fi