modify install script

This commit is contained in:
2026-03-06 14:42:25 +01:00
parent 13b984bd4c
commit 4bde813b8f

147
install
View File

@@ -1,26 +1,16 @@
#!/bin/bash #!/usr/bin/env bash
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" >/dev/null 2>&1 &&
pwd)" pwd)"
PROFILES_DIR="${SCRIPT_DIR}/profiles" readonly script_dir
STOW_ARGS=( readonly profiles_dir="$script_dir/profiles"
readonly -a stow_args=(
"--no-folding" "--no-folding"
"--adopt" "--adopt"
"--dir" "${PROFILES_DIR}" "--dir" "$profiles_dir"
"--target" "${HOME}" "--target" "$HOME"
) )
SCRIPT_NAME="${0##*/}" readonly 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() { shell_quote() {
local -r string="$1" local -r string="$1"
@@ -33,7 +23,7 @@ array_contains() {
local -a -r array=("$@") local -a -r array=("$@")
local item local item
for item in "${array[@]}"; do for item in "${array[@]}"; do
if [[ ${item} == "${target_item}" ]]; then if [[ ${item} == "$target_item" ]]; then
return 0 return 0
fi fi
done done
@@ -42,14 +32,14 @@ array_contains() {
die() { die() {
local -r message="$1" local -r message="$1"
printf "%s: %b\n" "${SCRIPT_NAME}" "${message}" >&2 printf "%s: %b\n" "$script_name" "$message" >&2
exit 1 exit 1
} }
invalid_option() { invalid_option() {
local -r option="$1" local -r option="$1"
die "invalid option $(shell_quote "${option}") die "invalid option $(shell_quote "$option")
Try '${SCRIPT_NAME} --help' for usage." Try '$script_name --help' for usage."
} }
check_deps() { check_deps() {
@@ -76,9 +66,9 @@ EOF
} }
require_profiles_dir() { require_profiles_dir() {
if [[ ! -d "${PROFILES_DIR}" ]]; then if [[ ! -d "$profiles_dir" ]]; then
cat >&2 <<EOF cat >&2 <<EOF
Error: profiles directory not found: ${PROFILES_DIR} Error: profiles directory not found: $profiles_dir
This directory must contain profile subdirectories with dotfiles. This directory must contain profile subdirectories with dotfiles.
@@ -87,11 +77,11 @@ Make sure you:
- create the directory, or - create the directory, or
- clone the dotfiles repository correctly. - clone the dotfiles repository correctly.
More information about what is profile can be found using '${SCRIPT_NAME} --help profile'. More information about what is profile can be found using '$script_name --help profile'.
EOF EOF
exit 1 exit 1
fi fi
mapfile -t PROFILES < <(find "${PROFILES_DIR}" -mindepth 1 -maxdepth 1 \ mapfile -t PROFILES < <(find "$profiles_dir" -mindepth 1 -maxdepth 1 \
-type d -printf '%f\n') -type d -printf '%f\n')
readonly PROFILES readonly PROFILES
} }
@@ -113,24 +103,21 @@ parse_args() {
;; ;;
--help=*) --help=*)
local -r topic="${1#*=}" local -r topic="${1#*=}"
[[ -z ${topic} ]] && show_general_help && exit 0 [[ -z $topic ]] && show_general_help && exit 0
show_help_topic "${topic}" show_help_topic "$topic"
exit 0 exit 0
;; ;;
-l | --list-profiles) -l | --list-profiles)
list_profiles="true" list_profiles="true"
shift shift
;; ;;
-P | --prune-symlinks)
prune_symlinks="true"
shift
;;
-v | --verbose) -v | --verbose)
verbose="true" verbose="true"
shift shift
;; ;;
-p | --profile) -p | --profile)
( (($# < 2)) || [[ -z "$2" ]]) && die "--profile requires an argument" ( (($# < 2)) || [[ -z "$2" ]]) &&
die "--profile requires an argument"
profile="$2" profile="$2"
shift 2 shift 2
;; ;;
@@ -140,43 +127,41 @@ parse_args() {
} }
validate_args() { validate_args() {
if [[ -n "${list_profiles}" ]] && if [[ -n "$list_profiles" ]] &&
[[ -n "${prune_symlinks}" || -n "${verbose}" || -n "${profile}" ]]; then [[ -n "$verbose" || -n "$profile" ]]; then
die "options '--list-profiles' and others cannot be used together" die "options '--list-profiles' and others cannot be used together"
fi fi
if [[ -n "${profile}" ]] && if [[ -n "$profile" ]] &&
! array_contains "${profile}" "${PROFILES[@]}"; then ! array_contains "$profile" "${PROFILES[@]}"; then
die "profile $(shell_quote "${profile}") cannot be found" die "profile $(shell_quote "$profile") cannot be found"
fi fi
} }
show_general_help() { show_general_help() {
echo "Usage: ${SCRIPT_NAME} [options]" cat <<EOF
echo " ${SCRIPT_NAME} -l | --list-profiles" Usage: $script_name [options]
echo " ${SCRIPT_NAME} --help[=TOPIC]" $script_name -l | --list-profiles
echo $script_name --help[=TOPIC]
echo "Options:"
print_opt "-h, --help[=TOPIC]" \ Options:
"show general help or help for a specific topic" -h, --help[=TOPIC] show general help or help for a specific topic
print_opt "-p, --profile PROFILE" "select profile" -p, --profile PROFILE select profile
print_opt "-v, --verbose" "increase verbosity" -v, --verbose increase verbosity
print_opt "-P, --prune-symlinks" \ -l, --list-profiles list available profiles (cannot be combined with other options)
"remove dangling symlinks in home directory"
print_opt "-l, --list-profiles" \ Topics:
"list available profiles (cannot be combined with other options)" profile explanation of profile structure and usage
echo
echo "Topics:" Profile:
print_opt "profile" "explanation of profile structure and usage" Directory containing dotfiles plus optional parent and hooks/.
echo Used by the installer to group and deploy related configurations.
echo "Profile:" EOF
echo " Directory containing dotfiles plus optional parent and hooks/."
echo " Used by the installer to group and deploy related configurations."
} }
show_help_topic() { show_help_topic() {
local topic="$1" local topic="$1"
case "${topic}" in case "$topic" in
profile) profile)
cat <<EOF cat <<EOF
A profile is a directory under 'profiles/' that groups dotfiles to be A profile is a directory under 'profiles/' that groups dotfiles to be
@@ -197,26 +182,26 @@ Use .stow-local-ignore to prevent linking internal files such as hooks/
and parent. and parent.
EOF EOF
;; ;;
*) die "unknown help topic: ${topic}" ;; *) die "unknown help topic: $topic" ;;
esac esac
} }
run_pre_hooks() { run_pre_hooks() {
local -r target_profile="$1" local -r target_profile="$1"
for hook in "${PROFILES_DIR}/${target_profile}"/hooks/pre/*; do for hook in "$profiles_dir/$target_profile"/hooks/pre/*; do
if [[ -f "${hook}" && -x "${hook}" ]]; then if [[ -f "$hook" && -x "$hook" ]]; then
echo "Running pre-install hook ${hook##*/}" echo "Running pre-install hook ${hook##*/}"
"${hook}" "$hook"
fi fi
done done
} }
run_post_hooks() { run_post_hooks() {
local -r target_profile="$1" local -r target_profile="$1"
for hook in "${PROFILES_DIR}/${target_profile}"/hooks/post/*; do for hook in "$profiles_dir/$target_profile"/hooks/post/*; do
if [[ -f "${hook}" && -x "${hook}" ]]; then if [[ -f "$hook" && -x "$hook" ]]; then
echo "Running post-install hook ${hook##*/}" echo "Running post-install hook ${hook##*/}"
"${hook}" "$hook"
fi fi
done done
} }
@@ -229,12 +214,12 @@ prune_symlinks() {
local -r target_profile="$1" local -r target_profile="$1"
mapfile -t common_files < <(comm -12 <(find "${HOME}" -type l \ mapfile -t common_files < <(comm -12 <(find "${HOME}" -type l \
-printf '%P\n' 2>/dev/null | sort) \ -printf '%P\n' 2>/dev/null | sort) \
<(find "${PROFILES_DIR}/${target_profile}" -type f -printf '%P\n' | sort)) <(find "$profiles_dir/$target_profile" -type f -printf '%P\n' | sort))
local target_file local target_file
for target_file in "${common_files[@]}"; do for target_file in "${common_files[@]}"; do
local -a rm_args=("--force") local -a rm_args=("--force")
[[ ${verbose} ]] && rm_args+=("--verbose") [[ ${verbose} ]] && rm_args+=("--verbose")
rm "${rm_args[@]}" -- "${target_file}" rm "${rm_args[@]}" -- "$target_file"
done done
popd >/dev/null || exit 1 popd >/dev/null || exit 1
@@ -243,37 +228,37 @@ prune_symlinks() {
stow_profile() { stow_profile() {
local -r target_profile="$1" local -r target_profile="$1"
if stow "${STOW_ARGS[@]}" "${target_profile}" 2>&1; then if stow "${stow_args[@]}" "$target_profile" 2>&1; then
echo "Profile ${target_profile} has been stowed" echo "Profile $target_profile has been stowed"
else else
echo "Failed to create symlinks pointing to dotfiles" \ echo "Failed to create symlinks pointing to dotfiles" \
"in profile ${target_profile}" "in profile $target_profile"
fi fi
} }
fetch_parents() { fetch_parents() {
local -r target_profile="$1" local -r target_profile="$1"
if [[ -s "${PROFILES_DIR}/${target_profile}/parent" ]]; then if [[ -s "$profiles_dir/$target_profile/parent" ]]; then
local parent local parent
parent="$(cat "${PROFILES_DIR}/${target_profile}/parent")" parent="$(cat "$profiles_dir/$target_profile/parent")"
if [[ -s "${PROFILES_DIR}/${parent}/parent" ]]; then if [[ -s "$profiles_dir/$parent/parent" ]]; then
fetch_parents "${parent}" fetch_parents "$parent"
fi fi
echo "${parent}" echo "$parent"
fi fi
} }
build_deptree() { build_deptree() {
local -r target_profile="$1" local -r target_profile="$1"
fetch_parents "${target_profile}" fetch_parents "$target_profile"
echo "${target_profile}" echo "$target_profile"
} }
install_dotfiles() { install_dotfiles() {
local target_profile local target_profile
for target_profile in $(build_deptree "${profile}"); do for target_profile in $(build_deptree "$profile"); do
run_pre_hooks "${target_profile}" run_pre_hooks "${target_profile}"
[[ -n "${prune_symlinks}" ]] && prune_symlinks "${target_profile}" prune_symlinks "${target_profile}"
stow_profile "${target_profile}" stow_profile "${target_profile}"
run_post_hooks "${target_profile}" run_post_hooks "${target_profile}"
done done
@@ -285,7 +270,7 @@ main() {
require_profiles_dir require_profiles_dir
(($# == 0)) && show_general_help && exit 1 (($# == 0)) && show_general_help && exit 1
validate_args validate_args
[[ -n "${list_profiles}" ]] && echo "${PROFILES[@]}" && exit 0 [[ -n "$list_profiles" ]] && echo "${PROFILES[@]}" && exit 0
install_dotfiles install_dotfiles
} }