I needed a realpath
replacement on OS X, one that operates correctly on paths with symlinks and parent references just like readlink -f
would. This includes resolving symlinks in the path before resolving parent references; e.g. if you have installed the homebrew coreutils
bottle, then run:
$ ln -s /var/log/cups /tmp/linkeddir
$ greadlink -f /tmp/linkeddir/..
/private/var/log
Note that readlink -f
has resolved /tmp/linkeddir
before resolving the ..
parent dir reference. Of course, there is no readlink -f
on Mac either.
So as part of the a bash implementation for realpath
I re-implemented what a GNUlib canonicalize_filename_mode(path, CAN_ALL_BUT_LAST)
function call does, in Bash 3.2; this is also the function call that GNU readlink -f
makes:
set -euo pipefail
_contains() {
local elem value
value="$1"
shift
for elem in "$@"; do
if [[ $elem == "$value" ]]; then
return 0
fi
done
return 1
}
_canonicalize_filename_mode() {
local path result component seen
seen=()
path="$1"
result="/"
if [[ $path != /* ]]; then
result="$PWD"
fi
while [[ -n $path ]]; do
component="${path%%/*}"
case "$component" in
'')
path="${path:1}" ;;
.)
path="${path:1}" ;;
..)
if [[ $result != "/" ]]; then
result="${result%/*}"
fi
path="${path:2}" ;;
*)
if [[ $result != */ ]]; then
result="$result/"
fi
result="$result$component"
path="${path:${#component}}"
if [[ $path =~ [^/] && ! -e $result ]]; then
echo "$1: No such file or directory" >&2
return 1
fi
if [[ -L $result ]]; then
if _contains "$result" "${seen[@]+"${seen[@]}"}"; then
# we've seen this link before, abort
echo "$1: Too many levels of symbolic links" >&2
return 1
fi
seen+=("$result")
path="$(readlink "$result")$path"
if [[ $path = /* ]]; then
# if the link is absolute, restart the result from /
result="/"
elif [[ $result != "/" ]]; then
# otherwise remove the basename of the link from the result
result="${result%/*}"
fi
elif [[ $path =~ [^/] && ! -d $result ]]; then
# otherwise all but the last element must be a dir
echo "$1: Not a directory" >&2
return 1
fi
;;
esac
done
echo "$result"
}
It includes circular symlink detection, exiting if the same (intermediary) path is seen twice.
If all you need is readlink -f
, then you can use the above as:
readlink() {
if [[ $1 != -f ]]; then
command readlink "$@"
return
fi
local path result seenerr
shift
seenerr=
for path in "$@"; do
if ! result=$(_canonicalize_filename_mode "$path" 2>/dev/null); then
seenerr=1
continue
fi
echo "$result"
done
if [[ $seenerr ]]; then
return 1;
fi
}
For realpath
, I also needed --relative-to
and --relative-base
support, which give you relative paths after canonicalizing:
_realpath() {
local relative_to relative_base seenerr path
relative_to=
relative_base=
seenerr=
while [[ $# -gt 0 ]]; do
case $1 in
"--relative-to="*)
relative_to=$(_canonicalize_filename_mode "${1#*=}")
shift 1;;
"--relative-base="*)
relative_base=$(_canonicalize_filename_mode "${1#*=}")
shift 1;;
*)
break;;
esac
done
if [[
-n $relative_to
&& -n $relative_base
&& ${relative_to#${relative_base}/} == "$relative_to"
]]; then
relative_to=
relative_base=
elif [[ -z $relative_to && -n $relative_base ]]; then
relative_to="$relative_base"
fi
for path in "$@"; do
if ! real=$(_canonicalize_filename_mode "$path"); then
seenerr=1
continue
fi
if [[
-n $relative_to
&& (
-z $relative_base || ${real#${relative_base}/} != "$real"
)
]]; then
local common_part parentrefs
common_part="$relative_to"
parentrefs=
while [[ ${real#${common_part}/} == "$real" ]]; do
common_part="$(dirname "$common_part")"
parentrefs="..${parentrefs:+/$parentrefs}"
done
if [[ $common_part != "/" ]]; then
real="${parentrefs:+${parentrefs}/}${real#${common_part}/}"
fi
fi
echo "$real"
done
if [[ $seenerr ]]; then
return 1
fi
}
if ! command -v realpath > /dev/null 2>&1; then
realpath() { _realpath "$@"; }
fi
I included unit tests in my Code Review request for this code.
$( cd "$(dirname "$0")" ; pwd -P )
– Jason Sbrew install coreutils
– Vojtěch