# 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
+ # ownership is right.
+ # already exists. links all get 777 perms, so
+ # we dun have to mess with that.
+ if [[ $(stat -L -c%a "$dest_dir") == 2* ]]; then
+ grp=$(stat -L -c%g "$dest_dir")
+ else
+ grp=$(id -g)
+ fi
+ if [[ $EUID == 0 && $(stat -c%u "$dest_file") != 0 ]]; then
+ chown 0:$grp "$dest_file"
+ elif [[ $(stat -c%g "$dest_file") != "$grp" ]]; then
+ chgrp $grp "$dest_file"
+ fi
+ return 1
+ fi
+ to_remove+=("$dest_file")
+ elif [[ -e $dest_file ]]; then
+ to_remove+=("$dest_file")
+ fi
+ to_link+=("$target")
+}
lnf() {
local help="Usage:
- lnf -T TARGET LINK_NAME (1st form)
- lnf TARGET (2nd form)
- lnf TARGET... DIRECTORY (3rd form)
+ lnf [OPTIONS] -T TARGET LINK_NAME (1st form)
+ lnf [OPTIONS] TARGET (2nd form)
+ lnf [OPTIONS] TARGET... DIRECTORY (3rd form)
Create symlinks forcefully
-Removes existing files using trash-put or rm -rf if it is not available,
-or trash-put fails due to a limitation such as 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.
-Do export LNF_VERBOSE=true for verbose output
+-n|--dry-run Do verbose dry run.
+-v|--verbose Print commands which modify the filesystem.
+-h|--help Print help and exit.
"
- if [[ $1 == --help || $1 == -h || $# -eq 0 ]]; then
+
+ local temp
+ local verbose=false
+ local dry_run=false
+ local nodir
+ 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
-
local reset_extglob=false
! shopt extglob >/dev/null && reset_extglob=true
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
dest_file="$2"
dest_dir="$(dirname "$dest_file")"
- if [[ -e $dest_file || -L $dest_file ]]; then
- to_remove+=("$dest_file")
- elif [[ ! -d $dest_dir ]]; then
+ _lnf_existing_link "$1" "$dest_file" "$dest_dir" || return 0
+ if [[ ! -d $dest_dir ]]; then
mkdir=true
if [[ -e $dest_dir || -L $dest_dir ]]; then
- to_remove+=("$dir")
+ to_remove+=("$dest_dir")
fi
fi
+ to_link+=("$dest_file")
elif (( $# >= 2 )); then
dest_dir="${!#}"
if [[ -d $dest_dir ]]; then
- prefix="$dest_dir/" # last arg
- for x in "${@:1:$(( $# - 1 ))}"; do # all but last arg
+ 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
+ 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 0
fi
if (( ${#to_remove[@]} >= 1 )); then
if type -P trash-put >/dev/null; then
- if [[ $LNF_VERBOSE == true ]]; then
+ if $verbose; then
echo "lnf: trash-put -- ${to_remove[*]}"
fi
- trash-put -- "${to_remove[@]}" || ret=$?
+ 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"
+ echo "lnf: using rm -rf to overcome cross filesystem trash-put limitation"
rm -rf -- "${to_remove[@]}"
elif [[ $ret == 74 ]]; then
- echo "$0: using rm -rf to overcome empty file & hardlink 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
- if [[ $LNF_VERBOSE == true ]]; then
+ if $verbose; then
echo "lnf: rm -rf -- ${to_remove[*]}"
fi
- rm -rf -- "${to_remove[@]}"
+ if ! $dry_run; then
+ rm -rf -- "${to_remove[@]}"
+ fi
fi
fi
$reset_extglob && shopt -u extglob
if $mkdir; then
- if ! mkdir -p "$dest_dir"; 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
- ln -s $nodir -- "$@"
+ if $verbose; then
+ echo "lnf: ln -s $nodir -- ${to_link[*]}"
+ fi
+ if ! $dry_run; then
+ ln -s $nodir -- "${to_link[@]}"
+ fi
}
lnf "$@"