Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@geoff-nixon
Last active April 18, 2019 03:47
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save geoff-nixon/1f23957288d371b75a2e to your computer and use it in GitHub Desktop.
Save geoff-nixon/1f23957288d371b75a2e to your computer and use it in GitHub Desktop.
Portable realpath(1) / readlink -f, written is portable POSIX C.
// So, this used to be a really terrible shell script I wrote years ago.
// Its was buggy in all kinds of corner cases, If you really need it, check
// out the revision history. Otherwise, if you have a functioning C compiler,
// you *really* should be using the system's realpath(3) function to do this.
// Here's a bare-bones version. To compile, just: `cc realpath.c -o realpath`
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[]) {
char *symlinkpath = argv[1];
char *actualpath = realpath(symlinkpath, NULL);
if (actualpath != NULL) {
realpath(symlinkpath, actualpath);
printf("%s", actualpath);
free(actualpath);
return 0;
} else {
return 1;
}
}
@hasufell
Copy link

hasufell commented Oct 30, 2018

If you use shellcheck you will see a lot of warnings in this script (especially the use of $@).

Additionally $(ls -ld "$@" | sed 's|.* -> ||') really makes this script unreliable, imo. ls is not meant to be used programmatically. The output is for humans, see https://github.com/koalaman/shellcheck/wiki/SC2012

This is a better solution for realpath imo:

# @FUNCTION: posix_realpath
# @USAGE: <file>
# @DESCRIPTION:
# Portably gets the realpath and prints it to stdout.
# This was initially inspired by
#   https://gist.github.com/tvlooy/cbfbdb111a4ebad8b93e
#   and
#   https://stackoverflow.com/a/246128
#
# If the file does not exist, just prints the argument unchanged.
# @STDOUT: realpath of the given file
# @RETURNS: 0 on success, 1 otherwise (e.g. internal error)
posix_realpath() {
    [ -z "$1" ] && return 1
    mysource=$1

    while [ -h "${mysource}" ]; do
        mydir="$( cd -P "$( dirname "${mysource}" )" > /dev/null 2>&1 && pwd )"
        mysource="$(readlink "${mysource}")"
        [ "${mysource%${mysource#?}}"x != '/x' ] && mysource="${mydir}/${mysource}"
    done
    mydir="$( cd -P "$( dirname "${mysource}" )" > /dev/null 2>&1 && pwd )"

    if [ -z "${mydir}" ] ; then
        (>&2 echo "${1}: Permission denied")
    elif [ ! -e "$1" ] ; then
        echo "${mysource}"
    else
        echo "${mydir%/}/$(basename "${mysource}")"
    fi

    unset mysource mydir posix_realpath_error
}

The error handling is not 100% the same though. E.g. "directory does not exist" vs "cannot enter directory".

@geoff-nixon
Copy link
Author

geoff-nixon commented Nov 29, 2018

@hasufell Yeah... that script was pretty terrible. You're quite right about parsing the output of ls, but you can do something similar safely, using cut on the output of stat -F.

I do like your script, though it still gets caught on corner cases like symlink loops and nonexistent relative paths. Its also a bit inefficient: I think you could do away with much of that loop if you just used pwd -P?

I've decided to take mine down; if someone wants to resurrect it, they can go through the history. :)

@hasufell
Copy link

hasufell commented Apr 18, 2019

I do like your script, though it still gets caught on corner cases like symlink loops and nonexistent relative paths.

--- script
+++ script
@@ -12,24 +12,30 @@
 # @RETURNS: 0 on success, 1 otherwise (e.g. internal error)
 posix_realpath() {
     [ -z "$1" ] && return 1
+    current_loop=0
+    max_loops=50
     mysource=$1
 
     while [ -h "${mysource}" ]; do
+        current_loop=$((current_loop+1))
         mydir="$( cd -P "$( dirname "${mysource}" )" > /dev/null 2>&1 && pwd )"
         mysource="$(readlink "${mysource}")"
         [ "${mysource%${mysource#?}}"x != '/x' ] && mysource="${mydir}/${mysource}"
+
+       if [ ${current_loop} -gt ${max_loops} ] ; then
+           (>&2 echo "${1}: Too many levels of symbolic links")
+           break
+       fi
     done
     mydir="$( cd -P "$( dirname "${mysource}" )" > /dev/null 2>&1 && pwd )"
 
     if [ -z "${mydir}" ] ; then
         (>&2 echo "${1}: Permission denied")
-    elif [ ! -e "$1" ] ; then
-        echo "${mysource}"
     else
         echo "${mydir%/}/$(basename "${mysource}")"
     fi
 
-    unset mysource mydir posix_realpath_error
+    unset current_loop max_loops mysource mydir
 }
 
 posix_realpath "$@"

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