dotfiles/bin/dotfiles

377 lines
10 KiB
Bash
Executable File

#!/bin/bash
##
## binaryDiv's dotfiles management script
## Version 0.5.2
##
usage() {
cat <<- END_OF_HELPTEXT
Usage: $(basename $0) COMMAND [...]
Manage dotfiles via git.
General commands:
help print this help message
Installation:
install run this on first start: install dotfiles script in ~/bin, configure git, ...
(this will NOT create links for all dotfiles, though)
Dotfile management:
ls [DIR] list contents of .dotfiles directory (or DIR relative to it)
(-l: list host-specific files, i.e. ~/.dotfiles/_local/HOSTNAME/DIR)
edit FILE open ~/.dotfiles/FILE in vim
(-l: open host-specific files, i.e. ~/.dotfiles/_local/HOSTNAME/DIR)
diff FILE use vimdiff to compare ~/FILE to ~/.dotfiles/FILE
(-l: compare host-specific files, i.e. ~/.dotfiles/_local/HOSTNAME/DIR)
link FILE... create a symlink from ~/FILE to ~/.dotfiles/FILE (multiple files possible)
(-q: be quiet, only print something if an error occurs)
(-l: link host-specific files, i.e. ~/FILE to ~/.dotfiles/_local/HOSTNAME/FILE)
linkstatus [DIR] list contents of .dotfiles with information about whether there are links to
them in ~/, whether they differ, etc...
(-l: show linkstatus only for host-specific files, i.e. ~/.dotfiles/_local/HOSTNAME)
createfrom FILE... copy ~/FILE to ~/.dotfiles/FILE and symlink the file
(-n: only copy, leave the original, do not create symlink)
(-l: create host-specific files, i.e. ~/FILE to ~/.dotfiles/_local/HOSTNAME/FILE)
Synchronization:
git ARGS... wrapper for git (all arguments are passed on to git)
pull shortcut for git pull
END_OF_HELPTEXT
}
# Define root dir for dotfiles management (defaults to $HOME, can be overridden
# with environment variables for debugging)
if [[ -z $DOTFILESROOT ]]; then
DOTFILESROOT="$HOME"
fi
# Set variables
homedir="$DOTFILESROOT"
dotfilesdir="$DOTFILESROOT/.dotfiles"
dotfiles_local_base="_local"
dotfiles_local_prefix="$dotfiles_local_base/$(hostname)"
# Get command
[[ $# -gt 0 ]] || { usage; exit 1; }
cmd=$1
shift
# Parse command
case "$cmd" in
help|--help|-h)
usage
exit 1
;;
install)
bin_source="$dotfilesdir/bin/dotfiles"
bin_target="$homedir/bin/dotfiles"
bin_dir="$homedir/bin"
if [[ ! -e $bin_target ]]; then
echo "* Installing dotfiles script in $bin_dir/"
mkdir -pv "$bin_dir"
ln -sr "$bin_source" "$bin_target" || exit 1
elif [[ ! "$(realpath $bin_target)" -ef "$bin_source" ]]; then
echo "! Warning: File $bin_target already exists but is NOT a link to $bin_source"
fi
if [[ ":$PATH:" != *":$bin_dir:"* ]]; then
echo "! Warning: $bin_dir is not in your PATH."
fi
show_bash_restart_info=
# Enable user-defined bash completion per directory
bashcompletion_filename=.bash_completion
if [[ ! -e "$homedir/$bashcompletion_filename" ]] ||
[[ ! "$(realpath "$homedir/$bashcompletion_filename")" -ef "$dotfilesdir/$bashcompletion_filename" ]];
then
echo "* Installing user-defined bash completion (~/.bash_completion.d/)"
"$0" link -q "$bashcompletion_filename" &&
show_bash_restart_info=1
fi
mkdir -p "$homedir/.bash_completion.d"
# Activate dotfiles bash completion
bashcompletion_dotfiles_filename=.bash_completion.d/10-dotfiles.sh
if [[ ! -e "$homedir/$bashcompletion_dotfiles_filename" ]] ||
[[ ! "$(realpath "$homedir/$bashcompletion_dotfiles_filename")" -ef "$dotfilesdir/$bashcompletion_dotfiles_filename" ]];
then
echo "* Installing bash completion for dotfiles script"
"$0" link -q "$bashcompletion_dotfiles_filename" &&
show_bash_restart_info=1
fi
if [[ -n $show_bash_restart_info ]]; then
echo " Restart bash or run 'source $homedir/.bash_completion' to activate it now"
fi
echo "* Configuring git settings"
git -C "$dotfilesdir" config merge.ff only
;;
ls)
lsdir="$dotfilesdir"
[[ $1 = "-l" ]] && { lsdir="$lsdir/$dotfiles_local_prefix"; shift; }
[[ -n $1 ]] && lsdir="$lsdir/$1"
echo "$lsdir:"
ls -lAh --color=auto "$lsdir"
;;
linkstatus)
cd "$dotfilesdir"
subdir="$1"
[[ $subdir = "-l" ]] && { subdir="$dotfiles_local_prefix/$1"; shift; }
if [[ -n $subdir ]]; then
subdir="${subdir%/}"
[[ $subdir = $dotfiles_local_base ]] && subdir=$dotfiles_local_prefix
[[ -e $subdir ]] || { echo "$dotfilesdir/$subdir does not exist"; exit 1; }
[[ -d $subdir ]] && subdir="$subdir/"
fi
shopt -s nullglob dotglob
dotdirs=()
for file in $subdir*; do
[[ -e $file ]] || continue
[[ $file = ".git" ]] && continue
file_home="$homedir/$file"
file_dotfiles="$dotfilesdir/$file"
if [[ $subdir == $dotfiles_local_base/* ]]; then
file_home="$homedir/${file#$dotfiles_local_prefix/}"
fi
if [[ -e $file_home ]]; then
if [[ "$(realpath "$file_home")" -ef "$file_dotfiles" ]]; then
file_status=" LINKED "
elif [[ -d $file_home ]]; then
dotdirs+=($file)
continue
elif [[ -f $file_home ]] && cmp --silent "$file_home" "$file_dotfiles"; then
file_status="== EQUAL "
else
file_status="<> DIFFERS "
fi
elif [[ $file = $dotfiles_local_base ]]; then
dotdirs=("$dotfiles_local_prefix" "${dotdirs[@]}")
continue
else
if [[ -d $file_dotfiles ]]; then
file_status="++ NEW DIR "
else
file_status="++ NEW FILE"
fi
fi
[[ -d $file_dotfiles ]] && file="$file/"
echo "$file_status $file"
done
if [[ $dotdirs ]]; then
echo
for dir in ${dotdirs[@]}; do
echo "[$dir]"
dotfiles linkstatus "$dir" | grep '\(LINKED\|EQUAL\|DIFFERS\|NEW\)'
echo
done
fi
;;
edit)
basedir=$dotfilesdir
[[ $1 = "-l" ]] && { basedir="$basedir/$dotfiles_local_prefix"; shift; }
[[ -n $1 ]] || { echo "$(basename $0) $cmd: Missing argument."; exit 1; }
vim "$basedir/$1"
;;
diff)
basedir=$dotfilesdir
[[ $1 = "-l" ]] && { basedir="$basedir/$dotfiles_local_prefix"; shift; }
[[ -n $1 ]] || { echo "$(basename $0) $cmd: Missing argument."; exit 1; }
file_A="$homedir/$1"
file_B="$basedir/$1"
if [[ "$(realpath "$file_A")" -ef "$file_B" ]]; then
echo "$file_A is a link to $file_B"
else
vimdiff "$file_A" "$file_B"
fi
;;
link)
while getopts ":ql" opt; do
case $opt in
q) QUIET=1 ;;
l) link_local=1 ;;
\?)
echo "Invalid option: -$OPTARG"
exit 1
;;
esac
done
shift $((OPTIND-1))
[[ -n $1 ]] || { echo "$(basename $0) $cmd: Missing argument."; exit 1; }
for filename in "$@"; do
file_home="$homedir/$filename"
file_dotfiles="$dotfilesdir/$filename"
if [[ $link_local ]]; then
file_dotfiles="$dotfilesdir/$dotfiles_local_prefix/$filename"
fi
if [[ ! -e $file_dotfiles ]]; then
echo "! $file_dotfiles: target file does not exist"
exit 1
fi
if [[ -e $file_home ]]; then
if [[ "$(realpath "$file_home")" -ef "$file_dotfiles" ]]; then
# Target is already a link to the correct file
continue
fi
if cmp --silent "$file_home" "$file_dotfiles"; then
echo "! $file_home already exists and is equal to $file_dotfiles"
read -p " Delete $file_home and create link? [y/N] " -r
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
# Don't delete file
continue
fi
rm "$file_home" || exit 1
else
echo "! $file_home already exists but is NOT a link to $file_dotfiles, please fix manually"
exit 1
fi
fi
[[ $QUIET ]] || echo "* Creating link $file_home -> $file_dotfiles"
mkdir -p "$(dirname "$file_home")" || exit 1
ln -sr "$file_dotfiles" "$file_home" || exit 1
done
;;
createfrom)
while getopts ":nl" opt; do
case $opt in
n) NOLINK=1 ;;
l) create_local=1 ;;
\?)
echo "Invalid option: -$OPTARG"
exit 1
;;
esac
done
shift $((OPTIND-1))
[[ -n $1 ]] || { echo "$(basename $0) $cmd: Missing argument."; exit 1; }
for filename in "$@"; do
file_home="$homedir/$filename"
file_dotfiles="$dotfilesdir/$filename"
if [[ $create_local ]]; then
file_dotfiles="$dotfilesdir/$dotfiles_local_prefix/$filename"
fi
if [[ ! -e $file_home ]]; then
echo "! $file_home: source file does not exist"
exit 1
elif [[ -e $file_dotfiles ]]; then
echo "! $file_dotfiles already exists"
exit 1
else
mkdir -p "$(dirname "$file_dotfiles")"
[[ $QUIET ]] || echo "* Copy $file_home -> $file_dotfiles"
cp -n "$file_home" "$file_dotfiles" || exit 1
if [[ ! $NOLINK ]]; then
[[ $QUIET ]] || echo "* Remove original and link $file_home -> $file_dotfiles"
rm "$file_home" || exit 1
ln -sr "$file_dotfiles" "$file_home" || exit 1
fi
fi
done
;;
git)
git -C "$dotfilesdir" "$@"
;;
pull)
git -C "$dotfilesdir" pull "$@"
;;
bash-completion)
cat <<- 'END_OF_BASHCOMPLETION'
_dotfiles_filenames() {
compopt -o filenames
files=($(compgen -f -X '*/@(.|..|.git)' .dotfiles/$cur))
COMPREPLY=(${files[@]#.dotfiles/})
}
_dotfiles_filenames_local() {
compopt -o filenames
dotfiles_prefix=.dotfiles/_local/$(hostname)
files=($(compgen -f -X '*/@(.|..|.git)' $dotfiles_prefix/$cur))
COMPREPLY=(${files[@]#$dotfiles_prefix/})
}
_dotfiles() {
COMPREPLY=()
local cur="${COMP_WORDS[COMP_CWORD]}"
local commands="help install ls edit diff link linkstatus createfrom git pull"
local gitcommands="add commit diff fetch log pull push rebase status"
if [[ $COMP_CWORD -eq 1 ]]; then
COMPREPLY=($(compgen -W "$commands" -- $cur))
else
case "${COMP_WORDS[1]}" in
ls|edit|diff)
[[ $COMP_CWORD -eq 2 ]] && _dotfiles_filenames
;;
link|linkstatus)
if [[ $COMP_CWORD -gt 2 && ${COMP_WORDS[2]} = "-l" ]]; then
_dotfiles_filenames_local
else
_dotfiles_filenames
fi
;;
createfrom)
compopt -o filenames
COMPREPLY=($(compgen -f -- ${cur}))
;;
git)
if [[ $COMP_CWORD -eq 2 ]]; then
COMPREPLY=($(compgen -W "$gitcommands" -- ${cur}))
else
_dotfiles_filenames
fi
;;
esac
fi
}
complete -F _dotfiles dotfiles
END_OF_BASHCOMPLETION
;;
*)
echo "Unknown command '$cmd', use 'help' to show all commands."
exit 1
;;
esac