Skip to content

Instantly share code, notes, and snippets.

@dcsobral
Last active May 29, 2019 18:13
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dcsobral/b49fdd45b7226884a62624147703a0eb to your computer and use it in GitHub Desktop.
Save dcsobral/b49fdd45b7226884a62624147703a0eb to your computer and use it in GitHub Desktop.
Real Path

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.

Post Note

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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment