Skip to content

Instantly share code, notes, and snippets.

@mcmire
Last active October 31, 2022 01:17
Show Gist options
  • Save mcmire/60f3ae981595b09fd55f32372288ccc6 to your computer and use it in GitHub Desktop.
Save mcmire/60f3ae981595b09fd55f32372288ccc6 to your computer and use it in GitHub Desktop.
Installation and uninstallation scripts for dotfiles

This gist provides scripts that you can use to install files inside of a dotfiles repo into your home directory and uninstall them if you wish. To use these, download the zip file of this gist and unzip it into the bin folder of your dotfiles repo. Then run chmod +x bin/install bin/uninstall. After this, run:

bin/install --help
bin/uninstall --help
colorize() {
local code=
case $1 in
bold)
code=1
;;
red)
code=31
;;
green)
code=32
;;
yellow)
code=33
;;
blue)
code=34
;;
*)
echo "WARNING: $1 is an invalid color"
code=0
;;
esac
echo -ne "\033[${code}m"
echo -n "${@:2}"
echo -ne "\033[0m"
}
print-with-color() {
echo $(colorize "$@")
}
success() {
print-with-color green "$@"
}
warning() {
print-with-color yellow "$@"
}
info() {
print-with-color bold "$@"
}
error() {
print-with-color red "$@"
}
digest-file() {
if type md5 &>/dev/null; then
echo $(md5 -q "$1")
elif type md5sum &>/dev/null; then
echo $(md5sum "$1")
else
error "Could not find md5 or md5sum, aborting."
exit 1
fi
}
files-equal() {
[[ $(digest-file "$1") == $(digest-file "$2") ]]
}
build-destination-path() {
echo "$HOME/.$1"
}
inspect-command() {
echo " >" "$@"
}
recurse-dir() {
local dir="$1"
for source_path in $(ls -1 "$dir"); do
process-entry "$source_path" "$dir"
done
}
#!/usr/bin/env bash
set -euo pipefail
absolute-path-of() {
echo $(cd "$(dirname "$1")" &>/dev/null && pwd)/$(basename "$1")
}
PROJECT_DIR=$(dirname $(dirname $(absolute-path-of $0)))
source "$PROJECT_DIR/bin/helpers.sh"
GIT_NAME=
GIT_EMAIL=
GITHUB_USER=
GITHUB_TOKEN=
DRY_RUN=0
FORCE_ALL=0
FORCE_TEMPLATES=0
VERBOSE=0
announce() {
local subaction="$1"
local action="$2"
local source_path= destination_path=
local color=
if [[ $# -eq 4 ]]; then
source_path="${3/$PROJECT_DIR/\$DOTFILES}"
destination_path="${4/$HOME/~}"
else
destination_path="${3/$HOME/~}"
fi
if [[ -d $source_path ]]; then
source_path="${source_path}/"
fi
if [[ -d $destination_path ]]; then
destination_path="${destination_path}/"
fi
case $action in
create)
color=green
;;
overwrite)
color=red
;;
exists | same | unknown)
color=blue
;;
esac
local colorized_action=$(colorize $color "$(printf "%8s" "$action")")
local colorized_subaction=$(colorize yellow "$(printf "%5s" "$subaction")")
local prefix="${colorized_action} ${colorized_subaction}"
if [[ $subaction == "gen" ]]; then
echo "${prefix} ${source_path} ==> ${destination_path}"
elif [[ $source_path ]]; then
echo "${prefix} ${destination_path} --> ${source_path}"
else
echo "${prefix} ${destination_path}"
fi
}
print-help() {
cat <<TEXT
$(colorize bold "## DESCRIPTION")
This script will create symlinks in your home folder based on the contents of
the src/ directory. This directory is iterated over, and one of a few things
happens depending on what it is:
* For any file in src/, the script will create a symlink in your home folder
that points to this file.
EXAMPLE: for src/tmux.conf, ~/.tmux.conf is created that points to this file.
* For any directory in src/, the script will recurse the directory and create
symlinks inside of it according to the previous rule.
EXAMPLE: for src/rbenv/default-gems, ~/.rbenv/default-gems is created that
points to this file.
There are a couple of exceptions to this:
* For a file anywhere in src/ that ends in .erb, the script will evaluate
that file as an ERB template, producing another file instead of creating a
symlink. (\`args\` within a template refers to the OPTIONS below.)
EXAMPLE: for src/gitconfig.erb, it is run through ERB to produce ~/.gitconfig.
* For any directory in src/ that contains a .no-recurse file, the script will
NOT recurse the directory; instead, it will create a symlink for the
directory.
EXAMPLE: for src/zsh, because it contains a .no-recurse file, ~/.zsh is
created that points to this directory.
The script takes care not to overwrite any existing files, unless you specify
--force or --force-templates.
Finally, if you want to know what the script will do before running it for real,
and especially if this is the first time you're running this script, use the
--dry-run option. For further output, use the --verbose option.
$(colorize bold "## USAGE")
$0 [FIRST_TIME_OPTIONS] [OTHER_OPTIONS]
FIRST_TIME_OPTIONS are one or more of:
--git-email EMAIL
The email that you'll use to author Git commits.
--git-name NAME
The name that you'll use to author Git commits.
--github-user
Your username on GitHub. Used by tools like GitX.
--github-token
Your API token on GitHub. Used by tools like GitX.
OTHER_OPTIONS are one or more of:
--dry-run, --noop, -n
Don't actually create any symlinks or write any files.
--force-templates
Usually dotfiles generated from templates that already exist are not
overwritten. This bypasses that.
--force, -f
Usually dotfiles that already exist are not overwritten. This bypasses that.
(Implies --force-templates.)
--verbose, -V
Show every command that is run when it is run.
--help, -h
You're looking at it ;)
TEXT
}
parse-args() {
if [[ $# -eq 0 ]]; then
error "No arguments given."
echo "\
Please run --help for usage. (If this is the first time you're running this,
take special note of FIRST_TIME_OPTIONS!)"
exit 1
fi
while [[ ${1:-} ]]; do
local arg="${1:-}"
case "$arg" in
--git-email)
GIT_EMAIL="$2"
shift 2
;;
--git-name)
GIT_NAME="$2"
shift 2
;;
--github-user)
GITHUB_USER="$2"
shift 2
;;
--github-token)
GITHUB_TOKEN="$2"
shift 2
;;
--dry-run | --noop | -n)
DRY_RUN=1
shift
;;
--force | -f)
FORCE_ALL=1
shift
;;
--force-templates)
FORCE_TEMPLATES=1
shift
;;
--verbose | -V)
VERBOSE=1
shift
;;
--help | -h | -?)
print-help | more -R
exit
;;
*)
error "Unknown argument '$arg' given."
echo "Please run --help for usage."
exit 1
esac
done
}
generate-from-template() {
local full_source_path="$1"
local full_destination_path="$2"
if [[ $VERBOSE -eq 1 ]]; then
eval inspect-command bin/generate-from-template '"$full_source_path"' '"$full_destination_path"' "${GIT_NAME:+GIT_NAME=\"\\\"\$GIT_NAME\\\"\"}" "${GIT_EMAIL:+GIT_EMAIL=\"\\\"\$GIT_EMAIL\\\"\"}" "${GITHUB_USER:+GITHUB_USER=\"\\\"\$GITHUB_USER\\\"\"}" "${GITHUB_TOKEN:+GITHUB_TOKEN=\"\\\"\$GITHUB_TOKEN\\\"\"}"
fi
if [[ $DRY_RUN -eq 0 ]]; then
eval bin/generate-from-template '"$full_source_path"' '"$full_destination_path"' "${GIT_NAME:+GIT_NAME=\"\$GIT_NAME\"}" "${GIT_EMAIL:+GIT_EMAIL=\"\$GIT_EMAIL\"}" "${GITHUB_USER:+GITHUB_USER=\"\$GITHUB_USER\"}" "${GITHUB_TOKEN:+GITHUB_TOKEN=\"\$GITHUB_TOKEN\"}"
fi
}
always-generate-from-template() {
local old_dry_run=$DRY_RUN
local verbose=$VERBOSE
DRY_RUN=0
VERBOSE=0
generate-from-template "$@"
DRY_RUN=$old_dry_run
VERBOSE=$verbose
}
link-file() {
local full_source_path="$1"
local full_destination_path="$2"
if [[ $VERBOSE -eq 1 ]]; then
inspect-command mkdir -p $(dirname "$full_destination_path")
inspect-command ln -s "$full_source_path" "$full_destination_path"
fi
if [[ $DRY_RUN -eq 0 ]]; then
mkdir -p $(dirname "$full_destination_path")
ln -s "$full_source_path" "$full_destination_path"
fi
}
process-previously-generated-template() {
local full_source_path="$1"
local full_destination_path="$2"
always-generate-from-template "$full_source_path" "$generated_file_path"
if files-equal "$generated_file_path" "$full_destination_path"; then
announce gen same "$full_source_path" "$full_destination_path"
elif [[ $FORCE_TEMPLATES -eq 1 || $FORCE_ALL -eq 1 ]]; then
announce gen overwrite "$full_source_path" "$full_destination_path"
else
announce entry unknown "$full_destination_path"
fi
}
process-template() {
local full_source_path="$1"
local full_source_path_without_extension="${full_source_path%.erb}"
local destination_path="${full_source_path_without_extension#$PROJECT_DIR/src/}"
local full_destination_path=$(build-destination-path "$destination_path")
local generated_file_path="/tmp/setup_script_generator/generated-file"
if [[ -e $full_destination_path ]]; then
if [[ -f "${full_destination_path}.context" ]]; then
process-previously-generated-template "$full_source_path" "$full_destination_path"
elif [[ $FORCE_TEMPLATES -eq 1 || $FORCE_ALL -eq 1 ]]; then
announce gen overwrite "$full_source_path" "$full_destination_path"
generate-from-template "$full_source_path" "$full_destination_path"
else
announce entry exists "$full_source_path" "$full_destination_path"
fi
else
announce gen create "$full_source_path" "$full_destination_path"
generate-from-template "$full_source_path" "$full_destination_path"
fi
}
process-link() {
local full_source_path="$1"
local destination_path="${full_source_path#$PROJECT_DIR/src/}"
local full_destination_path=$(build-destination-path "$destination_path")
if [[ -e $full_destination_path ]]; then
if [[ $FORCE_ALL -eq 1 ]]; then
announce link overwrite "$full_source_path" "$full_destination_path"
link-file "$full_source_path" "$full_destination_path"
else
announce link exists "$full_source_path" "$full_destination_path"
fi
else
announce link create "$full_source_path" "$full_destination_path"
link-file "$full_source_path" "$full_destination_path"
fi
}
process-entry() {
local source_path="$1"
local dir="$2"
local full_source_path=$(absolute-path-of "$dir/$source_path")
if [[ -d $full_source_path && ! -e "$full_source_path/.no-recurse" ]]; then
recurse-dir "$full_source_path"
elif [[ $full_source_path =~ \.erb$ ]]; then
process-template "$full_source_path"
else
process-link "$full_source_path"
fi
}
main() {
parse-args "$@"
if [[ $DRY_RUN -eq 1 ]]; then
info "Running in dry-run mode."
echo
fi
recurse-dir "$PROJECT_DIR/src"
if [[ $DRY_RUN -eq 1 ]]; then
echo
info "Don't worry — nothing was written to the filesystem!"
else
echo
success "All files are installed, you're good!"
echo "(Not the output you expect? Run --force or --force-templates to force-update skipped files.)"
fi
}
main "$@"
#!/usr/bin/env bash
set -euo pipefail
absolute-path-of() {
echo $(cd "$(dirname "$1")" &>/dev/null && pwd)/$(basename "$1")
}
PROJECT_DIR=$(dirname $(dirname $(absolute-path-of $0)))
source "$PROJECT_DIR/bin/helpers.sh"
DRY_RUN=0
FORCE=0
VERBOSE=0
announce() {
local subaction="$1"
local action="$2"
local source_path= destination_path=
local color=
if [[ $# -eq 4 ]]; then
source_path="${3/$PROJECT_DIR/\$DOTFILES}"
destination_path="${4/$HOME/~}"
else
destination_path="${3/$HOME/~}"
fi
if [[ -d $source_path ]]; then
source_path="${source_path}/"
fi
if [[ -d $destination_path ]]; then
destination_path="${destination_path}/"
fi
case $action in
delete | purge)
color=red
;;
absent | different | unlinked | unrecognized)
color=blue
;;
esac
local colorized_action=$(colorize $color "$(printf "%12s" "$action")")
local colorized_subaction=$(colorize yellow "$(printf "%5s" "$subaction")")
local prefix="${colorized_action} ${colorized_subaction}"
if [[ $subaction == "gen" ]]; then
echo "${prefix} ${destination_path} <== ${source_path}"
elif [[ $source_path ]]; then
echo "${prefix} ${destination_path} <-- ${source_path}"
else
echo "${prefix} ${destination_path}"
fi
}
print-help() {
cat <<TEXT
$(colorize bold "## DESCRIPTION")
This script will remove symlinks in your home folder based on the contents of
the src/ directory. This directory is iterated over, and one of a few things
happens depending on what it is:
* For any file in src/, the script will remove the corresponding symlink in your
home folder if it points to this file.
EXAMPLE: for src/tmux.conf, ~/.tmux.conf is removed if it points to this file.
* For any directory in src/, the script will recurse the directory and remove
symlinks inside of it according to the previous rule.
EXAMPLE: for src/rbenv/default-gems, ~/.rbenv/default-gems is removed if it
points to this file.
There are a couple of exceptions to this:
* For a file anywhere in src/ that ends in .erb, the script will evaluate
that file as an ERB template. If the resulting file matches the corresponding
file in your home directory, that file will be removed.
EXAMPLE: for src/gitconfig.erb, it is run through ERB, and if it matches the
current contents of ~/.gitconfig, then ~/.gitconfig will be removed.
* For any directory in src/ that contains a .no-recurse, the script will NOT
recurse the directory; instead, it will remove the symlink for the directory
if it points to the source directory.
EXAMPLE: for src/zsh, because it contains a .no-recurse file, ~/.zsh is
removed if it points to this directory.
The script takes care not to remove any symlinks or ERB-generated files that do
not point to or match the files in this repo, unless you provide --force.
Finally, if you want to know what the script will do before running it for real,
and especially if this is the first time you're running this script, use the
--dry-run option. For further output, use the --verbose option.
$(colorize bold "## USAGE")
$0 OPTIONS
where OPTIONS are:
--dry-run, --noop, -n
Don't actually remove any files.
--force, -f
Usually dotfiles that are not symlinks that point to files in this directory
and dotfiles that do not match ERB templates are not removed. This bypasses
that.
--verbose, -V
Show every command that is run when it is run.
--help, -h
You're looking at it ;)
TEXT
}
parse-args() {
if [[ $# -eq 0 ]]; then
error "No arguments given."
echo "Please run --help for usage."
exit 1
fi
while [[ ${1:-} ]]; do
local arg="${1:-}"
case "$arg" in
--dry-run | --noop | -n)
DRY_RUN=1
shift
;;
--force | -f)
FORCE=1
shift
;;
--verbose | -V)
VERBOSE=1
shift
;;
--help | -h | -?)
print-help | more -R
exit
;;
*)
error "Unknown argument '$arg' given."
echo "Please run --help for usage."
exit 1
esac
done
}
remove-file() {
local full_destination_path="$1"
if [[ $VERBOSE -eq 1 ]]; then
inspect-command rm "$full_destination_path"
fi
if [[ $DRY_RUN -eq 0 ]]; then
rm -rf "$full_destination_path"
fi
}
process-previously-generated-template() {
local full_source_path="$1"
local full_destination_path="$2"
local context=("$(< "${full_destination_path}.context")")
local generated_file_path="/tmp/setup_script_generator/generated-file"
bin/generate-from-template "$full_source_path" "$generated_file_path" "${context[@]}"
if files-equal "$generated_file_path" "$destination_path"; then
announce gen delete "$full_source_path" "$full_destination_path"
remove-file "$full_destination_path"
elif [[ $FORCE -eq 1 ]]; then
announce gen purge "$full_source_path" "$full_destination_path"
remove-file "$full_destination_path"
else
announce gen different "$full_source_path" "$full_destination_path"
fi
}
process-unknown-template() {
local full_source_path="$1"
local full_destination_path="$2"
if [[ $FORCE -eq 1 ]]; then
announce gen purge "$full_source_path" "$full_destination_path"
remove-file "$full_destination_path"
else
announce gen unrecognized "$full_source_path" "$full_destination_path"
fi
}
process-template() {
local full_source_path="$1"
local full_source_path_without_extension="${full_source_path%.erb}"
local destination_path="${full_source_path_without_extension#$PROJECT_DIR/src/}"
local full_destination_path=$(build-destination-path "$destination_path")
if [[ -f $full_destination_path ]]; then
if [[ -f "${full_destination_path}.context" ]]; then
process-previously-generated-template "$full_source_path" "$full_destination_path"
else
process-unknown-template "$full_source_path" "$full_destination_path"
fi
else
announce gen absent "$full_source_path" "$full_destination_path"
fi
}
process-link() {
local full_source_path="$1"
local destination_path="${full_source_path#$PROJECT_DIR/src/}"
local full_destination_path=$(build-destination-path "$destination_path")
if [[ -h $full_destination_path ]]; then
announce link delete "$full_source_path" "$full_destination_path"
remove-file "$full_destination_path"
elif [[ -e $full_destination_path ]]; then
if [[ $FORCE -eq 1 ]]; then
announce entry purge "$full_source_path"
remove-file "$full_destination_path"
else
announce entry unlinked "$full_destination_path"
fi
fi
}
process-entry() {
local source_path="$1"
local dir="$2"
local full_source_path=$(absolute-path-of "$dir/$source_path")
local full_destination_path=
local context=
if [[ -d $full_source_path && ! -e "$full_source_path/.no-recurse" ]]; then
recurse-dir "$full_source_path"
elif [[ $full_source_path =~ \.erb$ ]]; then
process-template "$full_source_path"
else
process-link "$full_source_path"
fi
}
main() {
parse-args "$@"
if [[ $DRY_RUN -eq 1 ]]; then
info "Running in dry-run mode."
echo
fi
recurse-dir "$PROJECT_DIR/src"
if [[ $DRY_RUN -eq 1 ]]; then
echo
info "Don't worry — nothing was removed from the filesystem!"
else
echo
success "All files have been removed, you're good!"
echo "(Not the output you expect? Run --force to force-remove skipped files.)"
fi
}
main "$@"
@ProfessorManhattan
Copy link

Sexy bash script

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment