This is a walkthrough of my dotfiles repository.

This is mostly intended as a reference post I can use to point people to when they ask how I do a specific thing, but you might be able to find some fun little tweaks you didn’t know about.

A lot has happened since I started the repository to keep track of my dotfiles across machines in 2014. These days I’m using them mostly on MacOS, but I do have a small XPS 15 running Arch Linux that also see a bit of use.

We are skipping my tmux configuration as that is probably a post in itself.

psqlrc §

I have a custom psqlrc file that for when I use the psql tool.

\set QUIET 1
\pset null '¤' -- Show NULL values as the ¤ character. Much easier to spot.
\pset linestyle unicode -- Nicer borders
\pset border 2 -- show table frame border

-- Prompt line itself, like Bash's PS1 and PS2
\set PROMPT1 '%[%033[1m%][%n@%/] %R%# '
-- SELECT * FROM<enter>. %R shows what type of input it expects.
\set PROMPT2 '... %R %# '

-- Always time things

-- Expanded table formatting mode = auto
\x auto

-- On error, don't auto-rollback when in interactive mode
\set ON_ERROR_ROLLBACK interactive

-- Be verbose
\set VERBOSITY verbose

-- Histfile seperate per database
\set HISTFILE ~/.psql_history- :DBNAME
\set HISTCONTROL ignoredups

-- auto-completed keywords will be uppercased
\unset QUIET

Bash §

The meat of my dotfiles is the bashrc, which configures my bash that I use every day. I’ve tweaked this system over the last 5 years and this is where I’m at today.

Most of these I’ve probably grabbed from someone elses system and tweaked them as needed.

Architecture §

The bashrc file is designed to be either symlinked into place or sourced from your own bashrc script. On my current workstation I source it from ~/.bashrc to mix it with some local configuration.

The bashrc file sets up the library that I use to work with my dotfiles, and the rest of the configuration is stored in a bashrc.d/ directory next to the bashrc script.

The library consists of 4 bash functions:

  • dotfiles_directory
  • dotfiles_platform
  • dotfiles_match
  • dotfiles_source

dotfiles_directory outputs the directory that the bashrc file is located in. This works across symlinks, which is why it is a bit complicated.

dotfiles_platform outputs the current “platform” as a normalized string. I use this for configuration that is only relevant on certain platforms.

dotfiles_match outputs all configuration files that matches a pattern, or every relevant file if not pattern is given. It uses filenames from the configuration directory to check this. Valid filenames match <name>.<platform | all>.sh. The files are returned in sorted order by name.

dotfiles_source Takes an optional pattern just line the above function, and sources files that match. If the debug environment variable DOTFILES_DEBUG is set to true then it will do so while timing the source. If a source command fails it will write an error about it to STDERR.

Finally, the bashrc file calls dotfiles_source with no pattern, which will source all configuration files relevant for the current platform.

Dotfiles §

If the order of sourcing is important, the files are named <nn>.<name>.<platform>.sh which will source those files first.

Files that most be sourced last are prefixed with zz. I currently don’t have any of those.

Aliases §

The first file is my alias file. I don’t tend to use aliases a lot but I have a few:

if command -v nvim >/dev/null; then
    alias vim=nvim
if command -v pgcli >/dev/null; then
    alias pg=pgcli

alias v=vim # I'm really lazy
alias week="date +%GW%V" # e.g. 2020W42
alias cal="cal -Nw3sDK" # I prefer this

The command -v pgcli >/dev/null; checks if pgcli exists. I’m doing this a lot in my dotfiles to only perform configuration if some command is available on $PATH.

Path §

I’ve tinkered with PATH so much that I’ve made a simply utility to work with them. The file defines two helper functions that the rest of the configuration will use to work with PATH:

path_prepend () {
    if ! [[ "$PATH" = *"$1"* ]]; then
        export PATH="$1:$PATH"
    return 0
path_append () {
    if ! [[ "$PATH" = *"$1"* ]]; then
        export PATH="$PATH:$1"
    return 0

It also defines my python paths. I should have moved these to my python config but I haven’t yet :)

if command -v python3 >/dev/null; then
    path_append "$(python3 -c 'import site;print(site.USER_BASE)')/bin"
if command -v python2 >/dev/null; then
    path_append "$(python2 -c 'import site;print(site.USER_BASE)')/bin"

I normally use a single python2 and python3 on my system so this works fine.

I also have a Darwin specific path configuration at This is our first platform specific file:

if [ -x /usr/libexec/path_helper ]; then
    eval `/usr/libexec/path_helper -s`
if [[ -d /usr/local/opt/coreutils/libexec/gnubin ]]; then
    path_prepend "/usr/local/opt/coreutils/libexec/gnubin"
if [[ -d /usr/local/opt/coreutils/libexec/gnuman ]]; then
    export MANPATH="/usr/local/opt/coreutils/libexec/gnuman:$MANPATH"

I have Coreutils installed through brew, and this handles the path and manual paths for me.

Private Configuration §

On most platforms I have secret configuration (passwords, tokens and such) that I don’t want to commit to my public dotfile repo. I’ve made a habbit of placing such secrets in a ~/.bash_private file, and have a dotfile that simply source that if it exists:

if [[ -f $HOME/.bash_private ]]; then
    source $HOME/.bash_private

HOME paths §

Because different tools can’t agree on where to put there executables in my home folder, I have this catch-all kind of “just prepend bin/ to PATH please”

if [[ -d $HOME/bin/ ]]; then
    path_prepend "$HOME/bin"
if [[ -d $HOME/.bin/ ]]; then
    path_prepend "$HOME/.bin"
if [[ -d $HOME/.local/bin/ ]]; then
    path_prepend "$HOME/.local/bin"

Sublime Text on MacOS §

If sublime is installed, add it’s bin/ to the path.

if [ -d "/Applications/Sublime" ]; then
  path_append "/Applications/Sublime"

asdf version manager §

I use asdf for a lot of my tool version management. Currently I use it for erlang, elixir, nodejs, ruby.

The dotfile itself is simple: the tool itself and bash completions for it:

if [[ -d $HOME/.asdf ]]; then
  . $HOME/.asdf/
  . $HOME/.asdf/completions/asdf.bash


This enables bash completions for the aws CLI.

if command -v aws_completer >/dev/null; then
    complete -C aws_completer aws

Bash Completion §

This sources the bash-completion project if it is installed.

Note that brew installs these in it’s prefix, so this won’t work if you’ve installed it via brew. I have a section further down for that.

if [ -f /usr/share/bash-completion/bash_completion ]; then
  . /usr/share/bash-completion/bash_completion

Docker Completion §

This enables bash completion for the docker command when installed via the Docker for Mac project.

if [ -f /Applications/ ]; then
  . /Applications/

EDITOR environment variable §

This sets up the EDITOR environment variable such that if the subl command exists, we’ll use that, otherwise if vim exists, we’ll use that.

I love Sublime Text and it has been my primary editor for most of my professional career, but in a pinch I’m fine with vim as well.

if command -v subl >/dev/null; then
    export EDITOR="subl --wait"
elif command -v vim >/dev/null; then
    export EDITOR="vim"

Erlang and Elixir §

Some versions of erlang requires a kernel flag to be set to enable the shell history. This is an absolute pain to remember but luckily you can set those in an environment variable as well, so this does that. It also adds the ~/.mix/escripts path to my PATH

if command -v erl >/dev/null || command -v iex >/dev/null; then
    export ERL_AFLAGS="-kernel shell_history enabled"
    if [[ -d "$ESCRIPTS_PATH" ]]; then
      path_append "$ESCRIPTS_PATH"

Grep Aliases §

I want my grep in color.

if command -v grep >/dev/null; then
    alias grep='grep --color=auto'
    alias fgrep='fgrep --color=auto'
    alias egrep='egrep --color=auto'

Homebrew Bash Completions §

This handles the bash completion project installed via brew.

if command -v brew > /dev/null && [ -f $(brew --prefix)/etc/bash_completion ]; then
    . $(brew --prefix)/etc/bash_completion
elif command -v brew > /dev/null && [ -f $(brew --prefix)/share/bash-completion/bash_completion ]; then
    . $(brew --prefix)/share/bash-completion/bash_completion

Fake a slow network with ipfw §

It’s been years since I’ve used this, but I remember it being really useful.

if command -v ipfw >/dev/null; then
    #fake slow network
    alias slowNetwork='sudo ipfw pipe 1 config bw 350kbit/s plr 0.05 delay 500ms && sudo ipfw add pipe 1 dst-port http'
    alias flushNetwork='sudo ipfw flush'

kubectl §

My kubectl dotfile configuration is a bit complicated:

if command -v kubectl >/dev/null; then
    function k {
        local BLUE='\033[0;34m'
        local BOLD="\033[1m"
        local CLEAR='\033[0m'
        local context="$(awk '/^current-context:/{print $2}' $HOME/.kube/config)"
        printf "${BLUE}${BOLD}$context${CLEAR}\n" >&2
        kubectl "$@"
    if [[ -d $HOME/.kube ]] && [[ ! -f $HOME/.kube/ ]]; then
        kubectl completion bash > $HOME/.kube/
    # if the kubectl completion is not loaded, load it:
    if ! command -v __start_kubectl >/dev/null; then
        source $HOME/.kube/
    # make kubectl completions work for our short-name function `k` as well
    if command -v __start_kubectl >/dev/null; then
        complete -o default -F __start_kubectl k
    if command -v kubectx >/dev/null; then
        alias kx="kubectx"
        _kx_contexts () {
            local curr_arg;
            COMPREPLY=($(compgen -W "- $(kubectl config get-contexts --output='name')" -- $curr_arg ))
        complete -o default -F _kx_contexts kx

    # if krew plugin exists, add it to bin path
    if [[ -d "${HOME}/.krew" ]]; then
        path_append "${HOME}/.krew/bin"

    # if helm exists
    if command -v helm >/dev/null; then
        if [[ -d $HOME/.kube ]] && [[ ! -f $HOME/.kube/ ]]; then
            helm completion bash > $HOME/.kube/
        if ! command -v __start_helm >/dev/null; then
            source $HOME/.kube/

First, I have a bash function k that wraps kubectl. It’s purpose is both as an alias so that I can just type k for kubectl, but it also prints what context I’m currently using so I might notice that I’m running something in the wrong environment.

There is also some bash completions in there, both for kubectl itself, but also for the k function so I get completions for that as well.

Then I have a kubectx alias kx and a completion for it as well.

Then a bit for putting krew’s bin/ onto the PATH.

And finally some helm stuff that handles loading bash completions for helm.

Manual pages §

Adding some color to my man pages.

# colorized man pages
man() {
    env \
        LESS_TERMCAP_md=$'\e[1;36m' \
        LESS_TERMCAP_me=$'\e[0m' \
        LESS_TERMCAP_se=$'\e[0m' \
        LESS_TERMCAP_so=$'\e[1;40;92m' \
        LESS_TERMCAP_ue=$'\e[0m' \
        LESS_TERMCAP_us=$'\e[1;32m' \
            man "$@"

Nix Hack §

I’ve been toying a bit with Nix on-off, and I had issues with searching for packages. So I built a caching search function. Better tools exist to do this, and I don’t recommend stealing this, but for reference:

if [[ -d $HOME/.nix-profile ]]; then
    source $HOME/.nix-profile/etc/profile.d/

    export NIX_SEARCH_CACHE="$HOME/.cache/nix-search-cache"
    nix-search () {
      if ! [[ -e "$NIX_SEARCH_CACHE" ]]; then
        echo "nix-search cache is empty, populating..." >&2
        nix-search --update
      while (( "$#" )); do
        case "$1" in
            echo "nix-search - cache and search in nix package names." >&2
            echo "  search is done with 'grep -i'" >&2
            echo "usage:" >&2
            echo "  nix-search (-u|--update)  updates the nix-search cache" >&2
            echo "  nix-search <grep args>    searches the nix-search cache" >&2
            return 1
            echo "nix-search updating cache..." >&2
            nix-env -qaP '*' > "$NIX_SEARCH_CACHE"
            local ret=$?
            echo "packages available for searching: $(cat "$NIX_SEARCH_CACHE" | wc -l)" >&2
            return $ret
          --) # end argument parsing
          -*|--*=) # unsupported flags
            echo "Error: Unsupported flag $1" >&2
            return 1
          *) # preserve positional arguments
            local params="$params $1"
      if [[ "$(stat -f %c "$NIX_SEARCH_CACHE")" -lt "$(( $(date +%s) - 86400 ))" ]]; then
        echo "nix-search cache needs updating, run nix-search -u" >&2
      grep -i "$params" "$NIX_SEARCH_CACHE"


# will make bash-completion happy when installed via nix
export XDG_DATA_DIRS="$HOME/.nix-profile/share:${XDG_DATA_DIRS:-/usr/local/share:/usr/share}"

# bash completions from nix:
if [[ -f $HOME/.nix-profile/share/bash-completion/bash_completion ]]; then
    source $HOME/.nix-profile/share/bash-completion/bash_completion

# git bash completions from git package
if [[ -f $HOME/.nix-profile/share/git/contrib/completion/git-completion.bash ]]; then
    source $HOME/.nix-profile/share/git/contrib/completion/git-completion.bash

At the end there is some loading of completions when packages are installed via nix.

NodeJS §

if [[ -d "$HOME/.npm/global/bin" ]]; then
  path_append "$HOME/.npm/global/bin"


I don’t really use nvm anymore since I switched to asdf, but this is a good example of some of the lazy-loading of things I’ve done to speed up booting my bash shell:

# care about nvm, only if .nvm folder exists
if [[ -d "$HOME/.nvm" ]]; then
    export NVM_DIR="$HOME/.nvm"
    nvm () {
        echo 'lazy loading nvm...' >&2
        unset -f nvm
        # this works on my arch machines with the nvm package installed
        if [[ -f "/usr/share/nvm/" ]]; then
            source /usr/share/nvm/
        # this works on my mac with nvm installed through brew
        elif [[ -f "/usr/local/opt/nvm/" ]]; then
            source "/usr/local/opt/nvm/"
          echo "nvm doesn't seen to be present." >&2
          return 1
        nvm "$@"

This defines a function nvm that - when invoked - will unset itself and look for nvm in the different places I have it across my machines and source it in.

The reason for doing this is that nvm was painfully slow to load, and usually I don’t need it, so paying an extra second in bash loading time is not worth it.

OpenShift Completion §

Almost same story as with NVM. I don’t use openshift anymore (I had a brief encouter at work)

What I did here is wrapping the completion in a lazy loader. The reason is - again - that calling oc completion bash was a bit slow and I rarely needed it.

This might be useful to you if you want to lazy-load your own bash completions.

if command -v oc >/dev/null; then
  # lazy-load completions for oc
  __fake_oc_completer () {
    complete -r oc
    unset -f __fake_oc_completer
    source <(oc completion bash)
    __start_oc "$@"
  complete -F __fake_oc_completer oc

Pager §

This file used to be bigger, but currently it contains some pager configuration for psql if pspg is installed.

Side-note: if you don’t know pspg you should give it a look. It’s a really good pager for tabular content.

if command -v pspg >/dev/null; then
    export PSQL_PAGER="pspg"

Prompt §

The file defines my bash prompt (PS1 and friends)

Here it is in full:

function __fix_stdout_nonblock_bug () {
    # some tool somewhere keeps messing up my stdout.
    # It's most likely a nodejs tool, at least I've seen nodejs do this multiple times,
    # however something somewhere in my pipeline does it incosistently but often
    # enough that it is so annoying that I want to make sure it doesn't happen.
    # Hence this hack.
    # This python snippet "fixes" the issue, unsetting NONBLOCK mode if it is already set.
    # Might be worth compiling a tiny C program to do it but this seems Good Enough for now.
    if command -v python >/dev/null; then
        python -c 'import os,sys,fcntl; flags = fcntl.fcntl(sys.stdout, fcntl.F_GETFL); fcntl.fcntl(sys.stdout, fcntl.F_SETFL, flags&~os.O_NONBLOCK);'
        return 1

function __prompt_command () {
    local LASTEXIT="$?"

    # Fix a super annoying bug I keep seeing but can't figure out how to correct.
    # some nodejs tool somewhere leaves stdout in nonblock mode which messes up
    # other tools. So fix it ON EACH PROMPT!

    local RESET="\[\033[0m\]" #reset
    local BOLD="\[\033[1m\]" #bold
    local DIM="\[\033[2m\]" #dim
    local UNDERLINE="\[\033[4m\]" #underline

    local DEFAULT="\[\033[39m\]"
    local RED="\[\033[91m\]"
    local GREEN="\[\033[32m\]"
    local YELLOW="\[\033[93m\]"
    local BLUE="\[\033[34m\]"
    local MAGENTA="\[\033[95m\]"
    local CYAN="\[\033[96m\]"
    local WHITE="\[\033[97m\]"
    local GREY="\[\033[90m\]"
    # * == unstages
    # + == staged changes
    # $ next to branch named if stashed state
    # % next to branch name of untracked files
    # will show state compared to upstream
    # < you are behind upstream
    # > you are ahead of upstream
    # <> you have diverged from upstream
    # = matches upstream
    export GIT_PS1_SHOWUPSTREAM="auto"

    local r="$RESET"       # reset sequence
    local p="$BOLD$CYAN"  # primary color sequence
    local s="$DIM$CYAN"   # secondary color sequence
    local f="$GREY"        # framing color (usually grey)
    local e="$RED"         # error sequence

    # exit status in dimmed parens and error color number
    if [ $LASTEXIT != 0 ]; then
        local status="$r$f($r$e${LASTEXIT}$r$f)$r "
        local status="$r$f(0)$r "

    # virtualenv support
    if [[ "$VIRTUAL_ENV" != "" ]]; then
        local venv="$r$f(venv:$s${VIRTUAL_ENV##*/}$r$f)$r"
        local venv=""

    if [[ "$AWS_PROFILE" != "" ]]; then
        local awsenv="$r$f(aws:$s${AWS_PROFILE}$r$f)$r"
        local awsenv=""

    local gitline=''
    if type -t __git_ps1 > /dev/null; then
        gitline="\$(__git_ps1 \" $r$f[$s%s$r$f]\")"

    local k8sline=''
    if type -t kubectl > /dev/null; then
        k8sline=" $r$f[$s\$(kubectl config current-context)$f]"
    export PS1="${r}${f}╭─(\t) \u@\h $r$p\w$r${gitline}${k8sline}$r\n${f}╰─${status}$r${s}\$${venv}${awsenv}$r$s>$r "
    export PS2="${r}  ${status}${s}\$${venv}${awsenv}>${r} "

if ! type -t __git_ps1 > /dev/null; then
    if [[ -f /usr/share/git/ ]]; then
        . /usr/share/git/
    elif [[ -f $HOME/.nix-profile/share/git/contrib/completion/ ]]; then
        . $HOME/.nix-profile/share/git/contrib/completion/

export PROMPT_COMMAND=__prompt_command  # Func to gen PS1 after CMDs

This file contains all my prompt related things. Parts of this file dates all the way back to the start of this repo, 2014.

This is how it looks when rendered:

╭─(08:25:01) het@hetmbp ~/src/dotfiles [master=] [<k8s-context>]
╰─(0) $>

But in color, of course.

The prompt is a two-line prompt. The top information line shows

  • the time
  • current working directory
  • user and host
  • git branch and status if applicable
  • currently selected Kubernetes context

And the second line shows

  • exit status of last command
  • current python virtualenv if applicable
  • currently selected AWS profile if applicable
  • the prompt.

I’ve used this two-line prompt format since 2018 and I’m still pretty happy with it.

Ruby §

Some Ruby hack that I don’t use anymore since switching completely to asdf for Ruby version management.

if ! command -v asdf >/dev/null; then
  # when asdf is installed don't do this ruby stuff because asdf will
  # most likely be managing this.
  if command -v ruby >/dev/null && command -v gem >/dev/null; then
      if [[ -f /tmp/ruby_gem_home ]]; then
          export GEM_HOME="$(cat /tmp/ruby_gem_home)"
          # move GEM_HOME into home dir. Global install is messy.
          export GEM_HOME="$(ruby -r rubygems -e 'puts Gem.user_dir')"
          echo "$GEM_HOME" > /tmp/ruby_gem_home
      # prepend, because osx has some native ruby stuff that we cant really touch
      path_prepend "$GEM_HOME/bin"

This checks if ruby and gem is present, and if so, defines GEM_HOME. I also cache the GEM_HOME because invoking Ruby to get the Gem.user_dir is painfully slow.

I also prepend the bin/ folder in the GEM_HOME to PATH so that it’s picked first.

Rust Cargo Path §

I’ve toyed a bit with rust, and also have utilities installed via cargo. This simply appends the global cargo bin/ to my PATH.

# Add cargo bin to path
if [[ -d "$HOME/.cargo/bin" ]]; then
    path_append "$HOME/.cargo/bin"

Scaleway CLI Bash Completion §

At work we have some infrastructure running at Scaleway. They have a decent CLI that I use, and this defines auto-completion for it.

if command -v scw >/dev/null; then
  _scw() {
    _get_comp_words_by_ref -n = cword words
    output=$(scw autocomplete complete bash -- "$COMP_LINE" "$cword" "${words[@]}")
    # apply compopt option and ignore failure for older bash versions
    [[ $COMPREPLY == *= ]] && compopt -o nospace 2> /dev/null || true
  complete -F _scw scw

smux: ssh + tmux §

I have a dedicated bash function that I’ve named smux for doing ssh+tmux. It’s a shortcut for running ssh and then tmux once you get a session, which is something I do many times in a day.

I’ve used the default session name of 0 so that if I for some reason would ssh in, and then run tmux attach it would still attach to the expected session.

I also wrote a completion function for it, which will use hosts defined in ~/.ssh/config and hosts found in ~/.ssh/known_hosts as completions.

if command -v ssh >/dev/null; then
  smux () {
    if [[ "$#" -eq 0 ]]; then
      echo -e "SSH to <destination> and attach to a tmux session.\nAny argument is passed to ssh.\n\nusage: smux <destination> [...ssh opts]" >&2
      return 1
    ssh -t ${@} -- "tmux new-session -ADs0"
      local cur prev opts
        awk '/^host/ && $2 !~ /\*/ {print $2}' ~/.ssh/config &&
        awk '!/\[/{split($1, a, ",");for(i in a){print a[i]}}' ~/.ssh/known_hosts | sort -u
      COMPREPLY=( $(compgen -W "$opts" -- ${cur}) )
      return 0
  complete -F _smux smux

The meat of it is the line:

ssh -t ${@} -- "tmux new-session -ADs0"

Which will force a TTY and pass any arguments you gave smux along to ssh. It will then run

tmux new-session -ADs0

On the remote host.

The tmux flags given to new-session are:

  • -A makes new-session behave like attach-session if session-name already exists
  • -D -D behaves like -d to attach-session
  • -s0 specifies a session name. Here 0 is given as the session name, which is also the default value.

Final Thoughts §

I’ve left out a few things that hasn’t seen use in a while and probably don’t even work today.