X-Git-Url: https://iankelling.org/git/?p=lnf;a=blobdiff_plain;f=lnf;h=0620b32929d87c19ef5f79217094a9012f3df3ce;hp=fa9b59937108e5b6514cd983d629c3d4f8bfc34b;hb=HEAD;hpb=15c4639b1ee74247ac931088eea77e3ea37ba7c3 diff --git a/lnf b/lnf index fa9b599..28f77ee 100755 --- a/lnf +++ b/lnf @@ -1,46 +1,181 @@ #!/bin/bash -# Copyright (C) 2014 Ian Kelling -# This program is under GPL v. 3 or later, see +# I, Ian Kelling, follow the GNU license recommendations at +# https://www.gnu.org/licenses/license-recommendations.en.html. They +# recommend that small programs, < 300 lines, be licensed under the +# Apache License 2.0. This file contains or is part of one or more small +# programs. If a small program grows beyond 300 lines, I plan to switch +# its license to GPL. +# Copyright 2024 Ian Kelling + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +_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 [--help] LN_ARGUMENTS... -Create symlinks conveniently and forcefully. -Remove existing file/links using trash-put or rm -rf if it is not available. -Create directory if needed. Finally, ln -s -- LN_ARGUMENTS" - if [[ $1 == --help ]]; then - echo "$help" - return 0 - fi - - local remove x - if type -P dircolors >/dev/null; then - remove=trash-put + 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 +rm -rf. 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 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 - remove="rm -rf" - fi - - if [[ $# -ge 3 && ! -d ${!#} ]]; then - mkdir -p "${!#}" - elif [[ $# -ge 2 && -d ${!#} ]]; then - local oldcwd=$PWD - cd ${!#} # last arg - for x in "${@:1:$(($#-1))}"; do # all but last arg - # remove any trailing slashes - x="${x%%+(/)}" - # remove any leading directory components - x="${x##*/}" - [[ -e "$x" || -L "$x" ]] && $remove "$x" - done - cd "$oldcwd" - elif [[ $# -eq 2 ]]; then - if [[ -e "$2" || -L "$2" ]]; then - $remove "$2" - elif [[ ! -d "$2/.." ]]; then - mkdir -p "$2/.." - fi - elif [[ $# -eq 1 ]]; then - [[ -e "${1##*/}" || -L "${1##*/}" ]] && $remove "${1##*/}" - fi - ln -s -- "$@" + 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 $verbose; then + echo "lnf: rm -rf -- ${to_remove[*]}" + fi + if ! $dry_run; then + rm -rf -- "${to_remove[@]}" + 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 "$@"