diff --git a/README.md b/README.md index b52d5a53..66472b67 100644 --- a/README.md +++ b/README.md @@ -3,27 +3,69 @@ -These are my dotfiles. They fuel my madness. Designed for MacOS, Arch Linux and the -various debian-based boxes I frequently ssh into. +My dotfiles. Fuel for my madness. Designed for MacOS, Arch Linux and +the various debian-based boxes I frequently ssh into. -My dotfiles are organized into "topics". The `./boostrap` script is used to install or -update them. +It is organized into topics, some with their own READMEs. Check them out for +more targeted information. The `./boostrap` script is used to install and update +them. -+ `bootstrap install [topic1 topic2 ...]`: symlinks topics to `./.enabled.d` and - dotfiles therein to $HOME (then runs $topic/install) -+ `bootstrap update`: updates (enabled) topics that have a $topic/update script -+ `bootstrap clean`: deletes dead symlinks in $HOME -+ Each command has a one-letter shortcut: `./bootstrap i os/macos` +## Bootstrap -Or use one of my recipes: +`bootstrap` is my idempotent dotfile deployment script. This is how it works: -+ `bootstrap install recipes/ganymede` (my laptop) +You pass the directories of "topics" you want to install and it will: + +1. symlink `$topic/bin/*` to `~/.bin/` +2. symlink `$topic/.*` to `~/` _and_ `$topic/.*/*` to `~/.$1/` (i.e. 2-level + symlinking). e.g. `os/arch/.Xresources` will be symlinked to + `~/.Xresources`, and `os/arch/.config/redshift.conf` will be symlinked to + `~/.config/redshift.conf` (`~/.config` is not a symlink). +3. run `$topic/install` the first time +4. run `$topic/update` on consecutive runs +5. symlink `$topic` to `$DOTFILES/.enabled.d/` to keep track of what's enabled. + Running `bootstrap` without arguments will run this process on all + enabled topics in `.enabled.d/`. + +> NOTE: Be sure to add ~/.bin to PATH + +``` +Usage: bootstrap [-iuldyn] [topic1[ topic2[ ...]]] + +Options: + -d Do a dry run (only output commands, no changes) + -i Don't run install scripts + -l Don't symlink dotfiles or bin scripts + -n Don't overwrite conflicts (no prompts + -u Don't run update scripts + -y Overwrite conflicts (no prompts) +``` + +Or write a recipe list (examples in `recipes/`): + ++ `bootstrap $(recipes/ganymede)` (my laptop) + +To update the topics that you've already enabled, simply run `bootstrap`, and it +will update your symlinks and run `$topic/update` wherever they may exist. A few good-to-knows (where $topic is an enabled topic): + zshrc auto-sources `$topic/*.zsh` -+ `$topic/bin` is added to PATH -+ `$topic/.*` (starts with a dot) is symlinked to $HOME ++ `$topic/bin/*` scripts will be symlinked to `~/.bin`. The included bash/zsh + topics add `~/.bin` to `PATH`. + +## Clean + +`clean` will delete broken symlinks in `$HOME`, and can "disable" topics by +removing their symlinks in `.enabled.d/` + +``` +Usage: clean [-du] + +Options: + -d Do a dry run (only output commands, no changes) + -u Uninstall all enabled topics +``` ## Relevant diff --git a/bootstrap b/bootstrap index 13cafe3b..687493c9 100755 --- a/bootstrap +++ b/bootstrap @@ -1,82 +1,128 @@ -#!/bin/bash -export DOTFILES=$(cd $(dirname $0) && pwd -P) -source "$DOTFILES/shell/common.sh" +#!/usr/bin/env bash + +# My (idempotent) dotfile deployment script. +# +# You pass the directories of "topics" to it to install them. It will: +# 1. symlink $topic/bin/* to ~/.bin/ +# 2. symlink $topic/.* => ~/ and $topic/.*/* => ~/.$1/ +# 3. run $topic/install the first time +# 4. run $topic/update on consecutive runs +# +# NOTE: Be sure to add ~/.bin to PATH +# +# Options: +# -i Don't run install scripts +# -u Don't run update scripts +# -l Don't symlink dotfiles or bin scripts +# -d Do a dry run (only output commands, no changes) +# -y Overwrite conflicts (no prompts) +# -n Don't overwrite conflicts (no prompts + set -e +DOTFILES=$(cd $(dirname $0) && pwd -P) +source "$DOTFILES/shell/common.sh" +shopt -s nullglob # -install() { - for topic in $*; - do - local path="$(dirname $0)/$topic" - if [ -d "$path" ]; - then - [ -f "$path/install" ] && . "$path/install" +blue="\033[34m" +magenta="\033[35m" +green="\033[32m" +orange="\033[33m" +grey="\033[36m" +red="\033[31m" +nc="\033[0m" +# - # Link available dotfiles (1 level deep) - for file in "$1"/.[a-zA-Z]* - do - if [ -d "$file" ]; then - for subfile in "$file"/* - do - local base="$HOME/$(basename $file)" - [ ! -d "$base" ] && mkdir -p "$base" - ln -svf "$subfile" "$base" - done - elif [ -f "$file" ]; then - ln -svf "$file" "$HOME" - fi - done +_run() { + local cmd="${@//$DOTFILES\//}" + cmd="${cmd//$HOME/~}" + local clr="" + case $cmd in + *~/.bin/) clr="${magenta}" ;; + ln*) clr="" ;; + mkdir*) clr="${blue}" ;; + *) clr="${orange}" + esac + echo -e " $PREFIX$clr$cmd$nc" + [[ $DRYRUN ]] || { $@ | sed 's/^/ => /'; } +} - # Symlink enabled topic folders - if [ ! -h "$DOTFILES/.enabled.d/$(basename $path)" ] - then - ln -svf "$DOTFILES/$path" "$DOTFILES/.enabled.d" - fi - elif [ -f "$path" ] - then - install $(cat "$path") +_link() { + if [[ -f "$2" || ( -d "$2" && -f "$2/${1##*/}" ) ]]; then + if [[ $yes ]]; then + [[ $yes = 1 ]] && choice=y || choice=n + else + read -n 1 -p "? ${1##*/} exists in $2 -- overwrite? [y/n] " choice + echo fi - done -} - -update() { - for topic in $DOTFILES/.enabled.d/*/ - do - [ -x "$topic/update" ] && . "$topic/update" - done -} - -clean() { - # Temporarily move dotfiles to kill symlinks - mv $DOTFILES /tmp/dotfiles - # Clean up dead symlinks - find -L $HOME -iname '.*' -type l -maxdepth 1 -exec rm -fv {} \; - if [ -d "$HOME/.config" ] - then - find -L $HOME/.config -iname '.*' -type l -maxdepth 1 -exec rm -fv {} \; + case $choice in + y|Y) PREFIX="${red}OVERWRITE:${nc} " _run ln -svf "$1" "$2" + ;; + *) PREFIX="${red}ABORTED:${nc} " DRYRUN=1 _run ln -svf "$1" "$2" + ;; + esac + else + _run ln -svf "$1" "$2" fi - # Move it back - mv /tmp/dotfiles $DOTFILES } -usage() { - echo "Nope" - exit -} - - ## -(( "$#" == 0 )) && usage +install=1 +link=1 +update=1 +yes= +while getopts iuldyn opt; do + case $opt in + d) DRYRUN=1 ;; + i) install= ;; + l) link= ;; + n) yes=0 ;; + u) update= ;; + y) yes=1 ;; + *) >&2 echo "Aborted." + exit 1 ;; + esac +done +shift $((OPTIND-1)) -COMMAND="$1" -shift -case "$COMMAND" in - i|install) install $* ;; - u|update) update ;; - c|clean) clean ;; - r|reinstall) - clean - install $* - ;; - *) usage ;; -esac +# +# [[ -d ~/.bin ]] || _run mkdir -p ~/.bin +PATH="$DOTFILES/.bin:$PATH" +for topic in ${@:-$(find "$DOTFILES/.enabled.d" -maxdepth 1 -type l)}; do + [[ -d $topic ]] || continue + + tpath=$(realpath "$topic") + tname="$(basename $(dirname $tpath))_${tpath##*/}" + echo -e "${green}${tname//_//}${nc}" + # Link binaries + for bin in $tpath/bin/*; do + [[ "$link" && -x "$bin" ]] && _link "$bin" "~/.bin/" + done + + # Link topic to .enabled.d + if [[ -h "$DOTFILES/.enabled.d/$tname" ]]; then + [[ "$update" && -x $tpath/update ]] && _run "$tpath/update" + else + # Register topic as 'enabled' + _run ln -svf "$tpath" "$DOTFILES/.enabled.d/$tname" + + # Run install script + [[ "$install" && -x $tpath/install ]] && _run "$tpath/install" + fi + + # Symlink dotfiles from topic directories (2 levels deep) + [[ "$link" ]] || continue + for file in "$tpath"/.[a-zA-Z]*; do + if [[ -d $file ]]; then + for subfile in "$file"/*; do + base="$HOME/${file##*/}/" + [[ -d $base ]] || _run mkdir -p "$base" + _link "$subfile" "$base" + done + elif [[ -f $file ]]; then + _link "$file" "$HOME/" + fi + done +done + +# vim:set ft=sh: diff --git a/clean b/clean new file mode 100755 index 00000000..16033b00 --- /dev/null +++ b/clean @@ -0,0 +1,37 @@ +#!/usr/bin/env bash + +# A symlink cleanup script. +# +# Usage: clean [-du] +# +# Options: +# -d Do a dry run (only output commands, no changes) +# -u Uninstall all enabled topics + +_run() { [[ $DRYRUN ]] && echo "$@" || $@; } + +uninstall= +while getopts du opt; do + case $opt in + d) DRYRUN=1 ;; + u) uninstall=1 ;; + *) >&2 echo "Invalid argument: $OPTARG" + exit 1 + ;; + esac +done +shift $((OPTIND-1)) + +shopt -s nullglob +links=( $HOME/.* $HOME/.*/* ) +for link in ${links[@]}; do + if [[ -h "$link" && ! -e "$link" ]]; then + _run rm -vf "$link" + fi +done + +if [[ $uninstall ]]; then + _run rm -v "$(cd $(dirname $0) && pwd -P)"/.enabled.d/* +fi + +# vim:set ft=sh: