254 lines
5.6 KiB
Bash
Executable File
254 lines
5.6 KiB
Bash
Executable File
#! /bin/bash
|
|
#
|
|
# bootleg-stow
|
|
# Copyright (C) 2021 Vintage Salt <rehashedsalt@cock.li>
|
|
#
|
|
# Distributed under terms of the MIT license.
|
|
#
|
|
set -e
|
|
|
|
# Read-only set-once variables
|
|
declare -r _name="$(basename -- "$0")"
|
|
declare -i _opthelp
|
|
declare -i _optverbose
|
|
# Modes
|
|
declare -i _optstow=1
|
|
declare -i _optunstow
|
|
# Working variables
|
|
declare -a _args
|
|
declare _return
|
|
declare _files
|
|
declare _directories
|
|
|
|
# Helper functions
|
|
log() {
|
|
# Print a line to the terminal if _optverbose is greater than $2
|
|
# $2 defaults to 0
|
|
# loglevel 0: Daily-use messages
|
|
# loglevel 1: Detailed but not quite debugging
|
|
# loglevel 2: Definitely debugging
|
|
[ -z "$1" ] && return 1
|
|
if (( _optverbose >= ${2:-0} )); then
|
|
printf "%s\\n" "$1"
|
|
fi
|
|
}
|
|
warn() {
|
|
# Print a yellow line to the terminal, respecting _optverbose
|
|
[ -z "$1" ] && return 1
|
|
if (( _optverbose >= ${2:-0} )); then
|
|
if [ -t 1 ]; then
|
|
printf "\\e[33m%s\\e[0m\\n" "$1"
|
|
else
|
|
printf "WARN: %s\\n" "$1"
|
|
fi
|
|
fi
|
|
}
|
|
error() {
|
|
# Print a red line to the terminal, exit if $2 is specified
|
|
[ -z "$1" ] && return 1
|
|
if [ -t 2 ]; then
|
|
printf "\\e[31m%s\\e[0m\\n" "$1" 1>&2
|
|
else
|
|
printf "ERROR: %s\\n" "$1" 1>&2
|
|
fi
|
|
[ -z "$2" ] && return
|
|
exit "${2:-1}"
|
|
}
|
|
has() {
|
|
# Parse out all arguments and try to find them in path
|
|
# If an argument cannot be found, set _return and fail
|
|
for prog in "$@"; do
|
|
if ! command -v "$prog" > /dev/null 2>&1; then
|
|
_return="$prog"
|
|
return 1
|
|
fi
|
|
done
|
|
return 0
|
|
}
|
|
checkconflict() {
|
|
# Take a directory as argument 1 and stow its contents in ..
|
|
[ -z "$1" ] && return 1
|
|
if ! [ -d "$1" ]; then
|
|
error "Could not find directory: $1" 2
|
|
fi
|
|
if ! [ -r "$1" ]; then
|
|
error "Could not read directory: $1" 2
|
|
fi
|
|
# Get our list of files
|
|
local files="$(find "$1" ! -name "*.swp" -type f -o -type l)"
|
|
local directories="$(find "$1" -type d)"
|
|
local -a conflict
|
|
# Iterate over them
|
|
for file in $files; do
|
|
# Get the basename of the file
|
|
filename="${file#"$1"/}"
|
|
if [ -f ../"$filename" ]; then
|
|
if [ -h ../"$filename" ]; then
|
|
continue
|
|
else
|
|
conflict+=("$filename")
|
|
fi
|
|
fi
|
|
done
|
|
if [ -n "${conflict[*]}" ]; then
|
|
warn "The following non-symlinks would be touched by this operation:"
|
|
for file in "${conflict[@]}"; do
|
|
echo "$file"
|
|
done
|
|
error "Please resolve these conflicts manually." 3
|
|
fi
|
|
_files="$files"
|
|
_directories="$directories"
|
|
}
|
|
stow() {
|
|
# Stow all of _files and _directories in $1 as a package name
|
|
# Note that you should checkconflict first
|
|
[ -z "$1" ] && return 1
|
|
stowdir="$(basename -- "$PWD")"
|
|
log "Stowing package: $stowdir" 1
|
|
pushd .. > /dev/null 2>&1
|
|
for dir in $_directories; do
|
|
dirname="${dir#"$1"/}"
|
|
if [ "$dir" == "$1" ]; then
|
|
log "Skipping package directory: $dir" 2
|
|
continue
|
|
fi
|
|
if ! [ -d "$dirname" ]; then
|
|
log "Creating directory: $dirname" 2
|
|
mkdir -p "$dirname"
|
|
fi
|
|
done
|
|
for file in $_files; do
|
|
filename="${file#"$1"/}"
|
|
if has realpath dirname; then
|
|
path="$(realpath --relative-to="$(dirname "$filename")" "$PWD/$stowdir/$1/$filename")"
|
|
else
|
|
path="$PWD/$stowdir/$1/$filename"
|
|
fi
|
|
if [ -h "$filename" ]; then
|
|
rm "$filename"
|
|
fi
|
|
if [ -h "$PWD/$stowdir/$1/$filename" ]; then
|
|
log "Copying symlink: $filename" 2
|
|
cp -d "$PWD/$stowdir/$1/$filename" "$filename"
|
|
else
|
|
log "Linking file: $filename to $path" 2
|
|
ln -s "$path" "$filename"
|
|
fi
|
|
done
|
|
log "Done stowing package: $1" 1
|
|
popd > /dev/null 2>&1
|
|
}
|
|
unstow() {
|
|
# Unstow all of _files and _directories
|
|
# Takes a packagename as $1
|
|
[ -z "$1" ] && return 1
|
|
pushd .. > /dev/null 2>&1
|
|
for file in $_files; do
|
|
filename="${file#"$1"/}"
|
|
if [ -h "$filename" ]; then
|
|
rm "$filename"
|
|
elif ! [ -e "$filename" ]; then
|
|
warn "File does not exist, skipping: $filename" 1
|
|
else
|
|
warn "File is not a symlink, skipping: $filename"
|
|
fi
|
|
done
|
|
_directories="$(echo "$_directories" | tac)"
|
|
log "Removing empty directories" 2
|
|
for dir in $_directories; do
|
|
dirname="${dir#"$1"/}"
|
|
# We silently ignore errors here so that rmdiring a directory with stuff in
|
|
# it doesn't break our loop.
|
|
rmdir -p "$dirname" > /dev/null 2>&1 || continue
|
|
done
|
|
popd > /dev/null 2>&1
|
|
}
|
|
|
|
# Core program functions
|
|
printhelp() {
|
|
cat << EOF
|
|
Usage: $_name [OPTION]... PACKAGE...
|
|
Bootleg stow for the poor sods who can't get it.
|
|
|
|
-h Print this help text
|
|
-R Unstow a directory.
|
|
-S Stow a directory. Default operation.
|
|
-v Print more status messages. Stacks
|
|
|
|
Copyright (c) 2021 rehashedsalt@cock.li
|
|
Licensed under the MIT license
|
|
EOF
|
|
}
|
|
|
|
# Main
|
|
main() {
|
|
# Parse out arguments
|
|
while [ -n "$1" ]; do
|
|
# Parse out flags
|
|
while getopts ":hRSv" opt; do
|
|
case $opt in
|
|
h)
|
|
_opthelp=1
|
|
;;
|
|
R)
|
|
_optunstow=1
|
|
;;
|
|
S)
|
|
_optstow=1
|
|
;;
|
|
v)
|
|
_optverbose+=1
|
|
;;
|
|
:)
|
|
error "Option requires argument: -$OPTARG" 2
|
|
;;
|
|
*)
|
|
error "Invalid option: -$OPTARG" 2
|
|
;;
|
|
esac
|
|
done
|
|
# Store arguments
|
|
shift $((OPTIND - 1))
|
|
if [ -n "$1" ]; then
|
|
_args+=("$1")
|
|
shift
|
|
fi
|
|
unset OPTIND
|
|
done
|
|
# Early hook for help
|
|
[ -n "$_opthelp" ] && printhelp && exit 0
|
|
# Validate critical options
|
|
# TODO: That
|
|
# Validate core program dependencies
|
|
log "Validating dependencies" 2
|
|
if ! has basename find tac; then
|
|
error "Failed to find program: $_return" 1
|
|
fi
|
|
|
|
# Do the do
|
|
if [ -n "$_optunstow" ]; then
|
|
if [ -n "${_args[*]}" ]; then
|
|
for package in "${_args[@]}"; do
|
|
checkconflict "$package"
|
|
unstow "$package"
|
|
done
|
|
else
|
|
error "No package specified" 1
|
|
fi
|
|
elif [ -n "$_optstow" ]; then
|
|
if [ -n "${_args[*]}" ]; then
|
|
for package in "${_args[@]}"; do
|
|
checkconflict "$package"
|
|
stow "$package"
|
|
done
|
|
else
|
|
error "No package specified" 1
|
|
fi
|
|
fi
|
|
exit 0
|
|
}
|
|
|
|
main "$@"
|
|
|