#!/bin/bash ## ## binaryDiv's dotfiles management script ## Version 0.5.1 ## 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" 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