Skip to content

Instantly share code, notes, and snippets.

@ascarter
Created October 2, 2024 00:03
Show Gist options
  • Save ascarter/01bf9acdfdad9edd238f38fe4e1b6174 to your computer and use it in GitHub Desktop.
Save ascarter/01bf9acdfdad9edd238f38fe4e1b6174 to your computer and use it in GitHub Desktop.
stow
#!/bin/sh
# dfstow is a simplified version of GNU Stow, a tool that helps manage multiple
# software packages in a single directory.
#
# It does this by creating symbolic links from the destination directory
# to the original files and directories.
#
# The destination directory is created if it doesn't exist.
#
# If a file or directory already exists in the destination directory,
# it's replaced with a symbolic link.
#
# If a file or folder starts with `dot-`, the `dot-` prefix is replaced with a
# dot (`.`) when creating the symlink.
#
# Tree folding is not supported. This means that all directories and files
# in the source directory are linked to the destination directory.
#
# See https://www.gnu.org/software/stow/ for reference.
set -eu
# set -x
STOW_DIR=${STOW_DIR:-$PWD}
TARGET_DIR=${TARGET_DIR:-$STOW_DIR/..}
# Set default options
ADOPT=0
DELETE=0
LIST=0
VERBOSE=0
usage() {
echo "Usage: $0 [options] package1 [package2 ...]" >&2
echo "Options:"
echo " -d DIR Set stow directory (default: $STOW_DIR)"
echo " -t DIR Set target directory (default: $TARGET_DIR)"
echo " -a Adopt existing files"
echo " -D Delete existing symlinks"
echo " -l List packages"
echo " -v Verbose"
}
log() {
if [ "$#" -eq 1 ]; then
printf "%s\n" "$1"
elif [ "$#" -gt 1 ]; then
printf " $(tput bold)%-10s$(tput sgr0)\t%s\n" "$1" "$2"
fi
}
vlog() {
if [ $VERBOSE -eq 1 ]; then
log "$@"
fi
}
err() {
echo "$*" >&2
}
main() {
if [ "$#" -lt 1 ]; then
usage
exit 1
fi
mkdir -p "$TARGET_DIR"
# Convert to absolute paths
STOW_DIR=$(readlink -f $STOW_DIR)
TARGET_DIR=$(readlink -f $TARGET_DIR)
vlog "STOW_DIR" "$STOW_DIR"
vlog "TARGET_DIR" "$TARGET_DIR"
for package in "$@"; do
log "stow $package"
for f in $(find ${package} -type f -print); do
p=$(readlink -f ${f})
t=${TARGET_DIR}/${f#${package}/}
# Convert all `dot-` to `.` in the path
t=$(echo $t | sed 's/dot-/\./g')
# Check if source is in ignore list
if [ -f "${STOW_DIR}/.stowignore" ]; then
if grep -q "${f}" "${STOW_DIR}/.stowignore"; then
log "ignore" "${f}"
continue
fi
fi
# Check if symlink exists
if [ -h "${t}" ]; then
if [ $DELETE -eq 1 ] && [ $LIST -eq 0 ]; then
# Delete symlink
log "unlink" "${t}"
rm "${t}"
else
log "exists" "${t}"
fi
else
if [ $DELETE -eq 0 ] && [ $LIST -eq 0 ]; then
# Check if a file already exists
if [ -e "${t}" ]; then
if [ $ADOPT -eq 1 ]; then
# Adopt existing file
log "adopt" "${t}"
mv ${t} ${p}
else
# Conflict
log "conflict" "${t}"
continue
fi
fi
# Symlink file
log "link" "${p} -> ${t}"
mkdir -p $(dirname "${t}")
ln -s ${p} ${t}
else
log "missing" "${t}"
fi
fi
done
done
# Remove empty directories
if [ $DELETE -eq 1 ] && [ $LIST -eq 0 ]; then
find ${TARGET_DIR} -type d -empty -delete
fi
}
while getopts ":d:t:aDlv" opt_char; do
case $opt_char in
a) ADOPT=1 ;;
D) DELETE=1 ;;
d) STOW_DIR="$OPTARG" ;;
l) LIST=1 ;;
t) TARGET_DIR="$OPTARG" ;;
v) VERBOSE=1 ;;
\?) usage && exit 1 ;;
esac
done
shift "$(($OPTIND - 1))"
main "$@"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment