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.

 1\set QUIET 1
 2\pset null '¤' -- Show NULL values as the ¤ character. Much easier to spot.
 3\pset linestyle unicode -- Nicer borders
 4\pset border 2 -- show table frame border
 5
 6-- Prompt line itself, like Bash's PS1 and PS2
 7\set PROMPT1 '%[%033[1m%][%n@%/] %R%# '
 8-- SELECT * FROM<enter>. %R shows what type of input it expects.
 9\set PROMPT2 '... %R %# '
10
11-- Always time things
12\timing
13
14-- Expanded table formatting mode = auto
15\x auto
16
17-- On error, don't auto-rollback when in interactive mode
18\set ON_ERROR_ROLLBACK interactive
19
20-- Be verbose
21\set VERBOSITY verbose
22
23-- Histfile seperate per database
24\set HISTFILE ~/.psql_history- :DBNAME
25\set HISTCONTROL ignoredups
26
27-- auto-completed keywords will be uppercased
28\set COMP_KEYWORD_CASE upper
29\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:

 1if command -v nvim >/dev/null; then
 2    alias vim=nvim
 3fi
 4if command -v pgcli >/dev/null; then
 5    alias pg=pgcli
 6fi
 7
 8alias v=vim # I'm really lazy
 9alias week="date +%GW%V" # e.g. 2020W42
10alias 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 00.path.all.sh defines two helper functions that the rest of the configuration will use to work with PATH:

 1path_prepend () {
 2    if ! [[ "$PATH" = *"$1"* ]]; then
 3        export PATH="$1:$PATH"
 4    fi
 5    return 0
 6}
 7path_append () {
 8    if ! [[ "$PATH" = *"$1"* ]]; then
 9        export PATH="$PATH:$1"
10    fi
11    return 0
12}

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

1if command -v python3 >/dev/null; then
2    path_append "$(python3 -c 'import site;print(site.USER_BASE)')/bin"
3fi
4if command -v python2 >/dev/null; then
5    path_append "$(python2 -c 'import site;print(site.USER_BASE)')/bin"
6fi

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

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

1if [ -x /usr/libexec/path_helper ]; then
2    eval `/usr/libexec/path_helper -s`
3fi
4if [[ -d /usr/local/opt/coreutils/libexec/gnubin ]]; then
5    path_prepend "/usr/local/opt/coreutils/libexec/gnubin"
6fi
7if [[ -d /usr/local/opt/coreutils/libexec/gnuman ]]; then
8    export MANPATH="/usr/local/opt/coreutils/libexec/gnuman:$MANPATH"
9fi

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 02.private.all.sh that simply source that if it exists:

1if [[ -f $HOME/.bash_private ]]; then
2    source $HOME/.bash_private
3fi

§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”

1if [[ -d $HOME/bin/ ]]; then
2    path_prepend "$HOME/bin"
3fi
4if [[ -d $HOME/.bin/ ]]; then
5    path_prepend "$HOME/.bin"
6fi
7if [[ -d $HOME/.local/bin/ ]]; then
8    path_prepend "$HOME/.local/bin"
9fi

§Sublime Text on MacOS

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

1if [ -d "/Applications/Sublime Text.app/Contents/SharedSupport/bin" ]; then
2  path_append "/Applications/Sublime Text.app/Contents/SharedSupport/bin"
3fi

§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:

1if [[ -d $HOME/.asdf ]]; then
2  . $HOME/.asdf/asdf.sh
3  . $HOME/.asdf/completions/asdf.bash
4fi

§AWS CLI

This enables bash completions for the aws CLI.

1if command -v aws_completer >/dev/null; then
2    complete -C aws_completer aws
3fi

§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.

1if [ -f /usr/share/bash-completion/bash_completion ]; then
2  . /usr/share/bash-completion/bash_completion
3fi

§Docker Completion

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

1if [ -f /Applications/Docker.app/Contents/Resources/etc/docker.bash-completion ]; then
2  . /Applications/Docker.app/Contents/Resources/etc/docker.bash-completion
3fi

§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.

1if command -v subl >/dev/null; then
2    export EDITOR="subl --wait"
3elif command -v vim >/dev/null; then
4    export EDITOR="vim"
5fi

§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

