How I Automate the Process of Setting up a New Mac
Manage Dotfiles with GNU Stow and Bash Scripts
1️⃣ Introduction
If you’re anything like me, then the thought of getting a new computer invokes two emotions: excitement and dread. You’re excited to get a brand new computer that is an upgrade to your current machine or maybe you’re starting a new job and need to get things setup quickly. However, you’re also dreading the hassle of having to go through the tedious process of configuring your computer to feel comfy like your last one. After going through this song and dance multiple times, I got really interested in automating this process via dotfiles and bash scripts.
Throughout this article, I will go over the automated process that I finally landed on after copious amounts of research.
The Why ❓
Let's start with why I decided to invest so much time into automating this process. If you've ever looked into this topic, you'll quickly find that there are many ways to manage dotfiles. Some methods use ready-made tools that handle it for you (check out this site for a comprehensive list of options). Other solutions are more customized and tailor-made.
One thing I learned from all my research is that it all depends on what works best for you and your configurations. Interestingly enough, it seems like there is no universally accepted solution for managing dotfiles in the developer community. After scoping out the different options, I decided to go the tailor-made route.
🤔 Choosing the Right Approach
Narrowing the choice down to the customized route ruled out a large subset of options, but then came the hard part of figuring out how to manage these dotfiles in an elegant way. Thankfully, there are wonderful people on the web who share their setups, and it seemed that the two most popular ways to manage this were:
My initial research led me to the git bare repo method, and I began implementing it. However, I encountered some difficulties and decided to take a break. Months later, I revisited the project and noticed several references to using GNU Stow. Naturally, I explored this option and eventually decided that Stow was the best way to go.
Although there were many articles written about the git bare repo process, I found it a bit too confusing. I also worried I might screw things up if I missed a step. On the other hand, GNU Stow was more intuitive for me from the start, and I felt it would be easier to maintain long term.
👩🏽💻Scripting
In addition to managing dotfiles, I also wanted to automate the installation of my applications and Mac settings. Now that I had a plan for managing my dotfiles, I needed to write scripts to automate this process.
🍺 Installing Applications via Homebrew
Homebrew touts itself as "The Missing Package Manager for macOS" and it's an amazing tool that allows you to install apps and packages via the command line. The first thing I did for this process was perform an audit of all the apps that I use on my work and personal computer and add them to a spreadsheet.
Then I labeled which ones required a manual install and which ones could be installed via Homebrew. Thankfully a majority of the apps could be installed via the command line so I created a brew-install.sh
script that ended up looking something like this:
#!/usr/bin/env bash
# Check for Homebrew to be present, install if it's missing
if test ! $(which brew); then
echo "🍺 Installing Homebrew"
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
brew update
else
echo "Homebrew is already installed."
fi
checkForResponse() {
read -p "Install $1 [y/n]? " response
}
checkForResponse "Applications"
if [[ $response == "y" || $response == "Y" ]]; then
echo "🍱 Installing Applications"
brew install --cask figma
brew install --cask itsycal
brew install --cask notion
# List out other apps here...
fi
checkForResponse "Packages"
if [[ $response == "y" || $response == "Y" ]]; then
echo "📦 Installing Packages"
brew install bat
brew install git
brew install stow
# List out other packages here...
fi
checkForResponse "Fonts"
if [[ $response == "y" || $response == "Y" ]]; then
echo "🔠 Installing Fonts"
brew install --cask font-fira-code
brew install --cask font-cascadia-code
brew install --cask font-inconsolata-for-powerline
# List out other fonts here...
fi
echo "✅ Installation using Homebrew is complete"
🖥️ Configuring Mac settings
Another thing I wanted to automate was configuring my Mac settings. These are always the "set it and forget it" aspects of owning a computer. However, you really notice the differences when you switch to a new computer and have the feeling that something is off.
I had seen some other people's dotfiles using a strange syntax like this: domains.<something>.<else>
. After some Googling, I discovered it was a low-level way to configure Mac preferences. The only problem was that I didn't have a list of all the different options. Then, I found this list of macOS defaults. This site was extremely helpful in figuring out what settings I could automate, and it explained the syntax well. It may not be an exhaustive list of all the Mac settings you can set from the command line, but it gave me plenty to work with.
To handle this, I created a new file called mac-settings.sh
, and the final output looked something like this:
#======================Dock settings =================
defaults write com.apple.dock "orientation" -string "left" # Set dock to the left
defaults write com.apple.dock "show-recents" -bool false # Do not display recent apps in the Dock
defaults write com.apple.dock "autohide" -bool false # Always display the dock (default)
defaults write com.apple.dock "mineffect" -string "genie" # Genie effect for minimizing windows (default)
#======================Screencapture settings =================
mkdir -p $HOME/Desktop/Screenshots # Create "Screenshots" folder on the desktop and set it as the default location
defaults write com.apple.screencapture "location" $HOME/Desktop/Screenshots
defaults write com.apple.screencapture "show-thumbnail" -bool true # Display the thumbnail after taking a screenshot (default)
defaults write com.apple.screencapture "include-date" -bool false # Don't include date in the screenshot file name
#======================Finder settings =================
defaults write NSGlobalDomain "AppleShowAllExtensions" -bool true # Show filename extensions
defaults write com.apple.finder "ShowPathbar" -bool true # Show path bar in the bottom of the Finder windows
defaults write com.apple.finder "FXPreferredViewStyle" -string "Nlsv" # Set the default view style for folders without custom setting to list view
defaults write com.apple.finder "FXDefaultSearchScope" -string "SCcf" # Set the default search scope when performing a search in Finder to the current folder
defaults write NSGlobalDomain "NSDocumentSaveNewDocumentsToCloud" -bool false # Save to disk (not to iCloud) by default
#======================Trackpad settings =================
defaults write com.apple.AppleMultitouchTrackpad "FirstClickThreshold" -int 1
defaults write com.apple.AppleMultitouchTrackpad "DragLock" -bool false
defaults write com.apple.AppleMultitouchTrackpad "Dragging" -bool false
defaults write com.apple.AppleMultitouchTrackpad "TrackpadThreeFingerDrag" -bool false
#======================Mission Control settings =================
defaults write com.apple.dock "mru-spaces" -bool false # Do not rearrange spaces based on most recent use
defaults write com.apple.dock "expose-group-apps" -bool false # Do not group windows by application (default)
#======================Text Edit settings =================
defaults write com.apple.TextEdit "RichText" -bool false # Set default document format as plain text (.txt)
⚙️ Stowing dotfiles
One vital aspect of this new dotfiles workflow was to create a portable and reusable system that could be stored in version control. I wanted a way to easily update my dotfiles without having to manually copy them over in an ad hoc manner.
As I mentioned in the introduction, I chose GNU Stow because it felt more intuitive to me. GNU Stow describes itself as a "symlink farm manager". Simply put, it manages the creation and deletion of symbolic links. To stow something, you type stow
followed by the name of the package(s) you want to symlink. Here's what the final stow
command I used looks like:
stow -v --dir=$HOME/dotfiles/$LAPTOP_TYPE --target=$HOME zsh git vim
🛠️ Breaking down the code
-v
= Enables verbose mode, which outputs detailed information about what Stow is doing.--dir
= Specifies the directory where the dotfiles are located. If not specified, this defaults to the current directory.--target
= Sets the target directory where the symbolic links will be created. In this case, it is the$HOME
directory. If not specified, this defaults to the parent of the stow directory.
📦 Package Structure
When using GNU Stow, the package structure is crucial because it determines how the symbolic links are created and organized. Each package (e.g., zsh
, git
, vim
) are a subdirectory within the main directory specified by --dir
. Inside each package directory, the files and subdirectories mirror the structure of the target directory where the symlinks will be created. This ensures that the symbolic links are placed correctly in the target directory.
Maintaining a consistent structure across different packages helps in managing and updating dotfiles. For example, the .zshrc
file in the zsh
package will be placed in zsh/.zshrc
so that Stow can link it to $HOME/.zshrc
.
🧪 Testing
It’s all good and fun to have these scripts, but I needed an environment to test them in. It’s difficult to get access to a clean slate of an operating system so I looked around for options. Plus, I wanted an environment that I could test out these bash scripts without borking my own machine. At some point I heard about the idea of using Docker and I proceeded down that route.
🐳 Docker
Even though I have written about Docker before, I am by no means an expert. However, I understood that the containerization aspect of Docker would make it an ideal environment to use as my testing ground.
The first thing I did was write a Dockerfile
to create a simple container that could run bash scripts. When the container runs, the Dockerfile
is set up to sh
into the working directory. This setup allowed me to run the scripts and fix any issues. It also let me test CLI commands before adding them to the final scripts.
# Grab base image from Docker Hub
FROM ubuntu:latest
# Update and install necessary packages
RUN apt-get update && \
apt-get install -y git vim curl zsh bash build-essential procps curl file language-pack-en
WORKDIR /root/dotfiles/
# Copy the scripts files from the current directory into the container's working directory
COPY . .
# Make the brew-install.sh script executable
RUN chmod +x brew-install.sh setup.sh
# Start bash
CMD ["sh"]
The great thing about using a Docker container was that I could experiment with many different approaches. I don't write a lot of bash scripts, so this was especially helpful. If I messed something up, no problem—I could just delete the container, make some changes, and then restart and try again. It turned out to be a good sandbox, and I recommend using one if you want to fine-tune your own dotfiles workflow.
🔠 Putting it all Together
With all those steps in place, I finally had a workflow that I could run. Here’s a breakdown of how it all pans out:
setup.sh
This file is the main driver that executes all the complimentary scripts and functions.
#!/usr/bin/env bash
# Source the helpers file to get access to the functions
source ./helpers.sh
#======================Run setup scripts===============
./brew-install.sh
setup-stow
setup-oh-my-zsh
setup-cobalt2-theme
./mac-settings.sh
brew-install.sh
The first script called from setup.sh
is the brew-install.sh
script that I mentioned earlier. This script checks if Homebrew is installed. If it's not, it will install it and then ask if I want to install three types of items: applications, packages, and fonts.
setup-stow()
Once the Homebrew script finishes, the next step is to run the setup-stow
function. As the name suggests, this function takes care of setting up the correct Stow environment.
setup-stow() {
# Check if stow is present, install if it's missing
if test ! $(which stow); then
echo "Installing GNU Stow"
brew install stow
else
# Get the laptop type
LAPTOP_TYPE=$(get-laptop-type)
echo "💻 Laptop type: $LAPTOP_TYPE"
echo "🐐 Stowing dotfiles..."
stow -v --dir=$HOME/dotfiles/$LAPTOP_TYPE --target=$HOME zsh git vim
echo "🚀 Stow complete!"
fi
}
The function prompts me for the "laptop type" since I maintain separate directories for work and personal files to stay organized. This approach allows me to stow work-related files on my company laptop and personal files on my own Mac. While the differences between the two sets of files are minimal, this separation enables me to customize the experience for each device, ensuring that each setup is tailored to its specific use case.
setup-oh-my-zsh()
The third item run in the main script is the setup-oh-my-zsh()
function. This handles installing oh my zsh and is passed two important flags:
--keep-zshrc
- This flag tells oh my zsh to respect the existing.zshrc
file so that any pre-existing settings will be reflected.--unattended
- This flag uses the “unattended install” to prevent oh my zsh from exiting the script early.
setup-oh-my-zsh() {
# Check if oh-my-zsh is present, install if it's missing
if [ ! -d "$HOME/.oh-my-zsh" ]; then
echo "Installing oh my zsh..."
sh -c "$(curl -fsSL https://raw.github.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" "" --keep-zshrc --unattended
else
echo "oh my zsh is already installed."
fi
Having the set-up-stow
function run after the Stow setup is intentional. It's crucial that the .zshrc
file is in the $HOME
directory so that when the flag checks for it, it can find it and apply the existing settings. This wasn't immediately obvious to me, but I realized the order of operations was important after testing it in the Docker container. This is why I'm a big fan of having a sandbox environment that gives you the opportunity to experiment with these scripts.
mac-settings.sh
The last part of the script runs the mac-settings.sh
script that I shared earlier. This script handles running the CLI commands to update some Mac settings, such as moving the dock to the left and setting the default folder for screenshots.
👋🏽 El Fin
After a lot of trial and error and even more research, I finally feel happy with my dotfiles workflow. Now instead of grappling with the dichotomy of emotions when setting up a new computer, I’m prepared that the workflow I configured will make getting up and running with a new machine a more enjoyable process
If you enjoy what you read, feel free to like this article or subscribe to my newsletter, where I write about programming and productivity tips.
As always, thank you for reading, and happy coding!