dotfiles script v0.5.3

Change indentation from tabs to spaces.
This commit is contained in:
Lexi / Zoe 2026-08-01 17:14:49 +02:00
parent b197b7e2bc
commit 4a079ad19d
Signed by: binaryDiv
GPG key ID: F8D4956E224DA232

View file

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