200 lines
4.9 KiB
Bash
Executable file
200 lines
4.9 KiB
Bash
Executable file
#!/usr/bin/env zsh
|
|
|
|
# Dotfile manager
|
|
#
|
|
# By Henrik Lissner <henrik@lissner.net>
|
|
# https://github.com/hlissner/dotfiles
|
|
#
|
|
# Requires: git, zsh
|
|
|
|
source "${0:A:h}/env"
|
|
SEP='.'
|
|
|
|
# helpers
|
|
function echo-debug { echo "$@" | sed -e "s%$HOME%~%g" -e "s%$DOTFILES%.%g"; }
|
|
function echo-info { printf "\r\033[2K\033[0;34m[ .. ]\033[0m %s\n" "$*"; }
|
|
function echo-skip { printf "\r\033[2K\033[38;05;240m[SKIP]\033[0m %s\n" "$*"; }
|
|
function echo-ok { printf "\r\033[2K\033[0;32m[ OK ]\033[0m %s\n" "$*"; }
|
|
function echo-fail { printf "\r\033[2K\033[0;31m[FAIL]\033[0m %s\n" "$*"; }
|
|
|
|
function _exec { ${DRYRUN:+echo-debug} $@; return ${DRYRUN:+1}; }
|
|
function _topic-do {
|
|
if [[ -e "$1/_init" ]]; then
|
|
if [[ ! -x "$1/_init" ]]; then
|
|
echo-fail "$1's init script isn't executable"
|
|
return 1
|
|
fi
|
|
export TOPIC=$1
|
|
pushd -q $1
|
|
_exec ./_init $2
|
|
popd -q
|
|
return $?
|
|
fi
|
|
}
|
|
function _topics {
|
|
local topics; topics=( "$DOTFILES_DATA"/*.topic(N) )
|
|
echo ${${${topics[@]#$DOTFILES_DATA/}%.topic}/$SEP//}
|
|
}
|
|
|
|
|
|
# For _init scripts
|
|
function topic-enabled-p { [[ -L "$(topic-path $1)" ]] }
|
|
function topic-path { echo "$DOTFILES_DATA/${1/\//$SEP}.topic" }
|
|
|
|
function mklink {
|
|
if [[ $1 == '-s' ]]; then
|
|
local dosudo=1
|
|
shift
|
|
fi
|
|
local flags="-svf"
|
|
local dest=${@[-1]}
|
|
local srcs; srcs=( ${@:1:-1} )
|
|
if [[ $dest == */ && ! -d $dest ]]; then
|
|
mkdir -p $dest
|
|
elif [[ ! -d ${dest%/*} ]]; then
|
|
mkdir -p ${dest%/*}
|
|
fi
|
|
for lk in ${srcs[@]}; do
|
|
local src
|
|
case $lk in
|
|
/*) src=$lk ;;
|
|
.) src="$(topic-path $TOPIC)" ;;
|
|
*) src="$(topic-path $TOPIC)/$lk" ;;
|
|
esac
|
|
if [[ -d $src ]]; then
|
|
if [[ $dest != */ && -d $dest && -L $dest ]]; then
|
|
${dosudo:+sudo} rm -fv $dest
|
|
fi
|
|
fi
|
|
_exec ${dosudo:+sudo} ln $flags $src $dest
|
|
done
|
|
}
|
|
|
|
function init {
|
|
declare -f $1 >/dev/null || return 1
|
|
echo-info "$TOPIC.init.$1: start"
|
|
$1
|
|
local retval=$?
|
|
echo-ok "$TOPIC.init.$1: done"
|
|
return $retval
|
|
}
|
|
|
|
# deploy functions
|
|
function topic-init {
|
|
for topic in $@; do
|
|
if [[ ! -d $topic ]]; then
|
|
echo-fail "$topic doesn't exist!"
|
|
continue
|
|
fi
|
|
echo-ok "$topic"
|
|
|
|
local tpath="$DOTFILES/$topic"
|
|
local tsympath="$(topic-path $topic)"
|
|
if [[ -d "$topic/bin" ]]; then
|
|
export path; path=( $tpath/bin $path )
|
|
echo-ok "$topic: added $topic/bin to PATH, temporarily"
|
|
fi
|
|
|
|
if [[ -n $LINK ]] || ! topic-enabled-p $topic; then
|
|
local op=install
|
|
mklink $tpath $tsympath
|
|
_topic-do $topic link
|
|
fi
|
|
|
|
if [[ -z $INHIBIT_INIT ]]; then
|
|
_topic-do $topic ${FORCE_OP:-${op:-update}}
|
|
if [[ $op == install && $? != 0 ]]; then
|
|
topic-remove $topic
|
|
fi
|
|
fi
|
|
done
|
|
}
|
|
|
|
function topic-remove {
|
|
for topic in $@; do
|
|
if [[ ! -d $topic ]]; then
|
|
echo-fail "$topic doesn't exist"
|
|
continue
|
|
elif ! topic-enabled-p $topic; then
|
|
echo-fail "$topic isn't enabled"
|
|
continue
|
|
fi
|
|
|
|
[[ -z $INHIBIT_INIT ]] && _topic-do $topic clean
|
|
_exec rm -f "$(topic-path $topic)" && \
|
|
echo-ok "Removed $topic" || \
|
|
echo-fail "Couldn't remove $topic"
|
|
done
|
|
}
|
|
|
|
function clean {
|
|
local paths; paths=( $XDG_CACHE_HOME $XDG_CONFIG_HOME $XDG_DATA_HOME $XDG_BIN_HOME $DOTFILES_DATA )
|
|
local links; links=( ~/.*(-@DN) ${^paths}/**(-@DN))
|
|
if [[ ${#links[@]} > 1 ]]; then
|
|
echo-info "Removing dead dotfile symlinks..."
|
|
for link in "${links[@]}"; do
|
|
[[ -e $link ]] || _exec rm -fv "$link"
|
|
done
|
|
else
|
|
echo-ok "No dead symlinks in HOME"
|
|
fi
|
|
|
|
# Remove empty dotfile folders
|
|
local dirs; dirs=( ${^paths}/**(N/^F) ~/.*(N/^F) )
|
|
if [[ ${#dirs[@]} > 1 ]]; then
|
|
echo-info "Removing empty dotfile directories..."
|
|
for dir in "${dirs[@]}"; do
|
|
_exec rmdir "$dir" && echo-ok "Deleted ${dir/$HOME/~}"
|
|
done
|
|
else
|
|
echo-ok "No empty directories in HOME"
|
|
fi
|
|
}
|
|
|
|
|
|
## Bootstrap
|
|
if [[ $ZSH_EVAL_CONTEXT != *:file ]]; then
|
|
while getopts acdehlLit opt; do
|
|
case $opt in
|
|
a) TARGETS="$(_topics)" ;;
|
|
c) CLEAN=1 ;;
|
|
d) REMOVE=1 ;;
|
|
L) LIST=1 ;;
|
|
l) export LINK=1 ;&
|
|
i) export INHIBIT_INIT=1 ;;
|
|
t) export DRYRUN=1 ;;
|
|
h) cat <<EOL
|
|
Usage: ${0:A:t} [-acdlLit] [TOPIC...]
|
|
|
|
-a Target all enabled topics (ignores TOPIC args)
|
|
-c Afterwards, remove dead symlinks & empty dot-directories in $HOME.
|
|
Can be used alone.
|
|
-d Disable & unlink topic(s)
|
|
-l Only enable & relink topic(s) (implies -i)
|
|
-L List enabled topics
|
|
-i Do not run install/update/clean init scripts
|
|
-t Do a test run; do not actually do anything
|
|
EOL
|
|
exit ;;
|
|
*) >&2 echo-fail "Aborted."
|
|
exit 1 ;;
|
|
esac
|
|
done
|
|
shift $((OPTIND-1))
|
|
|
|
if [[ -n $LIST ]]; then
|
|
_topics | tr ' ' $'\n'
|
|
else
|
|
pushd -qL "$DOTFILES"
|
|
if [[ -n $REMOVE ]]; then
|
|
topic-remove ${TARGETS:-$@}
|
|
else
|
|
topic-init ${TARGETS:-$@}
|
|
fi
|
|
[[ -n $CLEAN ]] && clean
|
|
popd -q
|
|
fi
|
|
fi
|
|
|
|
# vim:set ft=sh:
|