377 lines
10 KiB
Bash
Executable File
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
|
|
|