Last active
August 9, 2025 05:33
-
-
Save gregberns/d38fafa628fe461112f729c20a21529c to your computer and use it in GitHub Desktop.
MacOS Setup
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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