dotfiles/bin/dotfiles

413 lines
11 KiB
Bash
Executable File

#!/usr/bin/env bash
if [[ "$1" != "source" ]]; then
echo "dctrud's dotfiles"
echo
echo "Adapted from:"
echo ' Dotfiles - "Cowboy" Ben Alman - http://benalman.com/'
fi
if [[ "$1" == "-h" || "$1" == "--help" ]]; then cat <<HELP
Usage: $(basename "$0")
See the README for documentation.
https://github.com/cowboy/dotfiles
Copyright (c) 2014 "Cowboy" Ben Alman
Licensed under the MIT license.
http://benalman.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_linux() {
[[ "$OSTYPE" =~ ^linux ]] || return 1
}
function is_osx() {
[[ "$OSTYPE" =~ ^darwin ]] || return 1
}
function get_os() {
for os in linux osx; 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 nums i n
exitcode=0
if [[ "$2" ]]; then
_prompt_menu_draws "$1"
read -r -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
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
# Ensure that we can actually, like, compile anything.
if [[ ! "$(type -P gcc)" ]]; then
e_error "gcc should be installed. It isn't. Aborting."
exit 1
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
# 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"
# Execute code for each file in these subdirectories.
do_stuff copy
do_stuff link
do_stuff config
do_stuff init
# All done!
e_header "All done!"