1if command -v erl >/dev/null || command -v iex >/dev/null; then
2    export ERL_AFLAGS="-kernel shell_history enabled"
3    ESCRIPTS_PATH="/Users/tbug/.mix/escripts"
4    if [[ -d "$ESCRIPTS_PATH" ]]; then
5      path_append "$ESCRIPTS_PATH"
6    fi
7fi

§Grep Aliases

I want my grep in color.

1if command -v grep >/dev/null; then
2    alias grep='grep --color=auto'
3    alias fgrep='fgrep --color=auto'
4    alias egrep='egrep --color=auto'
5fi

§Homebrew Bash Completions

This handles the bash completion project installed via brew.

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

§Fake a slow network with ipfw

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

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

§kubectl

My kubectl dotfile configuration is a bit complicated:

 1if command -v kubectl >/dev/null; then
 2    function k {
 3        local BLUE='\033[0;34m'
 4        local BOLD="\033[1m"
 5        local CLEAR='\033[0m'
 6        local context="$(awk '/^current-context:/{print $2}' $HOME/.kube/config)"
 7        printf "${BLUE}${BOLD}$context${CLEAR}\n" >&2
 8        kubectl "$@"
 9    }
10    if [[ -d $HOME/.kube ]] && [[ ! -f $HOME/.kube/completion.bash.inc ]]; then
11        kubectl completion bash > $HOME/.kube/completion.bash.inc
12    fi
13    # if the kubectl completion is not loaded, load it:
14    if ! command -v __start_kubectl >/dev/null; then
15        source $HOME/.kube/completion.bash.inc
16    fi
17    # make kubectl completions work for our short-name function `k` as well
18    if command -v __start_kubectl >/dev/null; then
19        complete -o default -F __start_kubectl k
20    fi
21    if command -v kubectx >/dev/null; then
22        alias kx="kubectx"
23        _kx_contexts () {
24            local curr_arg;
25            curr_arg=${COMP_WORDS[COMP_CWORD]};
26            COMPREPLY=($(compgen -W "- $(kubectl config get-contexts --output='name')" -- $curr_arg ))
27        }
28        complete -o default -F _kx_contexts kx
29    fi
30
31    # if krew plugin exists, add it to bin path
32    if [[ -d "${HOME}/.krew" ]]; then
33        path_append "${HOME}/.krew/bin"
34    fi
35
36    # if helm exists
37    if command -v helm >/dev/null; then
38        if [[ -d $HOME/.kube ]] && [[ ! -f $HOME/.kube/helm-completion.bash.inc ]]; then
39            helm completion bash > $HOME/.kube/helm-completion.bash.inc
40        fi
41        if ! command -v __start_helm >/dev/null; then
42            source $HOME/.kube/helm-completion.bash.inc
43        fi
44    fi
45fi

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.

 1# colorized man pages
 2man() {
 3    env \
 4        LESS_TERMCAP_md=$'\e[1;36m' \
 5        LESS_TERMCAP_me=$'\e[0m' \
 6        LESS_TERMCAP_se=$'\e[0m' \
 7        LESS_TERMCAP_so=$'\e[1;40;92m' \
 8        LESS_TERMCAP_ue=$'\e[0m' \
 9        LESS_TERMCAP_us=$'\e[1;32m' \
10            man "$@"
11}

