#!/bin/bash # Copyright (C) 2014-2016 Ian Kelling # This program is under GPL v. 3 or later, see _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="Usage: lnf [OPTIONS] -T TARGET LINK_NAME (1st form) lnf [OPTIONS] TARGET (2nd form) lnf [OPTIONS] TARGET... DIRECTORY (3rd form) Create symlinks forcefully 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. -n|--dry-run Do verbose dry run. -v|--verbose Print commands which modify the filesystem. -h|--help Print help and exit. " 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 1 fi if [[ $nodir ]]; then if (( $# != 2 )); then 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 -a to_remove to_link local x ret prefix dest_dir grp target local mkdir=false if [[ $nodir ]]; then 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 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. dest_file="${target%%+(/)}" # remove any leading directory components, add prefix 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 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 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 "lnf: using rm -rf to overcome cross filesystem trash-put limitation" rm -rf -- "${to_remove[@]}" || return $? elif [[ $ret == 74 ]]; then 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 $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 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 "$@"