You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

144 lines
5.5 KiB
Bash

#!/bin/sh
# Sets REPLY to the absolute, well-formed path of a physical directory that
# contains (or would contain) the supplied path. (Equivalent to the dirname of
# realpath.resolved "$1".) Always succeeds.
realpath_location(){ realpath_follow "$1"; realpath_absolute "$REPLY" ".."; }
# Sets REPLY to the absolute, well-formed path of $1, with symlinks in the
# final path portion resolved. (Equivalent to the realpath.absolute of
# realpath.follow "$1".) Always succeeds. The result is not a symlink unless it
# is inaccessible or part of a symlink cycle.
realpath_resolved(){ realpath_follow "$1"; realpath_absolute "$REPLY"; }
realpath_dirname() { REPLY=$(dirname "$1"); }
realpath_basename() { REPLY=$(basename "$1"); }
# Sets REPLY to the first non-symlink (or last accessible, non-looping symlink)
# in the symlink chain beginning at $1. Replies with the unchanged $1 if it is
# not a symlink. Always succeeds. The result is not a symlink unless it is
# inaccessible or part of a symlink cycle. (Note: the result can be a relative
# path, if both $1 and all the symlinks in the chain are relative. Use
# realpath.resolved instead if you want a guaranteed-absolute path.)
realpath_follow() {
while [ -L "$1" ] && target=$(readlink -- "$1"); do
realpath_dirname "$1"
# Resolve relative to symlink's directory
if [ "$REPLY" = "." ]; then
REPLY=$target
else
case "$target" in
/*)
REPLY=$target;;
*)
REPLY=$REPLY/$target;;
esac
fi
# Break out if we found a symlink loop
for target; do [ "$REPLY" = "$target" ] && break 2; done
# Add to the loop-detect list and tail-recurse
set -- "$REPLY" "$@"
done
REPLY="$1"
}
# Sets REPLY to the absolute, well-formed combination of the supplied path(s).
# Always succeeds.
#
# Each path may be absolute or relative. The resulting path is the combination
# of the last absolute path in the list supplied, combined with any relative
# paths that follow it. If no absolute paths are given, the relative paths are
# processed relative to $PWD -- so passing zero arguments simply returns $PWD.
#
# Relative path parts are resolved logically rather than physically. That is to
# say, .. is processed by removing elements from the path string, rather than
# by inspecting the filesystem. (So symlinks are not processed in any way, and
# the existence or accessibility of the files and directories is irrelevant:
# with the exception of defaulting to $PWD, the result is obtained solely via
# string manipulation of the supplied paths.)
#
# As per POSIX, bash, Python, and other path handling libraries, absolute paths
# beginning with exactly two slashes are treated specially. The string returned
# by realpath.absolute will begin with two slashes if the last absolute path
# argument began with exactly two slashes; otherwise the result will begin with
# only one slash, no matter how many slashes the last absolute path argument
# began with.
realpath_absolute() {
REPLY=$PWD
while [ $# -gt 0 ]; do
if echo "$1" | grep -E '^(//[^/]*)$' >/dev/null 2>&1; then
REPLY=//
first=$(echo "$1" | cut -c3-)
shift
set -- "$first" "$@"
continue
fi
case $1 in
/*)
REPLY=/
first=$(echo "$1" | sed -e 's/^\/\+//')
shift
set -- "$first" "$@"
;;
*/*)
front=${1%%/*}
back=$(echo "$1" | sed -e "s/^$front\\/\\+//")
shift
set -- "$front" "$back" "$@"
;;
''|.)
shift
;;
..)
realpath_dirname "$REPLY"
shift
;;
*) REPLY="${REPLY%/}/$1"; shift ;;
esac
done
}
# Sets REPLY to the fully canonicalized form of $1, recursively resolving
# symlinks in every part of the path where that can be done, roughly equivalent
# to realpath -m or readlink -m. Always succeeds, but potentially rather slow,
# depending on how many directories are symlinks.
#
# You don't really need this function unless you are trying to determine
# whether divergent paths lead to the "same" file. For use cases that don't
# involve comparing paths, realpath.resolved should be sufficient, or perhaps
# even realpath.absolute. (Note, too, that using canonical paths can result in
# user confusion, since users then have to reconcile their inputs with your
# outputs!)
realpath_canonical() {
realpath_follow "$1"; set -- "$REPLY" # $1 is now resolved
realpath_basename "$1"; set -- "$1" "$REPLY" # $2 = basename $1
realpath_dirname "$1"
[ "$REPLY" != "$1" ] && realpath_canonical "$REPLY"; # recurse unless root
realpath_absolute "$REPLY" "$2"; # combine canon parent w/basename
}
# Sets REPLY to the shortest relative path from basedir to path. basedir
# defaults to $PWD if not supplied. Always succeeds.
#
# The path and basedir are preprocessed with realpath.absolute, so they can be
# relative paths, or absolute ones containing relative components. (The result
# is identical to calling Python's os.path.relpath function with the same
# arguments, but is much faster than even the fork to start Python would be.)
#
# The main use case for this function is portably creating relative symlinks
# without needing ln --relative or realpath --relative-to. Specifically, if you
# are doing ln -s somepath somedir/link, you can make it relative using
# realpath.relative somepath somedir; ln -s "$REPLY" somedir/link.
realpath_relative() {
target=""
realpath_absolute "$1"
shift
set -- "$REPLY" "$@"
realpath_absolute "${2-$PWD}" X
while realpath_dirname "$REPLY"; [ "$1" != "$REPLY" ] && [ "$1" = "${1#${REPLY%/}/}" ]; do
target=../$target
done
[ "$1" = "$REPLY" ] && REPLY=${target%/} || REPLY="$target${1#${REPLY%/}/}"
REPLY=${REPLY:-.}
}