← back

MacOS setup with a simple bash script

From time to time I need to setup fresh MacOS installation. It’s not that complicated but since it happens not very often, I tend to forget the exact steps and commands. This blog post is a summary for myself and hopefully an inspiration for you.

Why bash?

Well, since all you have is a clean machine, you need to be cautious about what you depend on. Bash is pretty much commonplace and the last thing you want to do is lurking the web for a missing CLI required for your bootstrap script.

The script itself

There are a few things I do to setup my machine:

Installing packages

Personally I use Homebrew.

bash
ensure-homebrew-installed() {
which brew &>/dev/null && return 0
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
if [ "$(basename "$SHELL")" = "zsh" ]
then
echo '[ -e /opt/homebrew/bin/brew ] && eval "$(/opt/homebrew/bin/brew shellenv)"' >> ~/.zprofile
fi
}
bash
ensure-homebrew-installed() {
which brew &>/dev/null && return 0
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
if [ "$(basename "$SHELL")" = "zsh" ]
then
echo '[ -e /opt/homebrew/bin/brew ] && eval "$(/opt/homebrew/bin/brew shellenv)"' >> ~/.zprofile
fi
}

once package manager is setup, lets install all the missing pieces:

bash
install-packages() {
# in case brew is installed for the first time in the current shell session
eval "$(/opt/homebrew/bin/brew shellenv)"
# Taps
brew tap 'homebrew/cask-fonts'
brew update
# Personal
brew install --HEAD psmolak/tap/bin
# Fonts
brew install 'font-ubuntu'
brew install 'font-ubuntu-condensed'
brew install 'font-ubuntu-mono'
brew install 'font-fira-code'
brew install 'font-jetbrains-mono'
# Formulas
brew install 'coreutils'
brew install 'tree'
brew install 'nvm'
brew install 'pyenv'
brew install 'ripgrep'
brew install 'jq'
brew install 'gh'
brew install 'shfmt'
brew install 'zsh-autopair'
brew install 'zsh-completions'
brew install 'pipx'
pipx ensurepath
# Casks
brew install --cask 'arc'
brew install --cask 'iterm2'
brew install --cask 'raycast'
brew install --cask 'google-chrome'
brew install --cask 'docker'
brew install --cask 'figma'
brew install --cask 'signal'
brew install --cask 'slack'
brew install --cask 'visual-studio-code'
# needed for custom CSS plugin
setup-custom-css-vscode-file-perms
}
setup-custom-css-vscode-file-perms() {
# https://github.com/be5invis/vscode-custom-css
sudo chown -R "$(whoami)" "$(which code)"
if [ -d "/usr/share/code" ]; then
sudo chown -R "$(whoami)" /usr/share/code
fi
}
bash
install-packages() {
# in case brew is installed for the first time in the current shell session
eval "$(/opt/homebrew/bin/brew shellenv)"
# Taps
brew tap 'homebrew/cask-fonts'
brew update
# Personal
brew install --HEAD psmolak/tap/bin
# Fonts
brew install 'font-ubuntu'
brew install 'font-ubuntu-condensed'
brew install 'font-ubuntu-mono'
brew install 'font-fira-code'
brew install 'font-jetbrains-mono'
# Formulas
brew install 'coreutils'
brew install 'tree'
brew install 'nvm'
brew install 'pyenv'
brew install 'ripgrep'
brew install 'jq'
brew install 'gh'
brew install 'shfmt'
brew install 'zsh-autopair'
brew install 'zsh-completions'
brew install 'pipx'
pipx ensurepath
# Casks
brew install --cask 'arc'
brew install --cask 'iterm2'
brew install --cask 'raycast'
brew install --cask 'google-chrome'
brew install --cask 'docker'
brew install --cask 'figma'
brew install --cask 'signal'
brew install --cask 'slack'
brew install --cask 'visual-studio-code'
# needed for custom CSS plugin
setup-custom-css-vscode-file-perms
}
setup-custom-css-vscode-file-perms() {
# https://github.com/be5invis/vscode-custom-css
sudo chown -R "$(whoami)" "$(which code)"
if [ -d "/usr/share/code" ]; then
sudo chown -R "$(whoami)" /usr/share/code
fi
}

SSH keys

all right, next let’s take care of SSH keys:

