#!/bin/bash SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" PROFILES_DIR="${SCRIPT_DIR}/profiles" STOW_ARGS=( "--no-folding" "--adopt" "--dir" "${PROFILES_DIR}" "--target" "${HOME}" ) SCRIPT_NAME="${0##*/}" readonly SCRIPT_DIR readonly PROFILES_DIR readonly -a STOW_ARGS readonly SCRIPT_NAME 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}") Try '${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 [[ -n "${list_profiles}" ]] && [[ -n "${prune_symlinks}" || -n "${verbose}" || -n "${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 || 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}" [[ -n "${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 "$@"