Skip to content

Instantly share code, notes, and snippets.

@gregberns
Last active August 9, 2025 05:33
Show Gist options
  • Select an option

  • Save gregberns/d38fafa628fe461112f729c20a21529c to your computer and use it in GitHub Desktop.

Select an option

Save gregberns/d38fafa628fe461112f729c20a21529c to your computer and use it in GitHub Desktop.
MacOS Setup
#!/bin/bash
# Improved Mac Setup Script - Idempotent and Modular
# This script sets up a new Mac development environment with proper error handling
# and idempotency checks.
#
# # Download
# curl ~~Get gist raw url~~ > setup.sh
# chmod +x ~/setup.sh
# ~/setup.sh
#
# export GIT_USERNAME="Your Name"
# export GIT_EMAIL="[email protected]"
# export GITHUB_ACCOUNT_NAME="your-github-username"
# export MAC_USERNAME="your-username"
# export MAC_MACHINE_NAME="your-machine-name"
#
# ./setup_v2.sh
set -euo pipefail # Exit on error, treat unset variables as error, pipeline fails if any command fails
# ============================================================================
# CONFIGURATION
# ============================================================================
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Logging
LOG_FILE="$HOME/setup_script.log"
exec > >(tee -a "$LOG_FILE") 2>&1
# User configuration (edit these or set as environment variables)
GIT_USERNAME="${GIT_USERNAME:-$(git config --global user.name 2>/dev/null || echo 'Your Name')}"
GIT_EMAIL="${GIT_EMAIL:-$(git config --global user.email 2>/dev/null || echo '[email protected]')}"
GITHUB_ACCOUNT_NAME="${GITHUB_ACCOUNT_NAME:-$(git config --global github.user 2>/dev/null || echo 'your-github-username')}"
MAC_USERNAME="${MAC_USERNAME:-$(whoami)}"
MAC_MACHINE_NAME="${MAC_MACHINE_NAME:-$(hostname | cut -d. -f1)}"
# ============================================================================
# UTILITIES
# ============================================================================
log() {
echo -e "${BLUE}[$(date '+%Y-%m-%d %H:%M:%S')] $1${NC}"
}
log_success() {
echo -e "${GREEN}✓ $1${NC}"
}
log_error() {
echo -e "${RED}✗ $1${NC}" >&2
}
log_warning() {
echo -e "${YELLOW}⚠ $1${NC}"
}
check_command_exists() {
command -v "$1" >/dev/null 2>&1
}
require_command() {
if ! check_command_exists "$1"; then
log_error "Required command '$1' not found. Please install it first."
exit 1
fi
}
run_idempotent() {
local description="$1"
local check_cmd="$2"
local run_cmd="$3"
log "Checking: $description"
if eval "$check_cmd" >/dev/null 2>&1; then
log_success "$description - Already installed/skipped"
return 0
fi
log "Installing: $description"
if eval "$run_cmd"; then
log_success "$description - Installation completed"
return 0
else
log_error "$description - Installation failed"
return 1
fi
}
confirm_action() {
local prompt="$1"
local default="${2:-n}"
read -p "$prompt [$default] > " response
response="${response:-$default}"
[[ "$response" =~ ^[Yy]$ ]]
}
# ============================================================================
# SYSTEM SETUP
# ============================================================================
setup_machine_name() {
if ! confirm_action "Set machine name to '$MAC_MACHINE_NAME'?"; then
log_warning "Skipping machine name setup"
return 0
fi
log "Setting machine name: $MAC_MACHINE_NAME"
sudo scutil --set HostName "$MAC_MACHINE_NAME"
sudo scutil --set LocalHostName "$MAC_MACHINE_NAME"
sudo scutil --set ComputerName "$MAC_MACHINE_NAME"
log "Flushing DNS cache"
dscacheutil -flushcache
log_success "Machine name updated. Restart required to take full effect."
if confirm_action "Restart now?"; then
log "Restarting in 5 seconds..."
sleep 5
sudo shutdown -r now
fi
}
setup_system_settings() {
log "Setting up system preferences..."
# Create screenshots directory
mkdir -p "$HOME/Screenshots"
# System preferences
settings=(
"NSGlobalDomain:NSQuitAlwaysKeepsWindows=false:Disabling system-wide resume"
"NSGlobalDomain:NSDisableAutomaticTermination=true:Disabling automatic termination of inactive apps"
"com.apple.finder:QLEnableTextSelection=true:Allowing text selection in Quick Look"
"NSGlobalDomain:NSNavPanelExpandedStateForSaveMode=true:Expanding the save panel by default"
"NSGlobalDomain:PMPrintingExpandedStateForPrint=true:Expanding print panel"
"com.apple.print.PrintingPrefs:Quit When Finished=true:Automatically quit printer app when done"
"NSGlobalDomain:NSDocumentSaveNewDocumentsToCloud=false:Saving to disk by default"
"NSGlobalDomain:NSAutomaticQuoteSubstitutionEnabled=false:Disabling smart quotes"
"NSGlobalDomain:NSAutomaticDashSubstitutionEnabled=false:Disabling smart dashes"
"NSGlobalDomain:AppleKeyboardUIMode=3:Enabling full keyboard access"
"NSGlobalDomain:ApplePressAndHoldEnabled=false:Disabling press-and-hold for keys"
"NSGlobalDomain:AppleFontSmoothing=2:Enabling subpixel font rendering"
"com.apple.finder:ShowExternalHardDrivesOnDesktop=true:Showing external drives on desktop"
"NSGlobalDomain:AppleShowAllExtensions=true:Showing all filename extensions"
"com.apple.finder:FXEnableExtensionChangeWarning=false:Disabling extension change warning"
"com.apple.finder:FXPreferredViewStyle=Clmv:Using column view in Finder"
"com.apple.finder:DSDontWriteNetworkStores=true:Avoiding .DS_Store on network volumes"
"com.apple.dock:tilesize=36:Setting dock icon size"
"com.apple.dock:expose-animation-duration=0.1:Speeding up Mission Control"
"com.apple.dock:expose-group-by-app=true:Grouping windows by application"
"com.apple.dock:autohide=true:Auto-hiding dock"
"com.apple.dock:autohide-delay=0:Removing dock auto-hide delay"
"com.apple.dock:autohide-time-modifier=0:Instant dock appearance"
"com.apple.mail:AddressesIncludeNameOnPasteboard=false:Email addresses copy format"
"com.apple.terminal:StringEncodings=4:Setting UTF-8 in Terminal"
"com.apple.TimeMachine:DoNotOfferNewDisksForBackup=true:Preventing Time Machine prompts"
"com.apple.screencapture:location=$HOME/Screenshots:Setting screenshots location"
"com.apple.screencapture:type=png:Setting screenshot format"
"com.apple.Safari:ShowFavoritesBar=false:Hiding Safari bookmarks bar"
"com.apple.Safari:ShowSidebarInTopSites=false:Hiding Safari sidebar"
"com.apple.Safari:DebugSnapshotsUpdatePolicy=2:Disabling Safari thumbnail cache"
"com.apple.Safari:IncludeInternalDebugMenu=true:Enabling Safari debug menu"
"com.apple.Safari:FindOnPageMatchesWordStartsOnly=false:Safari search contains mode"
"com.apple.Safari:IncludeDevelopMenu=true:Enabling Safari Develop menu"
"com.apple.Safari:WebKitDeveloperExtrasEnabledPreferenceKey=true:Enabling Web Inspector"
"NSGlobalDomain:WebKitDeveloperExtras=true:Web Inspector context menu"
"com.apple.dock:mru-spaces=false:Don't rearrange Spaces automatically"
)
for setting in "${settings[@]}"; do
local domain=$(echo "$setting" | cut -d: -f1)
local key_value=$(echo "$setting" | cut -d: -f2)
local description=$(echo "$setting" | cut -d: -f3-)
local key=$(echo "$key_value" | cut -d= -f1)
local value=$(echo "$key_value" | cut -d= -f2)
log "Setting: $description"
defaults write "$domain" "$key" "$value" || log_warning "Failed to set $description"
done
# Special handling for some settings
sudo spctl --master-disable 2>/dev/null || log_warning "Failed to disable GateKeeper"
sudo defaults write /var/db/SystemPolicy-prefs.plist enabled -string no 2>/dev/null || log_warning "Failed to disable GateKeeper"
defaults write com.apple.LaunchServices LSQuarantine -bool false 2>/dev/null || log_warning "Failed to disable quarantine"
# Kill affected processes
killall Finder 2>/dev/null || true
killall Dock 2>/dev/null || true
log_success "System preferences configured"
}
# ============================================================================
# DEVELOPMENT TOOLS
# ============================================================================
install_xcode_tools() {
run_idempotent "Xcode Command Line Tools" \
"xcode-select --print-path &>/dev/null" \
"xcode-select --install"
}
install_homebrew() {
run_idempotent "Homebrew" \
"check_command_exists brew" \
'/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"'
}
setup_brew() {
if ! check_command_exists brew; then
log_error "Homebrew not installed. Please run install_homebrew first."
return 1
fi
log "Updating Homebrew..."
brew update
log "Installing essential packages..."
brew install git git-extras tree wget trash cmake fnm jq libpq kubernetes-cli watchman
log "Installing Node.js and npm..."
fnm install --lts
fnm use
# fnm install node
# eval "$(fnm env --use-on-cd --shell zsh)"
# node --version > ~/.node-version
log "Installing Python tools..."
brew install python
log "Installing Git utilities..."
brew install git-extras
# log "Installing additional tools..."
# brew install awscli cocoapods
brew cleanup
log_success "Homebrew packages installed"
}
setup_git() {
log "Configuring Git..."
git config --global user.name "$GIT_USERNAME"
git config --global user.email "$GIT_EMAIL"
if [[ -n "${GITHUB_ACCOUNT_NAME:-}" ]]; then
git config --global github.user "$GITHUB_ACCOUNT_NAME"
fi
git config --global init.defaultBranch main
git config --global core.excludesfile '~/.gitignore'
log_success "Git configured"
}
# ============================================================================
# APPLICATIONS
# ============================================================================
install_brew_apps() {
if ! check_command_exists brew; then
log_error "Homebrew not installed. Please run install_homebrew first."
return 1
fi
log "Installing command-line applications..."
local brew_apps=(
git node npm fnm yarn
python3 pipenv
# awscli kubectl
# helm
coreutils
jq yq fzf ripgrep
bat exa fd
neovim vim
tmux htop
tree wget curl
cmake pkg-config
# libpq
# postgresql
# redis
watchman
# cocoapods
dockutil
)
for app in "${brew_apps[@]}"; do
if brew list "$app" >/dev/null 2>&1; then
log_success "$app - Already installed"
else
log "Installing $app..."
brew install "$app" || log_error "Failed to install $app"
fi
done
brew cleanup
log_success "Brew applications installed"
}
install_cask_apps() {
if ! check_command_exists brew; then
log_error "Homebrew not installed. Please run install_homebrew first."
return 1
fi
log "Installing GUI applications..."
# Install Cask if not already installed
if ! brew list --cask >/dev/null 2>&1; then
brew install caskroom/cask/brew-cask
fi
local cask_apps=(
diffmerge
vscodium
flux
slack
google-chrome
iterm2
textexpander
desktoppr # Set background
# alfred
# bartender
# bettertouchtool
# cleanmymac
# docker
# visual-studio-code
# openvpn-connect
# firefox
# harvest
# licecap
# gitkraken
# spotify
# virtualbox
# vlc
# zoomus
# qlmarkdown
# qlstephen
# suspicious-package
# rectangle
# monitorcontrol
# keepingyouawake
# keycastr
# stats
)
for app in "${cask_apps[@]}"; do
if brew list --cask "$app" >/dev/null 2>&1; then
log_success "$app - Already installed"
else
log "Installing $app..."
brew install --cask "$app" || log_error "Failed to install $app"
fi
done
brew cleanup
log_success "Cask applications installed"
}
# ============================================================================
# SHELL AND DEVELOPMENT ENVIRONMENT
# ============================================================================
setup_ssh() {
local ssh_dir="$HOME/.ssh"
if [[ ! -d "$ssh_dir" ]]; then
log "Creating SSH directory..."
mkdir -p "$ssh_dir"
chmod 700 "$ssh_dir"
fi
if [[ ! -f "$ssh_dir/id_rsa" ]]; then
log "Generating SSH key..."
ssh-keygen -t rsa -b 4096 -f "$ssh_dir/id_rsa" -N ""
ssh-add -K "$ssh_dir/id_rsa" 2>/dev/null || ssh-add "$ssh_dir/id_rsa"
log "Copying public key to clipboard..."
pbcopy < "$ssh_dir/id_rsa.pub"
log_success "SSH public key copied to clipboard"
log "Please add this key to your GitHub account: https://github.com/settings/keys"
else
log_success "SSH key already exists"
fi
}
install_oh_my_zsh() {
run_idempotent "Oh My Zsh" \
"[[ -d ~/.oh-my-zsh ]]" \
"curl -L https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh | sh"
}
setup_zsh_theme() {
local theme_dir="$HOME/.oh-my-zsh/themes"
local theme_file="$theme_dir/brad-muse.zsh-theme"
if [[ ! -f "$theme_file" ]]; then
log "Installing Zsh theme..."
curl -s https://gist.githubusercontent.com/bradp/a52fffd9cad1cd51edb7/raw/cb46de8e4c77beb7fad38c81dbddf531d9875c78/brad-muse.zsh-theme > "$theme_file"
log_success "Zsh theme installed"
else
log_success "Zsh theme already installed"
fi
}
setup_zsh_plugins() {
local plugins_dir="$HOME/.oh-my-zsh/custom/plugins"
if [[ ! -d "$plugins_dir/zsh-syntax-highlighting" ]]; then
log "Installing Zsh syntax highlighting plugin..."
git clone https://github.com/zsh-users/zsh-syntax-highlighting.git "$plugins_dir/zsh-syntax-highlighting"
log_success "Zsh syntax highlighting plugin installed"
else
log_success "Zsh syntax highlighting plugin already installed"
fi
}
setup_zsh_as_default() {
local current_shell=$(dscl . -read /Users/"$MAC_USERNAME" UserShell | awk '{print $2}')
if [[ "$current_shell" != "/bin/zsh" ]]; then
log "Setting Zsh as default shell..."
chsh -s /bin/zsh
log_success "Zsh set as default shell"
else
log_success "Zsh is already the default shell"
fi
}
setup_dotfiles() {
local dotfiles_dir="$HOME/.dotfiles"
if [[ ! -d "$dotfiles_dir" ]]; then
log "Cloning dotfiles..."
if [[ -n "${GITHUB_ACCOUNT_NAME:-}" ]]; then
git clone "[email protected]:$GITHUB_ACCOUNT_NAME/dotfiles.git" "$dotfiles_dir"
else
log_warning "GitHub account name not set, cloning with HTTPS"
git clone "https://github.com/your-github-username/dotfiles.git" "$dotfiles_dir"
fi
if [[ -f "$dotfiles_dir/install.sh" ]]; then
log "Running dotfiles install script..."
cd "$dotfiles_dir"
./install.sh
fi
log_success "Dotfiles setup completed"
else
log_success "Dotfiles already exist"
fi
}
# ============================================================================
# DOCK AND FINDER
# ============================================================================
setup_dock() {
log "Setting up dock..."
# Set background to black
desktoppr color 000000
# Remove all items from dock
dockutil --remove all 2>/dev/null || true
# Add commonly used applications
local dock_apps=(
"/Applications/Google Chrome.app"
"/Applications/VSCodium.app"
"/Applications/iTerm.app"
# "/System/Applications/Utilities/Terminal.app"
)
for app in "${dock_apps[@]}"; do
if [[ -d "$app" ]]; then
dockutil --add "$app" 2>/dev/null || log_warning "Failed to add $app to dock"
fi
done
log_success "Dock configured"
}
# ============================================================================
# MAIN FUNCTIONS
# ============================================================================
setup_directories() {
log "Creating common directories..."
local dirs=(
"$HOME/github"
"$HOME/gitlab"
"$HOME/downloads"
"$HOME/screenshots"
# "$HOME/development"
# "$HOME/documents"
# "$HOME/pictures"
# "$HOME/movies"
# "$HOME/music"
# "$HOME/desktop"
)
for dir in "${dirs[@]}"; do
if [[ ! -d "$dir" ]]; then
mkdir -p "$dir"
log "Created directory: $dir"
fi
done
log_success "Directories created"
}
verify_setup() {
log "Verifying setup..."
local checks=(
"git:Git"
"brew:Homebrew"
"node:Node.js"
"npm:npm"
"python:Python"
"docker:Docker"
"code:Visual Studio Code"
"zsh:Zsh"
)
local all_passed=true
for check in "${checks[@]}"; do
local cmd=$(echo "$check" | cut -d: -f1)
local name=$(echo "$check" | cut -d: -f2)
if check_command_exists "$cmd"; then
log_success "$name - Installed"
else
log_error "$name - Not found"
all_passed=false
fi
done
if [[ "$all_passed" == true ]]; then
log_success "All checks passed!"
else
log_warning "Some checks failed. Please review the output above."
fi
}
# ============================================================================
# MAIN SCRIPT
# ============================================================================
main() {
log "Starting Mac setup script..."
log "Configuration:"
log " Username: $MAC_USERNAME"
log " Machine Name: $MAC_MACHINE_NAME"
log " Git Username: $GIT_USERNAME"
log " Git Email: $GIT_EMAIL"
log " GitHub Account: ${GITHUB_ACCOUNT_NAME:-Not set}"
echo ""
# System setup
if confirm_action "Setup machine name?"; then
setup_machine_name
fi
echo ""
# Development tools
log "=== Setting up Development Tools ==="
install_xcode_tools
install_homebrew
setup_brew
setup_git
setup_ssh
echo ""
# Applications
log "=== Installing Applications ==="
install_brew_apps
install_cask_apps
echo ""
# Development environment
log "=== Setting up Development Environment ==="
install_oh_my_zsh
setup_zsh_theme
setup_zsh_plugins
setup_zsh_as_default
# setup_dotfiles
echo ""
# System configuration
log "=== Configuring System ==="
setup_system_settings
setup_dock
setup_directories
echo ""
# Verification
log "=== Final Verification ==="
verify_setup
echo ""
log_success "Setup completed successfully!"
log "Log file saved to: $LOG_FILE"
if [[ -f "$HOME/.zshrc" ]]; then
log "Please restart your terminal or run 'exec zsh' to start using the new shell."
fi
}
# Run main function
main "$@"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment