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