From 3e9196a08ed98f9d74cf4e8e0d3c02b3be3c8ef8 Mon Sep 17 00:00:00 2001 From: lara Date: Tue, 22 Dec 2020 23:09:06 +0100 Subject: [PATCH] Add convenience function for shell scripting --- bin/rreadlink | 55 +++++++++++++++++++++++++++++++++++++++++++++++++++ lib/utils.sh | 24 ++++++++++++++++++++++ 2 files changed, 79 insertions(+) create mode 100755 bin/rreadlink create mode 100644 lib/utils.sh diff --git a/bin/rreadlink b/bin/rreadlink new file mode 100755 index 0000000..deb1aa1 --- /dev/null +++ b/bin/rreadlink @@ -0,0 +1,55 @@ +#!/bin/sh + +# Function by mklement0 +# https://stackoverflow.com/a/29835459/5309963 +# The following, POSIX-compliant shell function implements what +# GNU's readlink -e does and is a reasonably robust solution +# that only fails in two rare edge cases: +# - paths with embedded newlines (very rare) +# - filenames containing literal string -> (also rare) + +( # Execute the function in a *subshell* to localize variables and the effect of `cd`. + + target=$1 fname= targetDir= CDPATH= + + # Try to make the execution environment as predictable as possible: + # All commands below are invoked via `command`, so we must make sure that `command` + # itself is not redefined as an alias or shell function. + # (Note that command is too inconsistent across shells, so we don't use it.) + # `command` is a *builtin* in bash, dash, ksh, zsh, and some platforms do not even have + # an external utility version of it (e.g, Ubuntu). + # `command` bypasses aliases and shell functions and also finds builtins + # in bash, dash, and ksh. In zsh, option POSIX_BUILTINS must be turned on for that + # to happen. + { \unalias command; \unset -f command; } >/dev/null 2>&1 + [ -n "$ZSH_VERSION" ] && options[POSIX_BUILTINS]=on # make zsh find *builtins* with `command` too. + + while :; do # Resolve potential symlinks until the ultimate target is found. + [ -L "$target" ] || [ -e "$target" ] || { command printf '%s\n' "ERROR: '$target' does not exist." >&2; return 1; } + command cd "$(command dirname -- "$target")" # Change to target dir; necessary for correct resolution of target path. + fname=$(command basename -- "$target") # Extract filename. + [ "$fname" = '/' ] && fname='' # !! curiously, `basename /` returns '/' + if [ -L "$fname" ]; then + # Extract [next] target path, which may be defined + # *relative* to the symlink's own directory. + # Note: We parse `ls -l` output to find the symlink target + # which is the only POSIX-compliant, albeit somewhat fragile, way. + target=$(command ls -l "$fname") + target=${target#* -> } + continue # Resolve [next] symlink target. + fi + break # Ultimate target reached. + done + targetDir=$(command pwd -P) # Get canonical dir. path + # Output the ultimate target's canonical path. + # Note that we manually resolve paths ending in /. and /.. to make sure we have a normalized path. + if [ "$fname" = '.' ]; then + command printf '%s\n' "${targetDir%/}" + elif [ "$fname" = '..' ]; then + # Caveat: something like /var/.. will resolve to /private (assuming /var@ -> /private/var), i.e. the '..' is applied + # AFTER canonicalization. + command printf '%s\n' "$(command dirname -- "${targetDir}")" + else + command printf '%s\n' "${targetDir%/}/$fname" + fi +) diff --git a/lib/utils.sh b/lib/utils.sh new file mode 100644 index 0000000..60ca9c2 --- /dev/null +++ b/lib/utils.sh @@ -0,0 +1,24 @@ +die() { + # Use notify-send to send errors when not in a terminal + # [ -t 0 ] only works outside of pipes + [ -t 0 ] || notify-send "$@" && >&2 echo "$@" + exit 1 +} + +# Output error to stderr and to graphical notification +notify_err() { + tee /dev/fd/2 | xargs -n1 -d "\n" notify-send +} + +assert_exists() { + for c in $@; do + which "$c" > /dev/null 2>&1 || die "$c doesn't appear to be installed" + done +} + +check_exists() { + for c in $@; do + which "$c" > /dev/null 2>&1 || return 1 + done +} +