#!/bin/bash warn() { echo "$@" >&2 } think() { $Verbose || return warn "$@" } die() { warn "$@" exit 3 } usage() { warn "usage: $0 init VAULT SUBVAULT" warn "usage: $0 explore VAULT SUBVAULT" exit 2 } debug() { $Debug || return local msg for msg in "$@"; do warn "debug:$msg" done } debugv() { $Debug || return local vn local vd for vn in "$@"; do if vd=$(declare -p "$vn" 2>/dev/null); then warn "debug:${vd#declare ?? }" else warn "debug:$vn #Unset" fi done } debugc() { $Debug || return debug "-- cmd begin: $*" Debug=false "$@" | sed -e 's/^/debug:/' >&2 debug "-- cmd end --" } lssv() { # # List sub-vaults # # Sub-vault is a sub-path of vault directory that contains items # that should be linked separately, to a similar $HOME subpath, # rather than linking the whole sub-tree. This is typical for # .config directory specified by XDG standard. # # For example, imagine vault like this: # # dotfiles/ # ├── config # │   ├── dunst # │   │   └── dunstrc # │   └── uzbl # │      ├── config # │      └── style.css # ├── i3 # │   └── config # ├── i3status.conf # └── vimrc # # Without sub-vault, the "config" would be linked as "$HOME/.config": # # $HOME/.config -> dotfiles/.config # $HOME/.i3 -> dotfiles/i3 # $HOME/.i3status.conf -> dotfiles/i3status.conf # $HOME/.vimrc -> dotfiles/vimrc # # This is impractical in most cases, as normally you want to select # applications to link--some dotfiles could be specific to some # hosts or even private. # # Setting 'config' as sub-vault will tell mklinks that you don't # want to link this item directly but rather each of its items: # # $HOME/.config/dunst -> dotfiles/config/dunst # $HOME/.config/uzbl -> dotfiles/config/uzbl # $HOME/.i3 -> dotfiles/i3 # $HOME/.i3status.conf -> dotfiles/i3status.conf # $HOME/.vimrc -> dotfiles/vimrc # # This way, you can have other items under "$HOME/.config" that # are either purely local or linked to another vault. # # Note that the subvault can be deeper than one level, i.e. # subvault "local/share" also works: # # $HOME/.local/share/klavaro -> dotfiles/local/share/klavaro # local sv test -n "${SubVaults[0]}" && { think "using sub-vaults from command line" for sv in "${SubVaults[@]}"; do echo "$sv" done return } test -f "$Vault/dotvault/subvaults" && { think "reading subvaults from file: $Vault/dotvault/subvaults" grep . "$Vault/dotvault/subvaults" \ | grep -v "^#.*" return } think "using hard-coded list of subvaults" echo config } lsvault() { # # List all vaults and subvaults, depth-first # { echo "$Vault" lssv \ | while IFS= read -r sv; do test -e "$Vault/$sv" || { think "ignoring non-existent sub-vault: $sv" continue } test -d "$Vault/$sv" || { warn "ignoring non-directory sub-vault: $sv" continue } echo "$Vault/$sv" done } \ | LC_ALL=C sort \ | tac } istarget() { # # True if item $1 is possible target # # Item is obviously not a possible target if it's # a (sub-)vault. # # A less obvious case is when item is an intermediate directory # on a path leading to a sub-vault. Such item cannot be a target! # # For example: there are sub-vaults: # # foo # foo/bar/baz # # and a tree: # # foo # ├── A # └── bar # └── baz # └── B # # Items A and B could both be targets, but baz could not be! # #TODO: write an explanation # local item=$1 debugv item Verbose=false lsvault | grep -qxF "$item" && return 1 Verbose=false lsvault | grep -qx "$item/.*" && return 1 return 0 } pfxpath() { # # Prefix paths with $1 # # Just like `sed "s/^/$1/" but works with any characters # in $1 excepr newline, which is not allowed in paths.. # local pfx="$1" local path while IFS= read -r path; do echo "$pfx$path" done } lstarget() { # # List potential items in vault $1 # local vlt=$1 debugv vlt local item find "$vlt" -mindepth 1 -maxdepth 1 -printf "%P\n" \ | pfxpath "$vlt/" \ | grep -v "$Vault/dotvault" \ | while IFS= read -r item; do istarget "$item" && echo "$item" done } make_links() { # # Create all links # local slpath local target local vlt lsvault \ | while IFS= read -r vlt; do lstarget "$vlt" \ | while IFS= read -r target; do slpath=$(slpath "$target") if test -L "$slpath"; then $ClobberLinks || { warn "skipping existing slpath: $slpath" continue } think "removing existing link: $slpath" maybe rm "$slpath" fi test -e "$slpath" && { warn "skipping existing non-link: $slpath" continue } maybe ln -sr "$target" "$slpath" done done } explore() { # # Show what would be done # local vlt local tgt local tgts lsvault \ | sort \ | while IFS= read -r vlt; do tgts=$(lstarget "$vlt") test -n "$tgts" || continue if test "$vlt" == "$Vault"; then echo "VAULT:$vlt:" else echo echo "SUBVAULT:$vlt" fi sort <<<"$tgts" \ | while IFS= read -r tgt; do echo "${tgt}:$(slpath "$tgt")" done \ | sed -e "s|:$HOME|:=> ~|" \ | column -ts: \ | sed -e "s/^/ /" done } maybe() { # # Maybe do things $@, maybe not (if in debug mode) # $Debug && { debug "WOULD: $*"; return; } "$@" } slpath() { # # Print path where to create link for target $1 # local item=$1 echo "$HOME/.${item#$Vault/}" } route() { local Vault=$1; shift local SubVaults=() test -n "$Vault" || usage test -e "$Vault" || die "vault does not exist: $Vault" test -d "$Vault" || die "vault is not a directory: $Vault" SubVaults=("$@") case $Action in init) make_links ;; explore) explore ;; *) usage ;; esac } export LC_ALL=C main() { local Action local Debug=false local Verbose=false local ClobberLinks=false while true; do case $1 in -d) Debug=true; shift ;; -v) Verbose=true; shift ;; -f) ClobberLinks=true; shift ;; -*) usage ;; *) break ;; esac done Action=$1; shift route "$@" } main "$@"