How can I get the behavior of GNU's readlink -f on a Mac?

Viewed 158626

On Linux, the readlink utility accepts an option -f that follows additional links. This doesn't seem to work on Mac and possibly BSD based systems. What would the equivalent be?

Here's some debug information:

$ which readlink; readlink -f
/usr/bin/readlink
readlink: illegal option -f
usage: readlink [-n] [file ...]
26 Answers

readlink -f does two things:

  1. It iterates along a sequence of symlinks until it finds an actual file.
  2. It returns that file's canonicalized name—i.e., its absolute pathname.

If you want to, you can just build a shell script that uses vanilla readlink behavior to achieve the same thing. Here's an example. Obviously you could insert this in your own script where you'd like to call readlink -f

#!/bin/sh

TARGET_FILE=$1

cd `dirname $TARGET_FILE`
TARGET_FILE=`basename $TARGET_FILE`

# Iterate down a (possible) chain of symlinks
while [ -L "$TARGET_FILE" ]
do
    TARGET_FILE=`readlink $TARGET_FILE`
    cd `dirname $TARGET_FILE`
    TARGET_FILE=`basename $TARGET_FILE`
done

# Compute the canonicalized name by finding the physical path 
# for the directory we're in and appending the target file.
PHYS_DIR=`pwd -P`
RESULT=$PHYS_DIR/$TARGET_FILE
echo $RESULT

Note that this doesn't include any error handling. Of particular importance, it doesn't detect symlink cycles. A simple way to do this would be to count the number of times you go around the loop and fail if you hit an improbably large number, such as 1,000.

EDITED to use pwd -P instead of $PWD.

Note that this script expects to be called like ./script_name filename, no -f, change $1 to $2 if you want to be able to use with -f filename like GNU readlink.

You may be interested in realpath(3), or Python's os.path.realpath. The two aren't exactly the same; the C library call requires that intermediary path components exist, while the Python version does not.

$ pwd
/tmp/foo
$ ls -l
total 16
-rw-r--r--  1 miles    wheel  0 Jul 11 21:08 a
lrwxr-xr-x  1 miles    wheel  1 Jul 11 20:49 b -> a
lrwxr-xr-x  1 miles    wheel  1 Jul 11 20:49 c -> b
$ python -c 'import os,sys;print(os.path.realpath(sys.argv[1]))' c
/private/tmp/foo/a

I know you said you'd prefer something more lightweight than another scripting language, but just in case compiling a binary is insufferable, you can use Python and ctypes (available on Mac OS X 10.5) to wrap the library call:

#!/usr/bin/python

import ctypes, sys

libc = ctypes.CDLL('libc.dylib')
libc.realpath.restype = ctypes.c_char_p
libc.__error.restype = ctypes.POINTER(ctypes.c_int)
libc.strerror.restype = ctypes.c_char_p

def realpath(path):
    buffer = ctypes.create_string_buffer(1024) # PATH_MAX
    if libc.realpath(path, buffer):
        return buffer.value
    else:
        errno = libc.__error().contents.value
        raise OSError(errno, "%s: %s" % (libc.strerror(errno), buffer.value))

if __name__ == '__main__':
    print realpath(sys.argv[1])

Ironically, the C version of this script ought to be shorter. :)

  1. Install homebrew
  2. Run "brew install coreutils"
  3. Run "greadlink -f path"

greadlink is the gnu readlink that implements -f. You can use macports or others as well, I prefer homebrew.

Implementation

  1. Install brew

Follow the instructions at https://brew.sh/

  1. Install the coreutils package

brew install coreutils

  1. Create an Alias or Symlink

3a. Create an an alias (per user)

You can place your alias in ~/.bashrc, ~/.bash_profile, or wherever you are used to keeping your bash aliases. I personally keep mine in ~/.bashrc

alias readlink=greadlink

3b. Create a symbolic link (system wide)

ln -s /usr/local/bin/greadlink /usr/local/bin/readlink (credit: Izana)

This will create a symbolic link in /usr/local/bin while keeping the original readlink binary in tact. It works because the search for readlink will return 2 results. But the second in /usr/local/bin will take precedence.

e.g. which readlink

To undo this change simply unlink /usr/local/bin/readlink

Additional Tools

You can create similar aliases or symlinks for other coreutils such as gmv, gdu, gdf, and so on. But beware that the GNU behavior on a mac machine may be confusing to others used to working with native coreutils, or may behave in unexpected ways on your mac system.

Explanation

coreutils is a brew package that installs GNU/Linux core utilities which correspond to the Mac OSX implementation of them so that you can use those

You may find programs or utilties on your mac osx system which seem similar to Linux coreutils ("Core Utilities") yet they differ in some ways (such as having different flags).

This is because the Mac OSX implementation of these tools are different. To get the original GNU/Linux-like behavior you can install the coreutils package via the brew package management system.

