Syncing Homebrew Installs

Those of us who love the command line, have a tendency to install a lot of useful utilities, and want them available on all our computers. On macOS we tend to use Homebrew. This document serves to describe three ways to generate a useful file to solve that problem.

As I see it there are 3 basic approaches to syncing your homebrew utilities across machines.

  1. I don’t care about the details, just make it work.
  2. I would like a simple installation document that
    • lists what I’ve installed
    • what each is for
    • can be used to install the same things on other machines
  3. I would like an efficient installation document that
    • lists what I’ve installed
    • what each is for
    • can be used to install the same things on other machines
    • won’t waste time trying to reinstall things already installed
    • can handle post-install tasks, and make sure they only get run once.

This pots will address each of those, in order.

I don’t care about the details, just make it work

  1. run brew bundle dump
    • creates a Brewfile document in the current directory.
  2. copy that to the other machine
  3. cd to the same directory as the Brewfile on the new machine
  4. run brew bundle

That’s it. You’re done.

I’d like a simple installation document…

The following script will generate a documented bash script called which you can then sync across machines via whatever your normal method is. The downside to this is that rerunning it on a synced machine after adding one or two entries is going to be really slow. Also, there’s no good way to include post-install setup and have it still be idempotent and not break things. Post-install setups should generally only be run once, not every time you load the file.

The file will look like this:

brew tap charmbracelet/tap
brew tap d12frosted/emacs-plus

# Strip or convert ANSI codes into HTML, (La)Tex, RTF, or BBCode
brew install ansifilter

# Official Amazon AWS command-line interface
brew install awscli

# Clone of cat(1) with syntax highlighting and Git integration
brew install bat

#... and so on

Here’s the script that generated it (presumes you’re runing in bash / zsh shell)

echo '#!/bin/sh' >;
brew leaves --installed-on-request \
  | grep --color=none "/" \
  | sed -e "s/\(.*\)\/.*/\1/" -e "s/^/brew tap /" \
  | sort -u \
brew info $(brew leaves --installed-on-request) 2>/dev/null \
  | grep --color=none -A2 "==> [^[:space:]]\+:" \
  | grep -v -- "--" \
  | sed -e 's/==> \(.*\):.*/brew install \1/' \
    -e '/^brew install/!s/\(.*\)/# \1/' \
  | ruby -e 'lines=STDIN.each_line.to_a; (2..(lines.size - 1)).step(3){|n| puts "\n#{lines[n - 1]}"; puts lines[n]; puts lines[n - 2]}' \
chmod u+x

I’d like an efficient installation document…

This is basically the same idea BUT it adds in 3 bash functions at the top, which make rerunning this much faster and allow you to do post-install setup if any of the things you’re installing need that.

Start by creating a new file called and adding this to the top of it. I’ll explain how to use them at the end of this section.

#!/usr/bin/env bash

# run the following to extract the relevant info to manually append to this
# brew info <some_command> | head -n3

