Files
dotfiles/install

263 lines
7.5 KiB
Bash
Executable File

#!/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 <<EOF
Error: 'stow' not found in PATH.
GNU Stow is a symlink farm manager used to deploy dotfiles/packages
by creating symbolic links into \${HOME}.
Install it first:
Arch Linux: pacman -S stow
Debian/Ubuntu: apt install stow
Fedora: dnf install stow
Typically, on other Linux distributions you can use their own package
managers. For other systems, you can find in the Internet installation
instructions appropriate to them or build manually GNU Stow from the
source if GNU Stow is not packaged or your system requires it.
Project: https://www.gnu.org/software/stow/
EOF
exit 1
fi
}
require_profiles_dir() {
if [[ ! -d ${PROFILES_DIR} ]]; then
cat >&2 <<EOF
Error: profiles directory not found: ${PROFILES_DIR}
This directory must contain profile subdirectories with dotfiles.
Make sure you:
- run the script from the repository root, or
- create the directory, or
- clone the dotfiles repository correctly.
More information about what is profile can be found using '${SCRIPT_NAME} --help profile'.
EOF
exit 1
fi
mapfile -t PROFILES < <(find "${PROFILES_DIR}" -mindepth 1 -maxdepth 1 -type d -printf '%f\n')
readonly PROFILES
}
parse_args() {
while (( $# > 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 <<EOF
A profile is a directory under 'profiles/' that groups dotfiles to be installed
together using GNU Stow.
Contents:
dotfiles/ files or directories to deploy (e.g. .config)
parent optional text file with parent profile name
hooks/pre/ scripts run before installation
hooks/post/ scripts run after installation
Only executable files inside hooks are executed.
Profiles may inherit from a parent. The installer builds the dependency
tree automatically and installs profiles in the correct order.
Use .stow-local-ignore to prevent linking internal files such as hooks/ and parent.
EOF
;;
*) die "unknown help topic: ${topic}" ;;
esac
}
run_pre_hooks() {
local -r target_profile=$1
for hook in "${PROFILES_DIR}/${target_profile}"/hooks/pre/*; do
if [[ -f ${hook} && -x ${hook} ]]; then
echo "Running pre-install hook ${hook##*/}"
"${hook}"
fi
done
}
run_post_hooks() {
local -r target_profile=$1
for hook in "${PROFILES_DIR}/${target_profile}"/hooks/post/*; do
if [[ -f ${hook} && -x ${hook} ]]; then
echo "Running post-install hook ${hook##*/}"
"${hook}"
fi
done
}
prune_symlinks() {
echo -n "Removing dangling symlinks in home directory... "
pushd "${HOME}" >/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 "$@"