Last active
December 15, 2018 21:53
-
-
Save sams/69daa7dad7b29dde84b8087df815f04b to your computer and use it in GitHub Desktop.
dotfile install
This file contains 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
#!/usr/bin/env bash | |
[[ "$1" == "source" ]] || \ | |
echo 'Dotfiles - "sams" Sam Sherlock - http://samsherlock.com/' | |
if [[ "$1" == "-h" || "$1" == "--help" ]]; then cat <<HELP | |
Usage: $(basename "$0") | |
See the README for documentation. | |
https://github.com/sams/dotfiles | |
Copyright (c) 2018 "sams" Sam Sherlock | |
Licensed under the MIT license. | |
http://samsherlock.com/about/license/ | |
HELP | |
exit; fi | |
########################################### | |
# GENERAL PURPOSE EXPORTED VARS / FUNCTIONS | |
########################################### | |
# Where the magic happens. | |
export DOTFILES=~/.dotfiles | |
# Logging stuff. | |
function e_header() { echo -e "\n\033[1m$@\033[0m"; } | |
function e_success() { echo -e " \033[1;32m✔\033[0m $@"; } | |
function e_error() { echo -e " \033[1;31m✖\033[0m $@"; } | |
function e_arrow() { echo -e " \033[1;34m➜\033[0m $@"; } | |
# For testing some of these functions. | |
function assert() { | |
local success modes equals result | |
modes=(e_error e_success); equals=("!=" "=="); | |
[[ "$1" == "$2" ]] && success=1 || success=0 | |
if [[ "$(echo "$1" | wc -l)" != 1 || "$(echo "$2" | wc -l)" != 1 ]]; then | |
result="$(diff <(echo "$1") <(echo "$2") | sed ' | |
s/^\([^<>-].*\)/===[\1]====================/ | |
s/^\([<>].*\)/\1|/ | |
s/^< /actual |/ | |
s/^> /expected |/ | |
2,$s/^/ / | |
')" | |
[[ ! "$result" ]] && result="(multiline comparison)" | |
else | |
result="\"$1\" ${equals[success]} \"$2\"" | |
fi | |
${modes[success]} "$result" | |
} | |
# Test if the dotfiles script is currently | |
function is_dotfiles_running() { | |
[[ "$DOTFILES_SCRIPT_RUNNING" ]] || return 1 | |
} | |
# Test if this script was run via the "dotfiles" bin script (vs. via curl/wget) | |
function is_dotfiles_bin() { | |
[[ "$(basename $0 2>/dev/null)" == dotfiles ]] || return 1 | |
} | |
# OS detection | |
function is_osx() { | |
[[ "$OSTYPE" =~ ^darwin ]] || return 1 | |
} | |
function is_ubuntu() { | |
[[ "$(cat /etc/issue 2> /dev/null)" =~ Ubuntu ]] || return 1 | |
} | |
function is_ubuntu_desktop() { | |
dpkg -l ubuntu-desktop >/dev/null 2>&1 || return 1 | |
} | |
function get_os() { | |
for os in osx ubuntu ubuntu_desktop; do | |
is_$os; [[ $? == ${1:-0} ]] && echo $os | |
done | |
} | |
# Remove an entry from $PATH | |
# Based on http://stackoverflow.com/a/2108540/142339 | |
function path_remove() { | |
local arg path | |
path=":$PATH:" | |
for arg in "$@"; do path="${path//:$arg:/:}"; done | |
path="${path%:}" | |
path="${path#:}" | |
echo "$path" | |
} | |
# Display a fancy multi-select menu. | |
# Inspired by http://serverfault.com/a/298312 | |
function prompt_menu() { | |
local exitcode prompt choices nums i n | |
exitcode=0 | |
if [[ "$2" ]]; then | |
_prompt_menu_draws "$1" | |
read -t $2 -n 1 -sp "Press ENTER or wait $2 seconds to continue, or press any other key to edit." | |
exitcode=$? | |
echo "" | |
fi 1>&2 | |
if [[ "$exitcode" == 0 && "$REPLY" ]]; then | |
prompt="Toggle options (Separate options with spaces, ENTER when done): " | |
while _prompt_menu_draws "$1" 1 && read -rp "$prompt" nums && [[ "$nums" ]]; do | |
_prompt_menu_adds $nums | |
done | |
fi 1>&2 | |
_prompt_menu_adds | |
} | |
function _prompt_menu_iter() { | |
local i sel state | |
local fn=$1; shift | |
for i in "${!menu_options[@]}"; do | |
state=0 | |
for sel in "${menu_selects[@]}"; do | |
[[ "$sel" == "${menu_options[i]}" ]] && state=1 && break | |
done | |
$fn $state $i "$@" | |
done | |
} | |
function _prompt_menu_draws() { | |
e_header "$1" | |
_prompt_menu_iter _prompt_menu_draw "$2" | |
} | |
function _prompt_menu_draw() { | |
local modes=(error success) | |
if [[ "$3" ]]; then | |
e_${modes[$1]} "$(printf "%2d) %s\n" $(($2+1)) "${menu_options[$2]}")" | |
else | |
e_${modes[$1]} "${menu_options[$2]}" | |
fi | |
} | |
function _prompt_menu_adds() { | |
_prompt_menu_result=() | |
_prompt_menu_iter _prompt_menu_add "$@" | |
menu_selects=("${_prompt_menu_result[@]}") | |
} | |
function _prompt_menu_add() { | |
local state i n keep match | |
state=$1; shift | |
i=$1; shift | |
for n in "$@"; do | |
if [[ $n =~ ^[0-9]+$ ]] && (( n-1 == i )); then | |
match=1; [[ "$state" == 0 ]] && keep=1 | |
fi | |
done | |
[[ ! "$match" && "$state" == 1 || "$keep" ]] || return | |
_prompt_menu_result=("${_prompt_menu_result[@]}" "${menu_options[i]}") | |
} | |
# Array mapper. Calls map_fn for each item ($1) and index ($2) in array, and | |
# prints whatever map_fn prints. If map_fn is omitted, all input array items | |
# are printed. | |
# Usage: array_map array_name [map_fn] | |
function array_map() { | |
local __i__ __val__ __arr__=$1; shift | |
for __i__ in $(eval echo "\${!$__arr__[@]}"); do | |
__val__="$(eval echo "\"\${$__arr__[__i__]}\"")" | |
if [[ "$1" ]]; then | |
"$@" "$__val__" $__i__ | |
else | |
echo "$__val__" | |
fi | |
done | |
} | |
# Print bash array in the format "i <val>" (one per line) for debugging. | |
function array_print() { array_map $1 __array_print; } | |
function __array_print() { echo "$2 <$1>"; } | |
# Array filter. Calls filter_fn for each item ($1) and index ($2) in array_name | |
# array, and prints all values for which filter_fn returns a non-zero exit code | |
# to stdout. If filter_fn is omitted, input array items that are empty strings | |
# will be removed. | |
# Usage: array_filter array_name [filter_fn] | |
# Eg. mapfile filtered_arr < <(array_filter source_arr) | |
function array_filter() { __array_filter 1 "$@"; } | |
# Works like array_filter, but outputs array indices instead of array items. | |
function array_filter_i() { __array_filter 0 "$@"; } | |
# The core function. Wheeeee. | |
function __array_filter() { | |
local __i__ __val__ __mode__ __arr__ | |
__mode__=$1; shift; __arr__=$1; shift | |
for __i__ in $(eval echo "\${!$__arr__[@]}"); do | |
__val__="$(eval echo "\${$__arr__[__i__]}")" | |
if [[ "$1" ]]; then | |
"$@" "$__val__" $__i__ >/dev/null | |
else | |
[[ "$__val__" ]] | |
fi | |
if [[ "$?" == 0 ]]; then | |
if [[ $__mode__ == 1 ]]; then | |
eval echo "\"\${$__arr__[__i__]}\"" | |
else | |
echo $__i__ | |
fi | |
fi | |
done | |
} | |
# Array join. Joins array ($1) items on string ($2). | |
function array_join() { __array_join 1 "$@"; } | |
# Works like array_join, but removes empty items first. | |
function array_join_filter() { __array_join 0 "$@"; } | |
function __array_join() { | |
local __i__ __val__ __out__ __init__ __mode__ __arr__ | |
__mode__=$1; shift; __arr__=$1; shift | |
for __i__ in $(eval echo "\${!$__arr__[@]}"); do | |
__val__="$(eval echo "\"\${$__arr__[__i__]}\"")" | |
if [[ $__mode__ == 1 || "$__val__" ]]; then | |
[[ "$__init__" ]] && __out__="$__out__$@" | |
__out__="$__out__$__val__" | |
__init__=1 | |
fi | |
done | |
[[ "$__out__" ]] && echo "$__out__" | |
} | |
# Do something n times. | |
function n_times() { | |
local max=$1; shift | |
local i=0; while [[ $i -lt $max ]]; do "$@"; i=$((i+1)); done | |
} | |
# Do something n times, passing along the array index. | |
function n_times_i() { | |
local max=$1; shift | |
local i=0; while [[ $i -lt $max ]]; do "$@" "$i"; i=$((i+1)); done | |
} | |
# Given strings containing space-delimited words A and B, "setdiff A B" will | |
# return all words in A that do not exist in B. Arrays in bash are insane | |
# (and not in a good way). | |
# From http://stackoverflow.com/a/1617303/142339 | |
function setdiff() { | |
local debug skip a b | |
if [[ "$1" == 1 ]]; then debug=1; shift; fi | |
if [[ "$1" ]]; then | |
local setdiff_new setdiff_cur setdiff_out | |
setdiff_new=($1); setdiff_cur=($2) | |
fi | |
setdiff_out=() | |
for a in "${setdiff_new[@]}"; do | |
skip= | |
for b in "${setdiff_cur[@]}"; do | |
[[ "$a" == "$b" ]] && skip=1 && break | |
done | |
[[ "$skip" ]] || setdiff_out=("${setdiff_out[@]}" "$a") | |
done | |
[[ "$debug" ]] && for a in setdiff_new setdiff_cur setdiff_out; do | |
echo "$a ($(eval echo "\${#$a[*]}")) $(eval echo "\${$a[*]}")" 1>&2 | |
done | |
[[ "$1" ]] && echo "${setdiff_out[@]}" | |
} | |
# If this file was being sourced, exit now. | |
[[ "$1" == "source" ]] && return | |
########################################### | |
# INTERNAL DOTFILES "INIT" VARS / FUNCTIONS | |
########################################### | |
DOTFILES_SCRIPT_RUNNING=1 | |
function cleanup { | |
unset DOTFILES_SCRIPT_RUNNING | |
} | |
trap cleanup EXIT | |
# Initialize. | |
init_file=$DOTFILES/caches/init/selected | |
function init_files() { | |
local i f dirname oses os opt remove | |
dirname="$(dirname "$1")" | |
f=("$@") | |
menu_options=(); menu_selects=() | |
for i in "${!f[@]}"; do menu_options[i]="$(basename "${f[i]}")"; done | |
if [[ -e "$init_file" ]]; then | |
# Read cache file if possible | |
IFS=$'\n' read -d '' -r -a menu_selects < "$init_file" | |
else | |
# Otherwise default to all scripts not specifically for other OSes | |
oses=($(get_os 1)) | |
for opt in "${menu_options[@]}"; do | |
remove= | |
for os in "${oses[@]}"; do | |
[[ "$opt" =~ (^|[^a-z])$os($|[^a-z]) ]] && remove=1 && break | |
done | |
[[ "$remove" ]] || menu_selects=("${menu_selects[@]}" "$opt") | |
done | |
fi | |
prompt_menu "Run the following init scripts?" $prompt_delay | |
# Write out cache file for future reading. | |
rm "$init_file" 2>/dev/null | |
for i in "${!menu_selects[@]}"; do | |
echo "${menu_selects[i]}" >> "$init_file" | |
echo "$dirname/${menu_selects[i]}" | |
done | |
} | |
function init_do() { | |
e_header "Sourcing $(basename "$2")" | |
source "$2" | |
} | |
# Copy files. | |
function copy_header() { e_header "Copying files into home directory"; } | |
function copy_test() { | |
if [[ -e "$2" && ! "$(cmp "$1" "$2" 2> /dev/null)" ]]; then | |
echo "same file" | |
elif [[ "$1" -ot "$2" ]]; then | |
echo "destination file newer" | |
fi | |
} | |
function copy_do() { | |
e_success "Copying ~/$1." | |
cp "$2" ~/ | |
} | |
# Link files. | |
function link_header() { e_header "Linking files into home directory"; } | |
function link_test() { | |
[[ "$1" -ef "$2" ]] && echo "same file" | |
} | |
function link_do() { | |
e_success "Linking ~/$1." | |
ln -sf ${2#$HOME/} ~/ | |
} | |
# Link config files. | |
function config_header() { e_header "Linking files into ~/.config directory"; } | |
function config_dest() { | |
echo "$HOME/.config/$base" | |
} | |
function config_test() { | |
[[ "$1" -ef "$2" ]] && echo "same file" | |
} | |
function config_do() { | |
e_success "Linking ~/.config/$1." | |
ln -sf ../${2#$HOME/} ~/.config/ | |
} | |
# Copy, link, init, etc. | |
function do_stuff() { | |
local base dest skip | |
local files=($DOTFILES/$1/*) | |
[[ $(declare -f "$1_files") ]] && files=($($1_files "${files[@]}")) | |
# No files? abort. | |
if (( ${#files[@]} == 0 )); then return; fi | |
# Run _header function only if declared. | |
[[ $(declare -f "$1_header") ]] && "$1_header" | |
# Iterate over files. | |
for file in "${files[@]}"; do | |
base="$(basename $file)" | |
# Get dest path. | |
if [[ $(declare -f "$1_dest") ]]; then | |
dest="$("$1_dest" "$base")" | |
else | |
dest="$HOME/$base" | |
fi | |
# Run _test function only if declared. | |
if [[ $(declare -f "$1_test") ]]; then | |
# If _test function returns a string, skip file and print that message. | |
skip="$("$1_test" "$file" "$dest")" | |
if [[ "$skip" ]]; then | |
e_error "Skipping ~/$base, $skip." | |
continue | |
fi | |
# Destination file already exists in ~/. Back it up! | |
if [[ -e "$dest" ]]; then | |
e_arrow "Backing up ~/$base." | |
# Set backup flag, so a nice message can be shown at the end. | |
backup=1 | |
# Create backup dir if it doesn't already exist. | |
[[ -e "$backup_dir" ]] || mkdir -p "$backup_dir" | |
# Backup file / link / whatever. | |
mv "$dest" "$backup_dir" | |
fi | |
fi | |
# Do stuff. | |
"$1_do" "$base" "$file" | |
done | |
} | |
# Enough with the functions, let's do stuff. | |
# Set the prompt delay to be longer for the very first run. | |
export prompt_delay=5; is_dotfiles_bin || prompt_delay=15 | |
# Keep-alive: update existing sudo time stamp if set, otherwise do nothing. | |
# Note that this doesn't work with Homebrew, since brew explicitly invalidates | |
# the sudo timestamp, which is probably wise. | |
# See https://gist.github.com/sams/3118588 | |
while true; do sudo -n true; sleep 10; kill -0 "$$" || exit; done 2>/dev/null & | |
# Install a custom sudoers file that allows "sudo apt-get" to be executed | |
# without asking for a password. | |
sudoers_file=/etc/sudoers.d/sams-dotfiles | |
# Contents of the sudoers file. | |
function sudoers_text() { | |
cat <<EOF | |
# This file was created by sams's dotfiles script on $(date -I) | |
# (which will never update it, only recreate it if it's missing) | |
# Sudoers reference: http://ubuntuforums.org/showthread.php?t=1132821 | |
# Command aliases. | |
Cmnd_Alias APT = /usr/bin/apt-get | |
# Members of the sudo and admin groups can run these commands without password. | |
%sudo ALL=(ALL) ALL, NOPASSWD:APT | |
%admin ALL=(ALL) ALL, NOPASSWD:APT | |
EOF | |
} | |
# Bash commands to update the sudoers file. | |
function sudoers_code() { | |
cat <<EOF | |
echo "$(sudoers_text)" > $sudoers_file | |
chmod 0440 $sudoers_file | |
if visudo -c; then | |
echo; echo "Sudoers file created." | |
else | |
rm $sudoers_file | |
echo; echo "Unable to create sudoers file." | |
fi | |
EOF | |
} | |
# Offer to create the sudoers file if it doesn't exist. | |
if is_ubuntu && [[ ! -e $sudoers_file ]]; then | |
cat <<EOF | |
The sudoers file can be updated to allow "sudo apt-get" to be executed | |
without asking for a password. You can verify that this worked correctly by | |
running "sudo -k apt-get". If it doesn't ask for a password, and the output | |
looks normal, it worked. | |
THIS SHOULD ONLY BE ATTEMPTED IF YOU ARE LOGGED IN AS ROOT IN ANOTHER SHELL. | |
This will be skipped if "Y" isn't pressed within the next $prompt_delay seconds. | |
EOF | |
read -N 1 -t $prompt_delay -p "Update sudoers file? [y/N] " update_sudoers; echo | |
if [[ "$update_sudoers" =~ [Yy] ]]; then | |
e_header "Creating sudoers file" | |
sudo bash -c "$(sudoers_code)" | |
else | |
echo "Skipping." | |
fi | |
fi | |
# Ensure that we can actually, like, compile anything. | |
if [[ ! "$(type -P gcc)" ]] && is_osx; then | |
e_error "XCode or the Command Line Tools for XCode must be installed first." | |
exit 1 | |
fi | |
# If Git is not installed, install it (Ubuntu only, since Git comes standard | |
# with recent XCode or CLT) | |
if [[ ! "$(type -P git)" ]] && is_ubuntu; then | |
e_header "Installing Git" | |
sudo apt-get -qq install git-core | |
fi | |
# If Git isn't installed by now, something exploded. We gots to quit! | |
if [[ ! "$(type -P git)" ]]; then | |
e_error "Git should be installed. It isn't. Aborting." | |
exit 1 | |
fi | |
# Initialize. | |
if [[ ! -d $DOTFILES ]]; then | |
# Dotfiles directory doesn't exist? Clone it! | |
e_header "Downloading dotfiles" | |
git clone --branch ${DOTFILES_GH_BRANCH:-master} --recursive \ | |
git://github.com/${DOTFILES_GH_USER:-sams}/dotfiles.git $DOTFILES | |
cd $DOTFILES | |
elif [[ "$1" != "restart" ]]; then | |
# Make sure we have the latest files. | |
e_header "Updating dotfiles" | |
cd $DOTFILES | |
prev_head="$(git rev-parse HEAD)" | |
git pull | |
git submodule update --init --recursive --quiet | |
if [[ "$(git rev-parse HEAD)" != "$prev_head" ]]; then | |
if is_dotfiles_bin; then | |
e_header "Changes detected, restarting script" | |
exec "$0" restart | |
else | |
e_header "Changes detected, please re-run script" | |
exit | |
fi | |
fi | |
fi | |
# Add binaries into the path | |
[[ -d $DOTFILES/bin ]] && export PATH=$DOTFILES/bin:$PATH | |
# Tweak file globbing. | |
shopt -s dotglob | |
shopt -s nullglob | |
# Create caches dir and init subdir, if they don't already exist. | |
mkdir -p "$DOTFILES/caches/init" | |
# If backups are needed, this is where they'll go. | |
backup_dir="$DOTFILES/backups/$(date "+%Y_%m_%d-%H_%M_%S")/" | |
backup= | |
# Execute code for each file in these subdirectories. | |
do_stuff copy | |
do_stuff link | |
do_stuff config | |
do_stuff init | |
# Alert if backups were made. | |
if [[ "$backup" ]]; then | |
echo -e "\nBackups were moved to ~/${backup_dir#$HOME/}" | |
fi | |
# All done! | |
e_header "All done!" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment