Manage shell configurations across different systems

18 minute read

Diggin’ Dotfiles Collection

It’s time to dig the dotfiles, and to dig in to them! This post is a part of a blog post collection called Diggin’ Dotfiles where I unearth some gems from my personal dotfiles repo erikw/dotfiles.

Dynamic Github repo image with stats

The following articles are a part of this series:

Managing shell configurations across different environments

This is the first post in this blog collection and will tackle the problem of using your dotfiles across multiple systems that have different environments and even operating systems!

Why do that?

First of, why would you want to do this? Every environment and OS works differently and you would need to tailor it accordingly? Yes this is the case, however there’s also a great benefit on streamlining common configuration so that there is a “standard base” that works like expected. For the fast terminal typist, this will be much appreciated that the fingers can find their way in whatever shell is dropped in front of them.

My first (preserved & squashed) commit in my personal dotfiles repo was in 2012 and I don’t really remember how I manged them before this, maybe in a different git repo or some automated backup solution. Since, I’ve been working across many different systems for private usage, projects and professionally on systems like Arch Linux Probably the best Linux distribution you can get.
, macOS, FreeBSD, Windows, Ubuntu, *nix-whatever on stationary, laptops and remote/virtual machines and all of them had a working setup of my dotfiles.

How do that?

How did I set this up? I wanted to not explode the scope of the setup to automate 100% every configuration step but rather make the setup flexible, configurable, light-weight and composition based on needs, while still making sure that the common tools like the shell works out of the box. Thus it never made sense for me to have a super-install-all-for-everything-on-anywhere.sh. I might not have the luxury of jumping on to the system from scratch; maybe the system has been setup by an IT organization at a workplace for example, and the dotfiles must be integrated to this given setup.

I make sure that the README of my dotfiles is always up to date with the manual steps to bootstrap the dotfiles. For some environments I’ve taken the time to automate these e.g macos_config.sh, macos_install.sh or windows_install.ps1, but the README must contain the steps needed to get the base working.

Managing dependencies - use only what’s available

The actual installation of the dotfiles uses justone/dotfiles.git which is a cleaver script that just symlinks from the git repo to ~/. The guiding principle I work with is that after symlinking in all files and starting a new shell, there must be no warning or complains when spawning a shell. Everything must work out of the box with a minimal setup. Thus, only include a file or set up a feature if this feature is available. Then depending on my needs (or time available), I can extend the setup by adding for example dependencies. Here’s an example from my .bashrc:

# Enable bash bookmarks
if [ -e $HOME/.local/bin/bashmarks.sh ]; then
	source $HOME/.local/bin/bashmarks.sh
fi

Simply enable the handy bookmarks tool huyng/bashmarks) if it is installed/available on this system. It might not be necessary to spend time on installing this feature on a remote system that I rarely access, or I might be concerned about the shell startup time and keep it minimal and thus chose to not install this. If I do however, it will be picked up automatically by the .bashrc.

Actually, my real source does not look like this, I’ve defined a shell function in .shell_functions that makes my shell files smaller and more readable:

# Source file if it exists. Useful for shell startup scripts used across many systems.
sourceifexists() {
	[ -r "$1" ] && source "$1"
}
export sourceifexists >/dev/null

Now I can simply have lines like this in my shell init file:

sourceifexists $HOME/.local/bin/bashmarks.sh

I have a similar helper function that can reduce this condition below (that checks if a program is installed and we should configure the environment to use it)

# A better compiler for C langs.
if type clang >/dev/null 2>&1; then
	export CC=clang
	export CXX=clang++
fi

with

# Check if we have a program in PATH
program_is_in_path() {
	local program="$1"
	type "$1" >/dev/null 2>&1
}

to a more readable line

if program_is_in_path clang; then
	export CC=clang
	export CXX=clang++
fi

Manage different shells

zsh has become my standard shell after first being hesitant to move away from bash as it “worked”. The zsh have many nice features that I would not like to go without anymore like the customizable tab-completion for example! On some systems, there might not be a zsh installation initially before I’ve set the system up properly, or it might even not be allowed to install another shell or even room to have some shell binaries in $HOME/bin as the storage limitation is too small (like at my old University’s shell accounts we had). Then the shell setup must work in bash as well and not only zsh.

There are differences between bash and zsh, many, but there are also many similarities in the shell setup you would do in these ecosystems. Luckily zsh can also source many bash configurations (but not the other way around). Thus I’ve chosen to extract my shell configurations to some common files that are then included in the respective shell’s run commands (you know “rc” from .zshrc or .bashrc most likely stands for “run commands”).

There are some more details to the setup, but I’d refer to the source for that!

This way, e.g. .zshrc is not so large contains only configuration specific for this shell, and the same goes for .bashrc. A typical shell configuration task is to tweak the $PATH variable when wanting to make some binaries available on the command line without path prefix. When I do this configuration in .shell_commons, I know that it will now work for all the shells sourcing this base. How well this common extraction works with more shells would have to be tested, but as long as the common files contain only simple sh commands, it’s likely to work for many shells.

Manage different operating systems

I’ve used my dotfiles across Linux, BSD, macOS and Windows and since there are some differences in available features or how the system directory structure looks, there’s a need to make some special configurations per system.

Let’s for example say that for a macOS system I would like to set the $HOMEBREW_BUNDLE_FILE to point to to my Brewfile, so that I can manage my Homebrew formula and casks easily.

# if-is-macos-do
export HOMEBREW_BUNDLE_FILE=$HOME/.Brewfile

This envvar is however not relevant on other systems and would just clutter the environment, thus it should not be set (who knows, it could have unknown side effects on another system with clashing names). To support this, I have defined a few helper functions in .shell_functions:

export SHELL_PLATFORM=unknown
ostype() { echo $OSTYPE; }
case "$(ostype)" in
    *'linux'*   ) SHELL_PLATFORM=linux   ;;
    *'darwin'*  ) SHELL_PLATFORM=macos   ;;
    *'freebsd'* ) SHELL_PLATFORM=freebsd ;;
    *'bsd'*     ) SHELL_PLATFORM=bsd     ;;
esac

shell_is_linux()    { [ "$SHELL_PLATFORM" = linux ]; }
shell_is_macos()    { [ "$SHELL_PLATFORM" = macos ]; }
shell_is_freebsd()  { [ "$SHELL_PLATFORM" = freebsd ]; }
shell_is_bsd()      { [ "$SHELL_PLATFORM" = bsd ] || [ "$SHELL_PLATFORM" = freebsd ] || [ "$SHELL_PLATFORM" = macos ]; }

Now I can do:

if shell_is_macos; then
	export HOMEBREW_BUNDLE_FILE=$HOME/.Brewfile
	export HOMEBREW_BUNDLE_NO_LOCK=1
fi

Combined with some other helpers, tedious configuration checks now becomes sleek!:

shell_is_linux && program_is_in_path urxvt && export TERMEMU=urxvt

Wrap-up

This first post shed some light on how I manage my dotfiles across systems. It may or may not apply to you and your requirements, but maybe it could help inspire your own solution?

There will be more posts in this collection soon, let’s see what we can dig up! Until then, ^D

Leave a Comment

Your email address will not be published. Required fields are marked *

Loading...