#!/usr/bin/env bash set -o errexit set -o pipefail set -o nounset SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" \ || exit 1 readonly SCRIPT_DIR readonly PROFILES_DIR="${SCRIPT_DIR}/profiles" readonly -a STOW_ARGS=("--no-folding" "--adopt" "--dir" "${PROFILES_DIR}" "--target" "${HOME}") readonly SCRIPT_NAME="${0##*/}" print_opt() { local -r option="$1" local -r description="$2" printf " %-22s %s\n" "${option}" "${description}" } shell_quote() { local -r string="$1" printf "'%s'" "${string//'/'\\''}" } array_contains() { local -r target_item="$1"; shift local -a -r array=( "$@" ) local item for item in "${array[@]}"; do if [[ ${item} == "${target_item}" ]]; then return 0 fi done return 1 } die() { local -r message="$1" printf "%s: %b\n" "${SCRIPT_NAME}" "${message}" >&2 exit 1 } invalid_option() { local -r option="$1" die "invalid option $(shell_quote "${option}")\nTry '${SCRIPT_NAME} --help' for usage." } check_deps() { if ! command -v stow >/dev/null 2>&1; then cat >&2 <&2 < 0 )); do case "$1" in -h) show_general_help exit 0 ;; --help) if (( $# >= 2 )) && [[ -n $2 ]] && [[ $2 != -* ]]; then show_help_topic "$2" else show_general_help fi exit 0 ;; --help=*) local -r topic="${1#*=}" [[ -z ${topic} ]] && show_general_help && exit 0 show_help_topic "${topic}" exit 0 ;; -l|--list-profiles) list_profiles="true"; shift ;; -P|--prune-symlinks) prune_symlinks="true"; shift ;; -v|--verbose) verbose="true"; shift ;; -p|--profile) ( (( $# < 2 )) || [[ -z "$2" ]] ) && die "--profile requires an argument" profile="$2" shift 2 ;; *) invalid_option "$1" ;; esac done } validate_args() { if [[ ${list_profiles} && ( ${prune_symlinks} || ${verbose} || ${profile} ) ]]; then die "options '--list-profiles' and others cannot be used together" fi if [[ -n ${profile} ]] && ! array_contains "${profile}" "${PROFILES[@]}"; then die "profile $(shell_quote "${profile}") cannot be found" fi } show_general_help() { echo "Usage: ${SCRIPT_NAME} [options]" echo " ${SCRIPT_NAME} -l | --list-profiles" echo " ${SCRIPT_NAME} --help[=TOPIC]" echo echo "Options:" print_opt "-h, --help[=TOPIC]" "show general help or help for a specific topic" print_opt "-p, --profile PROFILE" "select profile" print_opt "-v, --verbose" "increase verbosity" print_opt "-P, --prune-symlinks" "remove dangling symlinks in home directory" print_opt "-l, --list-profiles" "list available profiles (cannot be combined with other options)" echo echo "Topics:" print_opt "profile" "explanation of profile structure and usage" echo echo "Profile:" echo " Directory containing dotfiles plus optional parent and hooks/." echo " Used by the installer to group and deploy related configurations." } show_help_topic() { local topic="$1" case "${topic}" in profile) cat </dev/null local common_files local -r target_profile="$1" mapfile -t common_files < <(comm -12 <(find "${HOME}" -type l -printf '%P\n' 2>/dev/null | sort) \ <(find "${PROFILES_DIR}/${target_profile}" -type f -printf '%P\n' | sort)) local target_file for target_file in "${common_files[@]}"; do local -a rm_args=( "--force" ) [[ ${verbose} ]] && rm_args+=( "--verbose" ) rm "${rm_args[@]}" "${target_file}" done popd >/dev/null echo "OK" } stow_profile() { local -r target_profile="$1" local error_log error_log="$(stow "${STOW_ARGS[@]}" "${target_profile}" 2>&1)" if (( $? == 0 )); then echo "Profile ${target_profile} has been stowed" else echo "Failed to create symlinks pointing to dotfiles in profile ${target_profile}" echo "Executed command: stow ${STOW_ARGS[*]} ${target_profile}" fi if [[ -n "${error_log}" ]]; then echo "${error_log}" > /tmp/stow-error.log [[ ${verbose} ]] && echo "${error_log}" echo "Error log has been saved into /tmp/stow-error.log" fi } fetch_parents() { local -r target_profile="$1" if [[ -s "${PROFILES_DIR}/${target_profile}/parent" ]]; then local parent parent=$(cat "${PROFILES_DIR}/${target_profile}/parent") if [[ -s "${PROFILES_DIR}/${parent}/parent" ]]; then fetch_parents "${parent}" fi echo "${parent}" fi } build_deptree() { local -r target_profile="$1" fetch_parents "${target_profile}" echo "${target_profile}" } install_dotfiles() { local target_profile for target_profile in $(build_deptree "${profile}"); do run_pre_hooks "${target_profile}" [[ ${prune_symlinks} ]] && prune_symlinks "${target_profile}" stow_profile "${target_profile}" run_post_hooks "${target_profile}" done } main() { check_deps parse_args "$@" require_profiles_dir (( $# == 0 )) && show_general_help && exit 1 validate_args [[ -n ${list_profiles} ]] && echo "${PROFILES[@]}" && exit 0 install_dotfiles } main "$@"