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" ]thenecho '[ -e /opt/homebrew/bin/brew ] && eval "$(/opt/homebrew/bin/brew shellenv)"' >> ~/.zprofilefi}
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" ]thenecho '[ -e /opt/homebrew/bin/brew ] && eval "$(/opt/homebrew/bin/brew shellenv)"' >> ~/.zprofilefi}
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 sessioneval "$(/opt/homebrew/bin/brew shellenv)"# Tapsbrew tap 'homebrew/cask-fonts'brew update# Personalbrew install --HEAD psmolak/tap/bin# Fontsbrew install 'font-ubuntu'brew install 'font-ubuntu-condensed'brew install 'font-ubuntu-mono'brew install 'font-fira-code'brew install 'font-jetbrains-mono'# Formulasbrew 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# Casksbrew 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 pluginsetup-custom-css-vscode-file-perms}setup-custom-css-vscode-file-perms() {# https://github.com/be5invis/vscode-custom-csssudo chown -R "$(whoami)" "$(which code)"if [ -d "/usr/share/code" ]; thensudo chown -R "$(whoami)" /usr/share/codefi}
bash
install-packages() {# in case brew is installed for the first time in the current shell sessioneval "$(/opt/homebrew/bin/brew shellenv)"# Tapsbrew tap 'homebrew/cask-fonts'brew update# Personalbrew install --HEAD psmolak/tap/bin# Fontsbrew install 'font-ubuntu'brew install 'font-ubuntu-condensed'brew install 'font-ubuntu-mono'brew install 'font-fira-code'brew install 'font-jetbrains-mono'# Formulasbrew 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# Casksbrew 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 pluginsetup-custom-css-vscode-file-perms}setup-custom-css-vscode-file-perms() {# https://github.com/be5invis/vscode-custom-csssudo chown -R "$(whoami)" "$(which code)"if [ -d "/usr/share/code" ]; thensudo chown -R "$(whoami)" /usr/share/codefi}
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: " commentif [ -z "$comment" ]; thenreturn 1fiif [ ! -d "$keypath" ]; thenmkdir -p "$keypath"chmod 700 "$keypath"fissh-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: " commentif [ -z "$comment" ]; thenreturn 1fiif [ ! -d "$keypath" ]; thenmkdir -p "$keypath"chmod 700 "$keypath"fissh-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" ]; thenecho "No config $config found. Creating $config with permissions 664."touch "$config"chmod 644 "$config"fiif ! 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"elseecho "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" ]; thenecho "No config $config found. Creating $config with permissions 664."touch "$config"chmod 644 "$config"fiif ! 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"elseecho "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/sshecho "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-fingerprintsecho "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" ]; thentouch "$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"elseecho "$hosts already exists, skipping."fi}
bash
known-hosts-bitbucket() {# https://bitbucket.org/site/sshecho "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-fingerprintsecho "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" ]; thentouch "$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"elseecho "$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-hostsif which pbcopy &>/dev/null; thenpbcopy <"${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-hostsif which pbcopy &>/dev/null; thenpbcopy <"${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 xtracedefaults 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 xtracedefaults 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à!