#!/usr/bin/env bash SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd) 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 )) && [[ $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 [[ ${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 || exit 1 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 || exit 1 echo "OK" } stow_profile() { local -r target_profile=$1 if stow "${STOW_ARGS[@]}" "${target_profile}" 2>&1; then echo "Profile ${target_profile} has been stowed" else echo "Failed to create symlinks pointing to dotfiles in profile ${target_profile}" 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 [[ ${list_profiles} ]] && echo "${PROFILES[@]}" && exit 0 install_dotfiles } main "$@"