#!/bin/bash # 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="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 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 "$@"