1
\$\begingroup\$

I wrote a function to resolve the full path to the current Bash script, and I'm wondering if it's SOLID:

#!/usr/bin/env bash

scriptpath()
{
    result=${BASH_SOURCE[0]}

    # unfortunately readlink -f isn't available on Macs,
    # so resolve the symlinks manually
    while [ -h "$result" ]
    do
        symdir=$(dirname "$result")
        cd -P "$symdir"
        result=$(readlink "$result")
    done

    echo "$result"
}

echo "The path to the current script is: $(scriptpath)"

Is there anything I may have looked over in the function? I'm not sure because my answer seems quite different than the solutions listed here or here, and I'm pretty fuzzy on symlinks.

I basically want to know if my function (scriptpath) is any different than this on a Linux system:

readlink -f "${BASH_SOURCE[0]}" | xargs -0 dirname
\$\endgroup\$
  • \$\begingroup\$ Use -L instead of -h in your while test as -h has been deprecated (they want to drop it but it would break scripts so it remains for now. Probably since -h is so commonly used for help.) \$\endgroup\$ – DocSalvager May 3 '16 at 9:55
  • \$\begingroup\$ How can I get the behavior of GNU's readlink -f on a Mac? seems to cover this issue pretty well. \$\endgroup\$ – DocSalvager May 3 '16 at 10:03
1
\$\begingroup\$

Let's make this testable

With a small twist, the function can become testable. Instead of finding the real path of the current script, it would be better to generalize it to find the real path of any path:

real_path()
{
    result=$1

    # ...
}

Fails to follow relative symlinks correctly

Now that the script is testable, here's a test it will not pass:

mkdir -p /tmp/a
touch /tmp/a/b
(cd /tmp/a; test -L /tmp/a/c || ln -s ../a/b c)

echo $(real_path /tmp/a/c)

The output will be ../a/b instead of the expected /tmp/a/b. Basically if there are relative links in the chain of links recursively followed, the script will break.

Alternative implementation

This is what I use in production, and I haven't seen a corner case yet where it breaks:

real_path() {
  target=$1

  (
  while true; do
    cd "$(dirname "$target")"
    target=$(basename "$target")
    test -L "$target" || break
    target=$(readlink "$target")
  done

  echo "$(pwd -P)/$target"
  )
}

Notice that the portion where the function changes the working directory is wrapped within a (...) sub-shell. This is probably overkill, but I think it's good to eliminate side effects as a general principle.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.