§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:

 1if [[ -d $HOME/.nix-profile ]]; then
 2    source $HOME/.nix-profile/etc/profile.d/nix.sh
 3
 4    export NIX_SEARCH_CACHE="$HOME/.cache/nix-search-cache"
 5    nix-search () {
 6      if ! [[ -e "$NIX_SEARCH_CACHE" ]]; then
 7        echo "nix-search cache is empty, populating..." >&2
 8        nix-search --update
 9      fi
10      while (( "$#" )); do
11        case "$1" in
12          -h|--help)
13            echo "nix-search - cache and search in nix package names." >&2
14            echo "  search is done with 'grep -i'" >&2
15            echo "usage:" >&2
16            echo "  nix-search (-u|--update)  updates the nix-search cache" >&2
17            echo "  nix-search <grep args>    searches the nix-search cache" >&2
18            return 1
19            ;;
20          -u|--update)
21            echo "nix-search updating cache..." >&2
22            nix-env -qaP '*' > "$NIX_SEARCH_CACHE"
23            local ret=$?
24            echo "packages available for searching: $(cat "$NIX_SEARCH_CACHE" | wc -l)" >&2
25            return $ret
26            ;;
27          --) # end argument parsing
28            shift
29            break
30            ;;
31          -*|--*=) # unsupported flags
32            echo "Error: Unsupported flag $1" >&2
33            return 1
34            ;;
35          *) # preserve positional arguments
36            local params="$params $1"
37            shift
38            ;;
39        esac
40      done
41      if [[ "$(stat -f %c "$NIX_SEARCH_CACHE")" -lt "$(( $(date +%s) - 86400 ))" ]]; then
42        echo "nix-search cache needs updating, run nix-search -u" >&2
43      fi
44      grep -i "$params" "$NIX_SEARCH_CACHE"
45    }
46
47fi
48
49# will make bash-completion happy when installed via nix
50export XDG_DATA_DIRS="$HOME/.nix-profile/share:${XDG_DATA_DIRS:-/usr/local/share:/usr/share}"
51
52# bash completions from nix:
53if [[ -f $HOME/.nix-profile/share/bash-completion/bash_completion ]]; then
54    source $HOME/.nix-profile/share/bash-completion/bash_completion
55fi
56
57# git bash completions from git package
58if [[ -f $HOME/.nix-profile/share/git/contrib/completion/git-completion.bash ]]; then
59    source $HOME/.nix-profile/share/git/contrib/completion/git-completion.bash
60fi

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

§NodeJS

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

§NVM

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:

 1# care about nvm, only if .nvm folder exists
 2if [[ -d "$HOME/.nvm" ]]; then
 3    export NVM_DIR="$HOME/.nvm"
 4    nvm () {
 5        echo 'lazy loading nvm...' >&2
 6        unset -f nvm
 7        # this works on my arch machines with the nvm package installed
 8        if [[ -f "/usr/share/nvm/init-nvm.sh" ]]; then
 9            source /usr/share/nvm/init-nvm.sh
10        # this works on my mac with nvm installed through brew
11        elif [[ -f "/usr/local/opt/nvm/nvm.sh" ]]; then
12            source "/usr/local/opt/nvm/nvm.sh"
13        else
14          echo "nvm doesn't seen to be present." >&2
15          return 1
16        fi
17        nvm "$@"
18    }
19fi

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.

 1if command -v oc >/dev/null; then
 2  # lazy-load completions for oc
 3  __fake_oc_completer () {
 4    complete -r oc
 5    unset -f __fake_oc_completer
 6    source <(oc completion bash)
 7    __start_oc "$@"
 8  }
 9  complete -F __fake_oc_completer oc
10fi

§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.

1if command -v pspg >/dev/null; then
2    export PSQL_PAGER="pspg"
3fi

§Prompt

The file prompt.all.sh defines my bash prompt (PS1 and friends)