bash
generate-new-ssh-key() {
local keyname="${1}"
local keypath="${2}"
read -rp "Enter a new SSH key comment: " comment
if [ -z "$comment" ]; then
echo "Please provide a comment for your new ssh key, i.e 'iMac27 [email protected]'"
return 1
fi
if [ ! -d "$keypath" ]; then
mkdir -p "$keypath"
chmod 700 "$keypath"
fi
ssh-keygen -t ed25519 -C "$comment" -f "${keypath}/${keyname}"
}
add-ssh-key-to-agent() {
local keyname="${1}"
local keypath="${2}"
eval "$(ssh-agent -s)"
ssh-add --apple-use-keychain "${keypath}/${keyname}"
}
bash
generate-new-ssh-key() {
local keyname="${1}"
local keypath="${2}"
read -rp "Enter a new SSH key comment: " comment
if [ -z "$comment" ]; then
echo "Please provide a comment for your new ssh key, i.e 'iMac27 [email protected]'"
return 1
fi
if [ ! -d "$keypath" ]; then
mkdir -p "$keypath"
chmod 700 "$keypath"
fi
ssh-keygen -t ed25519 -C "$comment" -f "${keypath}/${keyname}"
}
add-ssh-key-to-agent() {
local keyname="${1}"
local keypath="${2}"
eval "$(ssh-agent -s)"
ssh-add --apple-use-keychain "${keypath}/${keyname}"
}

I also like to add config entries for Github and BitBucket to ~/.ssh/config. The following function will make sure every time you connect to the servers i.e via git clone, the SSH client will retrieve the key from MacOS Keychain. The key is stored securely and you don’t have to type the password anymore!

bash
add-ssh-host-config-entry() {
local keyname="${1}"
local keypath="${2}"
local host="${3}"
local identity="${keypath}/${keyname}"
local config="$HOME/.ssh/config"
if [ ! -f "$config" ]; then
echo "No config $config found. Creating $config with permissions 664."
touch "$config"
chmod 644 "$config"
fi
if ! grep -q "Host $host" "$config"; then
{
echo "Host $host"
echo " AddKeysToAgent yes"
echo " UseKeychain yes"
echo " IdentityFile ${identity}"
echo
} >>"$config"
echo "SSH configuration added for $host"
else
echo "SSH configuration lines for $host already exist in $config."
fi
}
bash
add-ssh-host-config-entry() {
local keyname="${1}"
local keypath="${2}"
local host="${3}"
local identity="${keypath}/${keyname}"
local config="$HOME/.ssh/config"
if [ ! -f "$config" ]; then
echo "No config $config found. Creating $config with permissions 664."
touch "$config"
chmod 644 "$config"
fi
if ! grep -q "Host $host" "$config"; then
{
echo "Host $host"
echo " AddKeysToAgent yes"
echo " UseKeychain yes"
echo " IdentityFile ${identity}"
echo
} >>"$config"
echo "SSH configuration added for $host"
else
echo "SSH configuration lines for $host already exist in $config."
fi
}

since we are setting host specific settings anyway, why not add fingerprints as well:

bash
known-hosts-bitbucket() {
# https://bitbucket.org/site/ssh
echo "bitbucket.org ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDQeJzhupRu0u0cdegZIa8e86EG2qOCsIsD1Xw0xSeiPDlCr7kq97NLmMbpKTX6Esc30NuoqEEHCuc7yWtwp8dI76EEEB1VqY9QJq6vk+aySyboD5QF61I/1WeTwu+deCbgKMGbUijeXhtfbxSxm6JwGrXrhBdofTsbKRUsrN1WoNgUa8uqN1Vx6WAJw1JHPhglEGGHea6QICwJOAr/6mrui/oB7pkaWKHj3z7d1IC4KWLtY47elvjbaTlkN04Kc/5LFEirorGYVbt15kAUlqGM65pk6ZBxtaO3+30LVlORZkxOh+LKL/BvbZ/iRNhItLqNyieoQj/uh/7Iv4uyH/cV/0b4WDSd3DptigWq84lJubb9t/DnZlrJazxyDCulTmKdOR7vs9gMTo+uoIrPSb8ScTtvw65+odKAlBj59dhnVp9zd7QUojOpXlL62Aw56U4oO+FALuevvMjiWeavKhJqlR7i5n9srYcrNV7ttmDw7kf/97P5zauIhxcjX+xHv4M="
echo "bitbucket.org ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBPIQmuzMBuKdWeF4+a2sjSSpBK0iqitSQ+5BM9KhpexuGt20JpTVM7u5BDZngncgrqDMbWdxMWWOGtZ9UgbqgZE="
echo "bitbucket.org ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIazEu89wgQZ4bqs3d63QSMzYVa0MuJ2e2gKTKqu+UUO"
}
known-hosts-github() {
# https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/githubs-ssh-key-fingerprints
echo "github.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl"
echo "github.com ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEmKSENjQEezOmxkZMy7opKgwFB9nkt5YRrYMjNuG5N87uRgg6CLrbo5wAdT/y6v0mKV0U2w0WZ2YB/++Tpockg="
echo "github.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCj7ndNxQowgcQnjshcLrqPEiiphnt+VTTvDP6mHBL9j1aNUkY4Ue1gvwnGLVlOhGeYrnZaMgRK6+PKCUXaDbC7qtbW8gIkhL7aGCsOr/C56SJMy/BCZfxd1nWzAOxSDPgVsmerOBYfNqltV9/hWCqBywINIR+5dIg6JTJ72pcEpEjcYgXkE2YEFXV1JHnsKgbLWNlhScqb2UmyRkQyytRLtL+38TGxkxCflmO+5Z8CSSNY7GidjMIZ7Q4zMjA2n1nGrlTDkzwDCsw+wqFPGQA179cnfGWOWRVruj16z6XyvxvjJwbz0wQZ75XK5tKSb7FNyeIEs4TT4jk+S4dhPeAUC5y+bDYirYgM4GC7uEnztnZyaVWQ7B381AK4Qdrwt51ZqExKbQpTUNn+EjqoTwvqNj4kqx5QUCI0ThS/YkOxJCXmPUWZbhjpCg56i+2aB6CmK2JGhn57K5mj0MNdBXA4/WnwH6XoPWJzK5Nyu2zB3nAZp+S5hpQs+p1vN1/wsjk="
}
setup-known-hosts() {
local hosts="$HOME/.ssh/known_hosts"
if [ ! -f "$hosts" ]; then
touch "$hosts"
chmod 644 "$hosts"
echo "Created $hosts with permissions 644."
known-hosts-github >>"$hosts"
echo "Added github.com public keys to $hosts"
known-hosts-bitbucket >>"$hosts"
echo "Added bitbucket.org public keys to $hosts"
else
echo "$hosts already exists, skipping."
fi
}
bash
known-hosts-bitbucket() {
# https://bitbucket.org/site/ssh
echo "bitbucket.org ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDQeJzhupRu0u0cdegZIa8e86EG2qOCsIsD1Xw0xSeiPDlCr7kq97NLmMbpKTX6Esc30NuoqEEHCuc7yWtwp8dI76EEEB1VqY9QJq6vk+aySyboD5QF61I/1WeTwu+deCbgKMGbUijeXhtfbxSxm6JwGrXrhBdofTsbKRUsrN1WoNgUa8uqN1Vx6WAJw1JHPhglEGGHea6QICwJOAr/6mrui/oB7pkaWKHj3z7d1IC4KWLtY47elvjbaTlkN04Kc/5LFEirorGYVbt15kAUlqGM65pk6ZBxtaO3+30LVlORZkxOh+LKL/BvbZ/iRNhItLqNyieoQj/uh/7Iv4uyH/cV/0b4WDSd3DptigWq84lJubb9t/DnZlrJazxyDCulTmKdOR7vs9gMTo+uoIrPSb8ScTtvw65+odKAlBj59dhnVp9zd7QUojOpXlL62Aw56U4oO+FALuevvMjiWeavKhJqlR7i5n9srYcrNV7ttmDw7kf/97P5zauIhxcjX+xHv4M="
echo "bitbucket.org ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBPIQmuzMBuKdWeF4+a2sjSSpBK0iqitSQ+5BM9KhpexuGt20JpTVM7u5BDZngncgrqDMbWdxMWWOGtZ9UgbqgZE="
echo "bitbucket.org ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIazEu89wgQZ4bqs3d63QSMzYVa0MuJ2e2gKTKqu+UUO"
}
known-hosts-github() {
# https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/githubs-ssh-key-fingerprints
echo "github.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl"
echo "github.com ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEmKSENjQEezOmxkZMy7opKgwFB9nkt5YRrYMjNuG5N87uRgg6CLrbo5wAdT/y6v0mKV0U2w0WZ2YB/++Tpockg="
echo "github.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCj7ndNxQowgcQnjshcLrqPEiiphnt+VTTvDP6mHBL9j1aNUkY4Ue1gvwnGLVlOhGeYrnZaMgRK6+PKCUXaDbC7qtbW8gIkhL7aGCsOr/C56SJMy/BCZfxd1nWzAOxSDPgVsmerOBYfNqltV9/hWCqBywINIR+5dIg6JTJ72pcEpEjcYgXkE2YEFXV1JHnsKgbLWNlhScqb2UmyRkQyytRLtL+38TGxkxCflmO+5Z8CSSNY7GidjMIZ7Q4zMjA2n1nGrlTDkzwDCsw+wqFPGQA179cnfGWOWRVruj16z6XyvxvjJwbz0wQZ75XK5tKSb7FNyeIEs4TT4jk+S4dhPeAUC5y+bDYirYgM4GC7uEnztnZyaVWQ7B381AK4Qdrwt51ZqExKbQpTUNn+EjqoTwvqNj4kqx5QUCI0ThS/YkOxJCXmPUWZbhjpCg56i+2aB6CmK2JGhn57K5mj0MNdBXA4/WnwH6XoPWJzK5Nyu2zB3nAZp+S5hpQs+p1vN1/wsjk="
}
setup-known-hosts() {
local hosts="$HOME/.ssh/known_hosts"
if [ ! -f "$hosts" ]; then
touch "$hosts"
chmod 644 "$hosts"
echo "Created $hosts with permissions 644."
known-hosts-github >>"$hosts"
echo "Added github.com public keys to $hosts"
known-hosts-bitbucket >>"$hosts"
echo "Added bitbucket.org public keys to $hosts"
else
echo "$hosts already exists, skipping."
fi
}

