I'm getting increasingly fascinated by the trouble that is getting the real path to a file in Unixy filesystems. That is, the path to a file that has no symbolic links on any of its components. You don't see what the big deal is? Check https://stackoverflow.com/questions/284662/how-do-you-normalize-a-file-path-in-bash. Now... please note that, as far as pure shell solutions go, none of them are correct.
I've seen plenty of examples of this, and they work fine, for values of fine that do not include nested symlinking. For
example, sbt-extras
has this:
get_script_path () {
local path="$1"
[[ -L "$path" ]] || { echo "$path" ; return; }
local target="$(readlink "$path")"
if [[ "${target:0:1}" == "/" ]]; then
echo "$target"
else
echo "${path%/*}/$target"
fi
}
Now compare that to what I use on my .bash_profile
, adapted, I think from Kafka scripts, or an answer on one of the
Stack Exchange sites:
function getTruePath() {
local DIR
local SOURCE="$1"
while [ -h "$SOURCE" ]; do # resolve $SOURCE until the file is no longer a symlink
DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )"
SOURCE="$(readlink "$SOURCE")"
# if $SOURCE was a relative symlink, we need to resolve it relative to the path where the
# symlink file was located
[[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE"
done
(cd -P "$(dirname "${SOURCE}")" && pwd)
}
Mind you, their output is slightly different, but the difference here is that it handles relative links to return an
absolute path, and it loops until it has something that is not a symlink. Of course, there's an edge case there which
I ignored, but I see it handled on sbt
startup script:
realpath () {
(
TARGET_FILE="$1"
FIX_CYGPATH="$2"
cd "$(dirname "$TARGET_FILE")"
TARGET_FILE=$(basename "$TARGET_FILE")
COUNT=0
while [ -L "$TARGET_FILE" -a $COUNT -lt 100 ]
do
TARGET_FILE=$(readlink "$TARGET_FILE")
cd "$(dirname "$TARGET_FILE")"
TARGET_FILE=$(basename "$TARGET_FILE")
COUNT=$(($COUNT + 1))
done
# make sure we grab the actual windows path, instead of cygwin's path.
if [[ "x$FIX_CYGPATH" != "x" ]]; then
echo "$(cygwinpath "$(pwd -P)/$TARGET_FILE")"
else
echo "$(pwd -P)/$TARGET_FILE"
fi
)
}
Now, I find that fascinated, because not only it handled infinite symlink loops (or very long ones), though it doesn't treat it as an error condition, but it has a case I was completely unaware of: cygwin! Now, mind you, I never liked cygwin, and now that WLS exists, I don't see any point of using it. But, if you write code to be run by others, the only true prediction you can make about it is that someone somewhere sometime will run it under any of the possibilies that exist.
The SBT script sources a shell "library", which uses this code:
declare SCRIPT=$0
while [ -h "$SCRIPT" ] ; do
ls=$(ls -ld "$SCRIPT")
# Drop everything prior to ->
link=$(expr "$ls" : '.*-> \(.*\)$')
if expr "$link" : '/.*' > /dev/null; then
SCRIPT="$link"
else
SCRIPT=$(dirname "$SCRIPT")/"$link"
fi
done
That's, uh, awful? And to think sbt
itself has such a great implementation of it!