This will install corresponding core utilities, prefixed by g. E.g. for readlink, you will find a corresponding greadlink program.

In order to make readlink perform like the GNU readlink (greadlink) implementation, you can create a simple alias or symbolic link after you install coreutils.

A lazy way that works for me,

$ brew install coreutils
$ ln -s /usr/local/bin/greadlink /usr/local/bin/readlink
$ which readlink
/usr/local/bin/readlink
/usr/bin/readlink

Since my work is used by people with non-BSD Linux as well as macOS, I've opted for using these aliases in our build scripts (sed included since it has similar issues):

##
# If you're running macOS, use homebrew to install greadlink/gsed first:
#   brew install coreutils
#
# Example use:
#   # Gets the directory of the currently running script
#   dotfilesDir=$(dirname "$(globalReadlink -fm "$0")")
#   alias al='pico ${dotfilesDir}/aliases.local'
##

function globalReadlink () {
  # Use greadlink if on macOS; otherwise use normal readlink
  if [[ $OSTYPE == darwin* ]]; then
    greadlink "$@"
  else
    readlink "$@"
  fi
}

function globalSed () {
  # Use gsed if on macOS; otherwise use normal sed
  if [[ $OSTYPE == darwin* ]]; then
    gsed "$@"
  else
    sed "$@"
  fi
}

Optional check you could add to automatically install homebrew + coreutils dependencies:

if [[ "$OSTYPE" == "darwin"* ]]; then
  # Install brew if needed
  if [ -z "$(which brew)" ]; then 
    /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"; 
  fi
  # Check for coreutils
  if [ -z "$(brew ls coreutils)" ]; then
    brew install coreutils
  fi
fi

I suppose to be truly "global" it needs to check others...but that probably comes close to the 80/20 mark.

POSIX compliant readlink -f implementation for POSIX shell scripts

https://github.com/ko1nksm/readlinkf

This is POSIX compliant (no bashism). It uses neither readlink nor realpath. I have verified that it is exactly the same by comparing with GNU readlink -f (see test results). It has error handling and good performance. You can safely replace from readlink -f. The license is CC0, so you can use it for any project.

This code is adopted in the bats-core project.

# POSIX compliant version
readlinkf_posix() {
  [ "${1:-}" ] || return 1
  max_symlinks=40
  CDPATH='' # to avoid changing to an unexpected directory

  target=$1
  [ -e "${target%/}" ] || target=${1%"${1##*[!/]}"} # trim trailing slashes
  [ -d "${target:-/}" ] && target="$target/"

  cd -P . 2>/dev/null || return 1
  while [ "$max_symlinks" -ge 0 ] && max_symlinks=$((max_symlinks - 1)); do
    if [ ! "$target" = "${target%/*}" ]; then
      case $target in
        /*) cd -P "${target%/*}/" 2>/dev/null || break ;;
        *) cd -P "./${target%/*}" 2>/dev/null || break ;;
      esac
      target=${target##*/}
    fi

    if [ ! -L "$target" ]; then
      target="${PWD%/}${target:+/}${target}"
      printf '%s\n' "${target:-/}"
      return 0
    fi

    # `ls -dl` format: "%s %u %s %s %u %s %s -> %s\n",
    #   <file mode>, <number of links>, <owner name>, <group name>,
    #   <size>, <date and time>, <pathname of link>, <contents of link>
    # https://pubs.opengroup.org/onlinepubs/9699919799/utilities/ls.html
    link=$(ls -dl -- "$target" 2>/dev/null) || break
    target=${link#*" $target -> "}
  done
  return 1
}

Please refer to the latest code. It may some fixed.

echo $(cd $(dirname file1) ; pwd -P)

I have simply pasted the following to the top of my bash scripts:

#!/usr/bin/env bash -e

declare script=$(basename "$0")
declare dirname=$(dirname "$0")
declare scriptDir
if [[ $(uname) == 'Linux' ]];then
    # use readlink -f
    scriptDir=$(readlink -f "$dirname")
else
    # can't use readlink -f, do a pwd -P in the script directory and then switch back
    if [[ "$dirname" = '.' ]];then
        # don't change directory, we are already inside
        scriptDir=$(pwd -P)
    else
        # switch to the directory and then switch back
        pwd=$(pwd)
        cd "$dirname"
        scriptDir=$(pwd -P)
        cd "$pwd"
    fi
fi

And removed all instances of readlink -f. $scriptDir and $script then will be available for the rest of the script.

While this does not follow all symlinks, it works on all systems and appears to be good enough for most use cases, it switches the directory into the containing folder, and then it does a pwd -P to get the real path of that directory, and then finally switch back to the original.

The paths to readlink are different between my system and yours. Please try specifying the full path:

/sw/sbin/readlink -f

Related