Whenever I want to upgrade any one of my systems, I run sd-world.

You can find the current version of sd-world here in my dotfiles.

Here’s a snapshot1:

#!/usr/bin/env bash
# perform a full system upgrade

set -euo pipefail

log() {
	local bold=$(tput bold) normal=$(tput sgr0)
	echo "${bold}$*${normal}"
}

run_if_exists() {
	if command -v "$1" >/dev/null 2>&1; then
		if [[ $# -eq 1 ]]; then
			log "Running $1..."
			"$1"
		else
			shift
			log "Running $*..."
			"$@"
		fi
	elif [[ -d "$1" ]]; then
		shift
		log "Running $*..."
		"$@"
	fi
}

# usage: do_git <path/to/git/repo>
do_git() {
	if [[ -d "$1" ]]; then
		run_if_exists "$1" git -C "$1" pull origin master
	fi
}

case "$(uname)" in
	# linux
	Linux)
		# alpine linux
		run_if_exists "apk" doas apk upgrade

		# arch linux
		run_if_exists "pacman" sudo pacman -Syu

		# debian linux
		# warning: macos has /usr/bin/apt which is a Java thing
		run_if_exists "apt-get" sudo apt-get upgrade -y
		run_if_exists "apt-get" sudo apt-get autoremove
		;;

	# macOS
	Darwin)
		# homebrew
		run_if_exists "brew" brew upgrade
		run_if_exists "brew" brew cleanup

		# system update and app store
		# run_if_exists "softwareupdate" softwareupdate --install --all
		run_if_exists "mas" mas upgrade
		;;

	# windows
	MINGW*)
		# third-party package manager
		run_if_exists "scoop" scoop update
		;;
esac

# flatpaks
run_if_exists "flatpak" flatpak update

# nix
run_if_exists "nix-channel" nix-channel --update
run_if_exists "nix-env" nix-env -u

# pihole
# update pihole itself and gravity lists
run_if_exists "pihole" pihole -up

# dotfiles
do_git "$HOME/.dotfiles"
do_git "$HOME/.dotfiles_corp"

There’s a lot to unpack here.

Why is it called sd-world?

world is an inspiration taken from Gentoo Linux. To upgrade a typical gentoo system, you usually run:

emerge --ask --quiet --update --changed-use --deep @world

There’s something deeply inspiring about saying it out loud: “emerge the world”. As if the whole world is at your fingertips.

sd stands for “script directory”, it’s an inspiration taken from Ian Henry.

Rationale: I tend to put scripts I run semi-frequently in a .bin directory that is in my system $PATH. However, there’s always a chance their name could clash with a built-in one (e.g. in /usr/bin/). In order to prevent (or mitigate) it from happening, a prefix is added. For a long time in my life I used the t- prefix, merely because of my first name initial. At some point I migrated to sd-. That’s all, nothing fancy about it.

Why bash?

bash is the de-facto standard shell in most Linux distributions I care about. And it’s also easily available in macOS and BSDs. And it’s POSIX compliant.

Therefore: availability, portability and compatibility.

Why /usr/bin/env bash instead of /bin/bash?

Because the env shebang is more portable. This is more relevant when working with BSDs. On Linux /bin/bash should be mostly fine.

Why set -euo pipefail?

A well-established good practice.

Why use a separare log function?

  1. Old habits die hard.
  2. Consistent formatting.

Why run_if_exists?

Since the script attempts to upgrade (potentially) many package managers, at the very least we try to skip the ones that aren’t installed. For example, there’s no need to attempt to run pacman in a macOS system.

What else?

The rest should be quite straightforward to understand. Some design decisions:

  • sudo permissions are not asked upfront, because not every system uses sudo. Notably, Alpine Linux and OpenBSD use doas by default. Also, laziness is OK as the script is intended for interactive use.
  • git is there merely for convenience. Updating my dotfiles could be done from a separate script, but that would be overkill for my simple use case.
  • There’s no concurrency / parallelism, and that’s intentional. I prefer output readability and system stability in this case.

The world is yours.


  1. Prefer to refer to the up-to-date version in my dotfiles repository though. I included a snapshot merely because there’s a non-zero chance the git version could be moved elsewhere someday. ↩︎