#!/bin/bash set -ueo pipefail shopt -s nullglob declare -r TARGET_PATH="$HOME" declare -r DOTFILES_ROOT="$PWD" declare -r SUB="$DOTFILES_ROOT/home/user" export SUB declare -r -A TARGETS=( ["bash"]=".config/bash .bashrc .profile" ["zsh"]="%bash .config/zsh .zshrc .zprofile .zlogout .inputrc" ["tmux"]=".tmux.conf" ["alacritty"]=".config/alacritty" ["nvim"]=".config/nvim .local/bin/vim_askpass_helper .local/bin/vim_askpass_helper_python" ["ssh"]="" ["less"]=".lesskey" ["git"]=".config/git" ["ranger"]=".config/ranger" ["gpg"]="" ["i3"]=".xinitrc .xprofile .Xresources .config/i3 .config/i3status .local/bin/i3status_wrapper .config/rofi .config/picom .local/bin/slm .local/bin/slm_rofi.sh .local/bin/power_rofi.sh .local/bin/wifi .local/bin/bluetooth .local/bin/i3_switch_workspace.sh" ["bat"]=".config/bat" ["font"]="" ["termux"]="" ["arch"]="" ["psql"]=".psqlrc" ["docker"]=".docker/cli-plugins" ["ipython"]=".ipython/profile_default/ipython_config.py" ) _die() { echo "$0: $1" >&2 exit $2 } test "$0" = "./install" || _die "Error: Please, run this file from root of dotfiles, like this './install.sh TARGET" 1 _link_files_in_sandbox() { local targetfile for targetfile in "$@" do echo "installing: $targetfile" if [ "${targetfile::1}" = "%" ]; then _link_files_in_sandbox ${TARGETS["${targetfile:1}"]} else if [ ! "$(dirname "$targetfile")" = "." ]; then mkdir -p "$SANDBOX_PATH/$(dirname "$targetfile")" fi ln -sT "$SUB/$targetfile" "$SANDBOX_PATH/$targetfile" fi done } _compare_sandbox_to_home() { local comparisons comparisons="$(diff -rq "$SANDBOX_PATH" "$TARGET_PATH")" echo "$comparisons" | grep -vE "^Only in .+" || true } _merge_sandbox_to_home() { set +e cp -RTnP "$SANDBOX_PATH" "$TARGET_PATH" set -e } __install_from_sandbox() { local comparisons comparisons="$(_compare_sandbox_to_home)" if [ -n "$comparisons" ]; then echo "$comparisons" >&2 _die "Found conflicting files. Exiting" 1 fi echo "Merging to home..." _merge_sandbox_to_home echo "Successfully installed" } _execute_hook_if_executable() { local hook_path="$DOTFILES_ROOT/install-hooks/$1/$2" if [ -x "$hook_path" ]; then echo "Executing $2 for target '$1'" "$hook_path" fi } execute_pre_hook() { _execute_hook_if_executable "$1" "pre-install" } execute_post_hook() { _execute_hook_if_executable "$1" "post-install" } install_target() { execute_pre_hook "$1" _link_files_in_sandbox ${TARGETS["$1"]} __install_from_sandbox execute_post_hook "$1" } is_target_installed() { local not_fully_installed=false local targetfile for targetfile in ${TARGETS["$1"]} do if [ "${targetfile::1}" = "%" ]; then is_target_installed "${targetfile:1}" || not_fully_installed=true else if [ ! -e "$TARGET_PATH/$targetfile" ]; then echo "$targetfile not linked" not_fully_installed=true fi fi done if $not_fully_installed; then echo "Target '$1' not fully installed" echo return 1 fi return 0 } find_targets_that_depend_on() { local target for target in "${!TARGETS[@]}" do if [[ " ${TARGETS["$target"]} " =~ " %$1 " ]]; then echo "$target" fi done } die_if_installed_targets_depend_on() { for reverse_dependecy in $(find_targets_that_depend_on "$1") do if is_target_installed "$reverse_dependecy" >/dev/null; then _die "target '$reverse_dependecy' is depends on installed target '$1'. Exiting..." 1 fi done } cmd_unlink() { local target targetfile for target in "$@" do die_if_installed_targets_depend_on "$target" for targetfile in ${TARGETS["$target"]} do if [ "${targetfile::1}" = "%" ]; then continue fi if [ -e "$TARGET_PATH/$targetfile" ]; then unlink "$TARGET_PATH/$targetfile" fi done done } cmd_no_target() { _die "TARGET not exists" 1 } cmd_list() { echo "${!TARGETS[@]}" } target_exists() { [[ " ${!TARGETS[*]} " =~ " $1 " ]] } cmd_install() { local target for target in "$@" do if target_exists "$target"; then SANDBOX_PATH="$(mktemp -td "${USER:-user}.dotfiles_XXXXXXX")" export SANDBOX_PATH install_target "$target" rm -rf "$SANDBOX_PATH" else cmd_no_target fi done } cmd_help() { echo "Dotfiles installation script: Usage: ./install TARGET... Usage: ./install unlink TARGET... Usage: ./install check TARGET Usage: ./install list" } unset executed_command readonly executed_command="$1" case "$executed_command" in unlink) shift; cmd_unlink "$@" ;; check) shift; is_target_installed "$@" ;; list) shift; cmd_list "$@" ;; help) shift; cmd_help "$@" ;; *) shift; cmd_install "$executed_command" "$@" ;; esac exit 0