function maybrew() {
	if ! is_installed $1; then
		install_or_die "$1"
		echo "-- Skipping $1 (installed already)"

function install_or_die() {
	brew install $1
	echo ""
	echo ""
	echo "-- Installing $1"
	brew install $1
	if [ $? -ne 0 ]; then
		echo "problem installing $1"
		echo "exiting"
		exit 1

function is_installed() {
	if brew ls --versions $1 > /dev/null; then
		# the package is installed
		# the package is not installed

Next we’ll run a script that’s almost identical to the last one, except instead of calling brew install foo it’s going to call maybrew foo.

brew leaves --installed-on-request \
	| grep --color=none "/" \
	| sed -e "s/\(.*\)\/.*/\1/" -e "s/^/brew tap /" \
	| sort -u \
brew info $(brew leaves --installed-on-request) 2>/dev/null \
	| grep --color=none -A2 "==> [^[:space:]]\+:" \
	| grep -v -- "--" \
	| sed -e 's/==> \(.*\):.*/maybrew "\1"/' \
	-e '/^maybrew/!s/\(.*\)/# \1/' \
	| ruby -e 'lines=STDIN.each_line.to_a; (2..(lines.size - 1)).step(3){|n| puts "\n#{lines[n - 1]}"; puts lines[n]; puts lines[n - 2]}' \
chmod u+x

Once again, you’ve got a file that you can sync, and append to over time.

The Three Functions

  • maybrew takes a package name as an argument and asks homebrew if it’s installed. If not, it attempts to install it. The script will die if it fails, because you’ve got bigger problems to sort out at this point.
  • install_or_die takes a package name, and installs it or dies. called by maybrew
  • is_installed performs the fastest test i could find to determine if a homebrew package is installed. This is what you’re going to want to use if you need to do post install setup.


  1. maybrew

    Just call it with the name of a package: maybrew foo

  2. install_or_die

    Just call it with the name of a package: install_or_die foo

  3. is_installed

    Use this in an if statement to test if something’s been installed. If not, install it, and perform whatever post-install things the given script needs. For example, here’s how i use it to install fzf

if ! is_installed "fzf"; then
	# install it
	brew install fzf
	# now do post install setup for fzf
	y | $(brew --prefix)/opt/fzf/install
	echo "-- Skipping fzf (installed already)"

An explanation

For those of you who think those scripts look like dark magic, and would like to understand how they work, here’s a breakdown. For everone else, you can stop here. Good luck.

# There isn't a way (that i know of), to continue a line ( \ ) AND
# have a comment before the next line.

# create a new file with a "shebang line". Unix reads the first line
# of the executable and if it starts with `#!` and a path it will pass
# the contents of the file to whatever executable lives at that path
# the  `>`  tells it to send the output of echo to a new file
# named
echo '#!/bin/sh' >;
# `brew leaves` outputs the minimal list of things to install
# to regenerate your setup. Their dependencies will fill in the gaps
# BUT that's not necessarily the list of things you specifically
# told it to install. `--installed-on-request` should be just
# the list of things you specifically told it to install.
brew leaves --installed-on-request \
	# if the line has a / in it, it means its a package installed from a tap
	# and we need to run `brew tap` on them before we can install them
	# so i'm trying to extract the taps. This isn't perfect, but it seems to work.
	# `--color=none` is to compensate for anyone who has their grep configured to
	# highlight matches (like me).
	| grep --color=none "/" \
	# strips off the last slash and everything after it
	# foo/bar/baz becomes foo/bar
	# then replaces the start of the line with `brew tap`
	| sed -e "s/\(.*\)\/.*/\1/" -e "s/^/brew tap /" \
	# sort the results and only include unique items
	| sort -u \
	# append the output to the file
# `brew info` optionally takes multiple lines of input.
# If you do this it's way faster than calling brew info
# on each item that `brew leaves` returns individually.
# so that's what we're doing. HOWEVER this always produces
# errors and warnings on my machine that you can't do anthing
# about so `2>/dev/null` instructs it to take everything printed
# to Standard Error and shove it into a black hole.
brew info $(brew leaves --installed-on-request) 2>/dev/null \
	# this is a long piece of output, but we're looking for the
	# lines with package names. Those can be identified by
	# the fact that they all start with this (but with the appropriate
	# package name)
	# `==> package_name:`
	# extract those lines with `grep` using a regexp that asks for
	# lines with ==> followed by a single space, and 1 or more non-space
	# characters and then a colon.
	# we also want the 2 lines that follow.
	# the first line afterwards is the short description of the package
	# the 2nd line afterwards is the url where you can find it.
	# for example:
	# ➜ brew info git | head -n3
	# ==> git: stable 2.37.3 (bottled), HEAD
	# Distributed revision control system
	| grep --color=none -A2 "==> [^[:space:]]\+:" \
	# each section is separated by a `--` line.
	# get rid of those lines with `grep -v`
	# this is problematic because shells can't tell the
	# difference between an argument of -- and "--" so
	# it interprets the -- as the start of a command like
	# `--color=none` from above. So, we use a common Unix
	# idiom of passing JUST `--` which is generally interpreted
	# to designate the end of the options to the command.
	# now that it knows the next thing won't be an option,
	# we can give it the thing we actually want it to search for "--"
	| grep -v -- "--" \
	# the line with the ==> is the one with the command name
	# perform a regular expression capture for the stuff
	# after the space and before the colon (the command name)
	# then replace the line with `brew install <captured text>`
	# `\1` just means "the first capture group".
	| sed -e 's/==> \(.*\):.*/brew install \1/' \
	# take every line that DOES NOT start with "brew install"
	# The `/!s/` bit is the pice that makes this magic work
	# capture the entire line, and then print out a #
	# to make it into a comment, and then the whole captured line.
	-e '/^brew install/!s/\(.*\)/# \1/' \
	# things are in the wrong order.
	# as you can see in the `brew info git` example above, its
	# 1. command, 2. description, 3. url.
	# I want the 2. description, 3. url, 1. command.
	# this would probably be a nightmare in bash, so i just
	# did a ruby onliner that reads everything from Standard input
	# into an array, iterates over the array jumping 3 ahead each time
	# (it's always a set of 3 lines), then prints out the lines
	# in the order i want with an extra newline before the first one
	| ruby -e 'lines=STDIN.each_line.to_a; (2..(lines.size - 1)).step(3){|n| puts "\n#{lines[n - 1]}"; puts lines[n]; puts lines[n - 2]}' \
	# append it all to the file
# make the file executable.
chmod u+x