I'm addicted to automation. I said it. Okay?
Today we are going to learn how we can automatize all our macOS settings, apps and homebrew
packages using bash
and stow (and a few other cli tools).
Intro
As developers we might encounter ourselves setting up a new macOS environment quite often (I had to do it 2 times so far this year). We tend to accumulate dotfiles
pretty easily and keeping them in sync across multiple machines becomes painful.
And not only that. We also accumulate a galaxy of different packages, tools and apps that we use on daily bases to work.
Oh and all the macOS settings that we surely need? our poor souls.
In order to reduce the pain and enjoy life we can make use of different tools and our coding skills to automatize all this process.
The objective
When I receive a new Macbook at a new job, all I want to do is:
- Login in the app store
- Download and run a magic script
- Go out and enjoy the sun while the macbook is being set up
Bootstraping
So first we need a few tools to start setting up everything. As we already know xcode
, brew
and git
are a must. Here we start with:
xcode-select --install
sudo xcodebuild -license accept
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)"
brew install git
Brew taps, casks and formulas
We want to install all the packages and apps we need.
To list all your currently installed packages (formulae) and apps (casks) you can run:
brew list
we also need to list all the taps
(Third-Party Repositories) we use:
brew tap
Perfect. With this information we can code some for loops
to iterate on the taps
, casks
and formulas
:
apply_brew_taps() {
local tap_packages=$*
for tap in $tap_packages; do
if brew tap | grep "$tap" > /dev/null; then
warn "Tap $tap is already applied"
else
brew tap "$tap"
done
}
install_brew_formulas() {
local formulas=$*
for formula in $formulas; do
if brew list --formula | grep "$formula" > /dev/null; then
warn "Formula $formula is already installed"
else
info "Installing package < $formula >"
brew install "$formula"
fi
done
}
install_brew_casks() {
local casks=$*
for cask in $casks; do
if brew list --casks | grep "$cask" > /dev/null; then
warn "Cask $cask is already installed"
else
info "Installing cask < $cask >"
brew install --cask "$cask"
fi
done
}
Note: in order to keep our scripts some how idempotent, we want to check if the formulas/casks/taps are already installed.
These functions will take a list and run brew install
. How do we use them?
# apply the taps first
taps=(homebrew/cask)
apply_brew_taps "${taps[@]}"
# install casks
apps=(firefox docker)
install_brew_casks "${apps[@]}"
# install formulas
packages=(curl go)
install_brew_formulas "${packages[@]}"
OK. But some of my apps are not available in hombrew 😡
Installing App Store apps
For all the apps that cannot be found as brew casks
we can use mas (Mac App Store cli).
This cli tool needs the IDs of the apps. To check the apps you have installed simply run:
mas list
If not all your apps are listed you can also search for them and get their IDs:
mas search Goodnotes
So again, writing some pretty code:
masApps=(
"937984704" # Amphetamine
"1444383602" # Good Notes 5
"768053424" # Gappling (svg viewer)
)
install_masApps() {
info "Installing App Store apps..."
for app in $masApps; do
mas install $app
done
}
Neat!
So far we have installed all our apps and cli tools.
Let's see how we can automatize our macOS settings.
defaults
: macOS settings from the terminal
This tool will help us to read and write settings. For that, you need to know the domain, key and type of the setting. Usually a google/duckduckgo/ecosia search should help you to find this information. If not, defaults
provides some commands to help you search for the settings.
Let's say I want to change some finder setting. First, find its domain:
defaults domains | grep finder
Read all the settings available for finder
defaults read com.apple.finder
I want to change ShowExternalHardDrivesOnDesktop but I don't know its type
defaults read-type com.apple.finder ShowExternalHardDrivesOnDesktop
Type is boolean
. So finally we can set this to false
defaults write com.apple.finder ShowExternalHardDrivesOnDesktop -bool false
(You can see all my settings in my dotfiles repo,link below)
Lastly, and this is a personal preference, I want to set vscode
as default application for all my source code files. To do so we will use duti.
code_as_default_text_editor() {
local extensions=(
".c"
".cpp"
".js"
".jsx"
".ts"
".tsx"
".json"
".md"
".sql"
".html"
".css"
".scss"
".sass"
".py"
".sum"
".rs"
".go"
".sh"
".log"
".toml"
".yml"
".yaml"
"public.plain-text"
"public.unix-executable"
"public.data"
)
for ext in $extensions; do
duti -s com.microsoft.VSCode $ext all
done
}
Settings: check.
Managing dotfiles with GNU Stow
stow is simply a symlink manager that let us define a folder structure (package) for our dotfiles and then symlink them following that structure. So let's say one of your dotfiles is ~/.config/kitty/kitty.conf
, this will be our kitty
package. In order to place this file in the correct path, we define the package as as
dotfiles/kitty/.config/kitty/kitty.conf
(assuming all our dotfiles are in a folder called dotfiles)
We can simulate stow to see the resulting symlinks
cd dotfiles
stow -nSv kitty
If the output looks correct, we symlink the package with
stow --target $HOME kitty
Note: we want to use stow only for files, since a folder like .config
will always contain many other files/folders, we don't want to symlink this folder. Therefor we need to first remove existing files (the ones we want to stow) and check that the needed directories exist. So, some lovely code for that:
stow_dotfiles() {
local files=(
".zprofile"
".gitconfig"
".zshrc"
".vimrc"
)
local folders=(
".config/nvim"
".config/kitty"
)
info "Removing existing config files"
for f in $files; do
rm -f "$HOME/$f" || true
done
for d in $folders; do
rm -rf "$HOME/$d" || true
mkdir -p "$HOME/$d"
done
local dotfiles="kitty nvim"
info "Stowing: $dotfiles"
stow -d stow --verbose 1 --target $HOME $dotfiles
}
So far so good, all our dotfiles are versioned and any change on them can be pushed and synced in all our macOS machines with git.
What else did I want to do?
...
Setting up Oh My Zsh
I use oh my zsh along with powerlevel10k and this is how my terminal looks
pretty, right?
So I need to install this scripts on my system, for which I have more lovely code:
install_oh_my_zsh() {
if [[ ! -f ~/.zshrc ]]; then
info "Installing oh my zsh..."
ZSH=~/.oh-my-zsh ZSH_DISABLE_COMPFIX=true sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"
chmod 744 ~/.oh-my-zsh/oh-my-zsh.sh
info "Installing powerlevel10k"
git clone --depth=1 https://github.com/romkatv/powerlevel10k.git ~/.oh-my-zsh/custom/themes/powerlevel10k
else
warn "oh-my-zsh already installed"
fi
}
Note: my powerlevel10k
configuration is part of my dotfiles. If you don't have any, you can run p10k configure
and follow the configuration process.
Installing (Neo)Vim plugins
Lastly, I also want to automatically install all my neovim plugins. First I need to install the plugin manager (I use vim-plug) and then I want to install all the plugins defined on my dotfiles
install_neovim() {
info "Installing NeoVim"
install_brew_formulas neovim
info "Installing Vim Plugged"
sh -c 'curl -fLo "${XDG_DATA_HOME:-$HOME/.local/share}"/nvim/site/autoload/plug.vim --create-dirs https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim'
}
# Install plugins and quit
nvim +PlugInstall +qall
Eureka. My dev environment is (almost) fully automated. So coming back to the objectives, this is the magic script:
curl -sO https://raw.githubusercontent.com/protiumx/.dotfiles/main/dotfiles
That's it. My life quality has been improved by 2Π%.
And here my dotfiles repo
dotfiles
Set up dev environment in a macOS machine. This script installs all the packages and apps I use, stow my dotfiles and sets all my preffered macOS configurations.
Check my Medium article.
Installing
Run the dotfiles
script:
curl -sO https://raw.githubusercontent.com/protiumx/.dotfiles/main/dotfiles
Reusing
In order to reuse these scripts, here a summary of files you can change/adapt to your needs:
-
scripts/packages.sh
: all thehomebrew
taps and packages to install -
scripts/fonts.sh
:homebrew
fonts to install -
scripts/apps.sh
:homebrew
casks to install -
scripts/osx.sh
: macOS settings -
scripts/config.sh
: general settings and dotfiles
Testing Stow
To double checks that the dot files will be correctly linked, you can use stow to simulate the result. E.g.
stow -nSv vim
Note: I have added a lot of read -p
on my script to debug each section but they can all be removed so there is no need for interaction. Prompting for the sudo password could be than an enable it for the rest of command.
What I'm missing?
- configuration for different apps like
clipy
.defaults
show some configuration but not all of them. - some macOS config like
disable autocapitalization
don't seem to be available throughdefaults
I'd love to hear some ideas to improve this current setup!
Currently I'm checking ansible so stay tuned for a possible upgrade.
👽
Top comments (0)