The idea behind known_hosts is pretty simple. The first time you connect to some host on the internet, the SSH client asks you to confirm its fingerprint. After all - how can you be sure that the host you are trying to connect to is not some hacker doing Man in the Middle attack? To verify this SSH prints short signature and all you need to do is compare it to the actual Github fingerprint which you shall obtain through some other trusted medium, i.e web browser.

For instance Github publishes their servers’ fingerprints here.

Once we have all SSH related functions in place, let’s wire them together:

bash
setup-default-ssh() {
local keypath="$HOME/.ssh"
read -rp "Enter your new SSH keyname (ed25519): " keyname
[ -z "$keyname" ] && keyname="ed25519"
generate-new-ssh-key "${keyname}" "${keypath}"
add-ssh-key-to-agent "${keyname}" "${keypath}"
add-ssh-host-config-entry "${keyname}" "${keypath}" "github.com"
add-ssh-host-config-entry "${keyname}" "${keypath}" "bitbucket.org"
setup-known-hosts
if which pbcopy &>/dev/null; then
pbcopy <"${keypath}/${keyname}.pub"
echo "${keypath}/${keyname}.pub" key content copied to clipboard!
fi
}
bash
setup-default-ssh() {
local keypath="$HOME/.ssh"
read -rp "Enter your new SSH keyname (ed25519): " keyname
[ -z "$keyname" ] && keyname="ed25519"
generate-new-ssh-key "${keyname}" "${keypath}"
add-ssh-key-to-agent "${keyname}" "${keypath}"
add-ssh-host-config-entry "${keyname}" "${keypath}" "github.com"
add-ssh-host-config-entry "${keyname}" "${keypath}" "bitbucket.org"
setup-known-hosts
if which pbcopy &>/dev/null; then
pbcopy <"${keypath}/${keyname}.pub"
echo "${keypath}/${keyname}.pub" key content copied to clipboard!
fi
}

and finally some sane settings:

bash
setup-settings() {
# https://macos-defaults.com/
(
set -o xtrace
defaults write com.apple.dock tilesize -int "50"
defaults write com.apple.dock autohide -bool "true"
defaults write com.apple.dock autohide-delay -float "0.1"
defaults write com.apple.dock show-recents -bool "false"
defaults write com.apple.finder AppleShowAllFiles -bool "false"
defaults write com.apple.finder ShowPathbar -bool "true"
defaults write com.apple.finder FXPreferredViewStyle -string "Nlsv"
defaults write com.apple.finder FXRemoveOldTrashItems -bool "true"
defaults write com.apple.finder ShowHardDrivesOnDesktop -bool "false"
defaults write com.apple.ActivityMonitor UpdatePeriod -int "2"
defaults write NSGlobalDomain ApplePressAndHoldEnabled -bool "false"
)
}
bash
setup-settings() {
# https://macos-defaults.com/
(
set -o xtrace
defaults write com.apple.dock tilesize -int "50"
defaults write com.apple.dock autohide -bool "true"
defaults write com.apple.dock autohide-delay -float "0.1"
defaults write com.apple.dock show-recents -bool "false"
defaults write com.apple.finder AppleShowAllFiles -bool "false"
defaults write com.apple.finder ShowPathbar -bool "true"
defaults write com.apple.finder FXPreferredViewStyle -string "Nlsv"
defaults write com.apple.finder FXRemoveOldTrashItems -bool "true"
defaults write com.apple.finder ShowHardDrivesOnDesktop -bool "false"
defaults write com.apple.ActivityMonitor UpdatePeriod -int "2"
defaults write NSGlobalDomain ApplePressAndHoldEnabled -bool "false"
)
}

Final thoughts

And here you have it, building blocks for your own bootstrap script. No Ansible, no Python, just your plain old bash script easy to debug and customize.

To make the experience even more pleasant to use I added help message and separate subcommands. You can find my setup script in all it’s glory here.

Now, whenever I need to quickly setup new environment I go to github.com/psmolak/macup, download the script somewhere on the disk, run chmod +x ./macup && ./macup and voilà!