#!/bin/bash
-# Copyright (C) 2014 Ian Kelling
+# Copyright (C) 2014-2016 Ian Kelling
# This program is under GPL v. 3 or later, see <http://www.gnu.org/licenses/>
+_lnf_existing_link() {
+ local target dest_file dest_dir
+ target="$1"
+ dest_file="$2"
+ dest_dir="$3"
+ if [[ -L $dest_file ]]; then
+ if [[ $(readlink $dest_file) == "$target" ]]; then
+ # Leave the link in place, but make sure it's
+ # owner & group is as if we created it.
+ # links all get 777 perms, so
+ # we already know that is right.
+
+ # test for setgid.
+ if [[ $(stat -L -c%a "$dest_dir") == 2??? ]]; then
+ grp=$(stat -L -c%g "$dest_dir") || return $?
+ else
+ grp=$(id -g) || return $?
+ fi
+ if [[ $EUID == 0 && $(stat -c%u "$dest_file") != 0 ]]; then
+ chown -h 0:$grp "$dest_file" || return $?
+ elif [[ $(stat -c%g "$dest_file") != "$grp" ]]; then
+ chgrp -h $grp "$dest_file" || return $?
+ fi
+ do_exit=true
+ return 0
+ fi
+ to_remove+=("$dest_file")
+ elif [[ -e $dest_file ]]; then
+ to_remove+=("$dest_file")
+ fi
+ to_link+=("$target")
+}
lnf() {
- local help="lnf - Create symlinks conveniently and forcefully.
-Usage:
- lnf -T TARGET LINK_NAME (1st form)
- lnf TARGET (2nd form)
- lnf TARGET... DIRECTORY (3rd form)
+ local help="Usage:
+ lnf [OPTIONS] -T TARGET LINK_NAME (1st form)
+ lnf [OPTIONS] TARGET (2nd form)
+ lnf [OPTIONS] TARGET... DIRECTORY (3rd form)
+Create symlinks forcefully
-Remove existing file in the using trash-put or rm -rf if it is not available,
-or trash-put fails due to a cross-filesystem link.
-Create directory if needed. Slightly more restrictive arguments than ln.
+If the link already exists, make it's ownership be the same as if it was
+newly created (only chown if we are root). Removes existing files using
+trash-put or rm -rf if it is not available, or if trash-put fails due to a
+limitation such as a cross-filesystem link. Create directory of link if
+needed. Slightly more restrictive arguments than ln.
In the 1st form, create a link to TARGET with the name LINK_NAME. In the 2nd
form, create a link to TARGET in the current directory. In the 3rd form, create
-links to each TARGET in DIRECTORY."
+links to each TARGET in DIRECTORY.
+
+-n|--dry-run Do verbose dry run.
+-v|--verbose Print commands which modify the filesystem.
+-h|--help Print help and exit.
+"
- if [[ $1 == --help || $# -eq 0 ]]; then
+
+ local temp nodir
+ local verbose=false
+ local dry_run=false
+ local do_exit=false
+ temp=$(getopt -l help,dry-run,verbose hnTv "$@") || usage 1
+ eval set -- "$temp"
+ while true; do
+ case $1 in
+ -n|--dry-run) dry_run=true; verbose=true; shift ;;
+ -T) nodir=-T; shift ;;
+ -v|--verbose) verbose=true; shift ;;
+ -h|--help) echo "$help"; return 0 ;;
+ --) shift; break ;;
+ *) echo "$0: Internal error! unexpected args: $*" ; exit 1 ;;
+ esac
+ done
+
+ if (( $# == 0 )); then
echo "$help"
- return 0
+ return 1
fi
- local nodir
- if [[ $1 == -T ]]; then
- nodir=-T
- shift
+ if [[ $nodir ]]; then
if (( $# != 2 )); then
- echo "lnf error: expected 2 arguments with -T flag. Got $#"
+ echo "lnf: error: expected 2 arguments with -T flag. Got $#"
return 1
fi
fi
shopt -s extglob
- local x ret prefix dir to_remove
+ local -a to_remove to_link
+ local x ret prefix dest_dir grp target
+ local mkdir=false
- to_remove=()
if [[ $nodir ]]; then
- dir="$(dirname "$2")"
- if [[ -e $2 || -L $2 ]]; then
- to_remove+=("$2")
- elif [[ ! -d $dir ]]; then
- if [[ -e $dir || -L $dir ]]; then
- to_remove+=("$dir")
- fi
- if ! mkdir -p "$(dirname "$2")"; then
- echo "lnf error: failed to make directory $(dirname "$2")"
- return 1
+ dest_file="$2"
+ dest_dir="$(dirname "$dest_file")"
+ _lnf_existing_link "$1" "$dest_file" "$dest_dir" || return $?
+ if $do_exit; then return 0; fi
+ if [[ ! -d $dest_dir ]]; then
+ mkdir=true
+ if [[ -e $dest_dir || -L $dest_dir ]]; then
+ to_remove+=("$dest_dir")
fi
fi
+ to_link+=("$dest_file")
elif (( $# >= 2 )); then
- if [[ -d ${!#} ]]; then
- prefix="${!#}/" # last arg
- for x in "${@:1:$(( $# - 1 ))}"; do # all but last arg
+ dest_dir="${!#}"
+ if [[ -d $dest_dir ]]; then
+ prefix="$dest_dir" # last arg
+ for target in "${@:1:$(( $# - 1 ))}"; do # all but last arg
# Remove 1 or more trailing slashes, using.
- x="${x%%+(/)}"
+ dest_file="${target%%+(/)}"
# remove any leading directory components, add prefix
- x="$prefix/${x##*/}"
- [[ -e "$x" || -L "$x" ]] && to_remove+=("$x")
+ dest_file="$prefix/${target##*/}"
+ _lnf_existing_link "$target" "$dest_file" "$dest_dir"
done
else
- if ! mkdir -p "${!#}"; then
- echo "lnf error: failed to make directory ${!#}"
- return 1
- fi
+ to_link+=("${@:1:$(( $# - 1 ))}")
+ mkdir=true
+ fi
+ if (( ${#to_link[@]} == 0 )); then
+ return 0
fi
+ to_link+=("$dest_dir")
elif [[ $# -eq 1 ]]; then
- [[ -e "${1##*/}" || -L "${1##*/}" ]] && to_remove+=("${1##*/}")
+ dest_file="${1##*/}"
+ _lnf_existing_link "$1" "$dest_file" . || return $?
+ if $do_exit; then return 0; fi
fi
if (( ${#to_remove[@]} >= 1 )); then
if type -P trash-put >/dev/null; then
- trash-put -- "${to_remove[@]}" || ret=$?
+ if $verbose; then
+ echo "lnf: trash-put -- ${to_remove[*]}"
+ fi
+ if ! $dry_run; then
+ trash-put -- "${to_remove[@]}" || ret=$?
+ fi
# trash-put will fail to trash a link that goes across filesystems (72),
# and for empty files (74)
# so revert to rm -rf in that case
if [[ $ret == 72 ]]; then
- echo "$0: using rm -rf to overcome cross filesystem trash-put limitation"
- rm -rf -- "${to_remove[@]}"
+ echo "lnf: using rm -rf to overcome cross filesystem trash-put limitation"
+ rm -rf -- "${to_remove[@]}" || return $?
elif [[ $ret == 74 ]]; then
- echo "$0: using rm -rf to overcome empty file/dir trash-put limitation"
+ echo "lnf: using rm -rf to overcome empty file & hardlink trash-put limitation"
rm -rf -- "${to_remove[@]}"
elif [[ $ret && $ret != 0 ]]; then
return $x
fi
else
- rm -rf -- "${to_remove[@]}"
+ if $verbose; then
+ echo "lnf: rm -rf -- ${to_remove[*]}"
+ fi
+ if ! $dry_run; then
+ rm -rf -- "${to_remove[@]}"
+ fi
fi
fi
$reset_extglob && shopt -u extglob
- ln -s $nodir -- "$@"
+
+ if $mkdir; then
+ if $verbose; then
+ echo "lnf: mkdir -p $dest_dir"
+ fi
+
+ if ! $dry_run && ! mkdir -p "$dest_dir"; then
+ echo "lnf error: failed to make directory $dest_dir"
+ return 1
+ fi
+ fi
+
+ if $verbose; then
+ echo "lnf: ln -s $nodir -- ${to_link[*]}"
+ fi
+ if ! $dry_run; then
+ ln -s $nodir -- "${to_link[@]}"
+ fi
}
lnf "$@"