Here it is in full:

  1function __fix_stdout_nonblock_bug () {
  2    # some tool somewhere keeps messing up my stdout.
  3    # It's most likely a nodejs tool, at least I've seen nodejs do this multiple times,
  4    # however something somewhere in my pipeline does it incosistently but often
  5    # enough that it is so annoying that I want to make sure it doesn't happen.
  6    # Hence this hack.
  7    # This python snippet "fixes" the issue, unsetting NONBLOCK mode if it is already set.
  8    # Might be worth compiling a tiny C program to do it but this seems Good Enough for now.
  9    if command -v python >/dev/null; then
 10        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);'
 11    else
 12        return 1
 13    fi
 14}
 15
 16function __prompt_command () {
 17    local LASTEXIT="$?"
 18
 19    # Fix a super annoying bug I keep seeing but can't figure out how to correct.
 20    # some nodejs tool somewhere leaves stdout in nonblock mode which messes up
 21    # other tools. So fix it ON EACH PROMPT!
 22    __fix_stdout_nonblock_bug
 23
 24    local RESET="\[\033[0m\]" #reset
 25    local BOLD="\[\033[1m\]" #bold
 26    local DIM="\[\033[2m\]" #dim
 27    local UNDERLINE="\[\033[4m\]" #underline
 28
 29    local DEFAULT="\[\033[39m\]"
 30    local RED="\[\033[91m\]"
 31    local GREEN="\[\033[32m\]"
 32    local YELLOW="\[\033[93m\]"
 33    local BLUE="\[\033[34m\]"
 34    local MAGENTA="\[\033[95m\]"
 35    local CYAN="\[\033[96m\]"
 36    local WHITE="\[\033[97m\]"
 37    local GREY="\[\033[90m\]"
 38    # * == unstages
 39    # + == staged changes
 40    export GIT_PS1_SHOWDIRTYSTATE="1"
 41    # $ next to branch named if stashed state
 42    export GIT_PS1_SHOWSTASHSTATE="1"
 43    # % next to branch name of untracked files
 44    export GIT_PS1_SHOWUNTRACKEDFILES="1"
 45    # will show state compared to upstream
 46    # < you are behind upstream
 47    # > you are ahead of upstream
 48    # <> you have diverged from upstream
 49    # = matches upstream
 50    export GIT_PS1_SHOWUPSTREAM="auto"
 51
 52
 53    local r="$RESET"       # reset sequence
 54    local p="$BOLD$CYAN"  # primary color sequence
 55    local s="$DIM$CYAN"   # secondary color sequence
 56    local f="$GREY"        # framing color (usually grey)
 57    local e="$RED"         # error sequence
 58
 59    # exit status in dimmed parens and error color number
 60    if [ $LASTEXIT != 0 ]; then
 61        local status="$r$f($r$e${LASTEXIT}$r$f)$r "
 62    else
 63        local status="$r$f(0)$r "
 64    fi
 65
 66    # virtualenv support
 67    if [[ "$VIRTUAL_ENV" != "" ]]; then
 68        local venv="$r$f(venv:$s${VIRTUAL_ENV##*/}$r$f)$r"
 69    else
 70        local venv=""
 71    fi
 72
 73    if [[ "$AWS_PROFILE" != "" ]]; then
 74        local awsenv="$r$f(aws:$s${AWS_PROFILE}$r$f)$r"
 75    else
 76        local awsenv=""
 77    fi
 78
 79    local gitline=''
 80    if type -t __git_ps1 > /dev/null; then
 81        gitline="\$(__git_ps1 \" $r$f[$s%s$r$f]\")"
 82    fi
 83
 84    local k8sline=''
 85    if type -t kubectl > /dev/null; then
 86        k8sline=" $r$f[$s\$(kubectl config current-context)$f]"
 87    fi
 88    export PS1="${r}${f}╭─(\t) \u@\h $r$p\w$r${gitline}${k8sline}$r\n${f}╰─${status}$r${s}\$${venv}${awsenv}$r$s>$r "
 89    export PS2="${r}  ${status}${s}\$${venv}${awsenv}>${r} "
 90}
 91
 92if ! type -t __git_ps1 > /dev/null; then
 93    if [[ -f /usr/share/git/git-prompt.sh ]]; then
 94        . /usr/share/git/git-prompt.sh
 95    elif [[ -f $HOME/.nix-profile/share/git/contrib/completion/git-prompt.sh ]]; then
 96        . $HOME/.nix-profile/share/git/contrib/completion/git-prompt.sh
 97    fi
 98fi
 99
100export 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.

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

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.

1# Add cargo bin to path
2if [[ -d "$HOME/.cargo/bin" ]]; then
3    path_append "$HOME/.cargo/bin"
4fi

§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.

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

§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.

 1if command -v ssh >/dev/null; then
 2  smux () {
 3    if [[ "$#" -eq 0 ]]; then
 4      echo -e "SSH to <destination> and attach to a tmux session.\nAny argument is passed to ssh.\n\nusage: smux <destination> [...ssh opts]" >&2
 5      return 1
 6    fi
 7    ssh -t ${@} -- "tmux new-session -ADs0"
 8  }
 9  _smux() 
10  {
11      local cur prev opts
12      COMPREPLY=()
13      cur="${COMP_WORDS[COMP_CWORD]}"
14      prev="${COMP_WORDS[COMP_CWORD-1]}"
15      opts=$(
16        awk '/^host/ && $2 !~ /\*/ {print $2}' ~/.ssh/config &&
17        awk '!/\[/{split($1, a, ",");for(i in a){print a[i]}}' ~/.ssh/known_hosts | sort -u
18      )
19      COMPREPLY=( $(compgen -W "$opts" -- ${cur}) )
20      return 0
21  }
22  complete -F _smux smux
23fi

The meat of it is the line:

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

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

1tmux 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.