How can I get the behavior of GNU's readlink -f on a Mac? [closed]
Asked Answered
R

26

452

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 ...]
Region answered 28/6, 2009 at 20:26 Comment(11)
A bit late, but your question is missing any mentioning of the shell you use. This is relevant, because readlink can be a builtin or an external command.Severalty
That would explain the difference perhaps. I'm fairly sure I used bash on both occasions though.Region
Why is this option illegal on Macs?Hy
@Hy - if I recall, for the "Unix" portion of the OS, Macs try to stick to POSIX and/or BSD derived tools and utilities. You can install GNU utilities on OS/X or adjust the commands to use "pure" Bourne shell (/bin/sh without bash extensions). For many developers these days shell scripts = Linux, GNU utilities and bash so portability issues appear.Arquit
I really wish Apple would make OS X support more default linux paths, and address things like this. They could do it without breaking anything, couldn't they?Hy
@Hy Well, they ship with Perl so ++ :-) ... open Terminal.app and type: touch myfile ; ln -s myfile otherfile ; perl -MCwd=abs_path -le 'print abs_path readlink(shift);' otherfile ... in my case I see: /Users/cito/myfile`. Added it to my response below. Cheers.Arquit
@Hy GNU's Not Unix :-) Stuff has been added. You can use a solution with pwd -P on OS X without installing anythingFredkin
@Hy readlink is GPLv3 and Apple won't touch anything GPLv3 because of the requirement to hand over cryptographic keys to a device in order to modify GPLv3 software on that device. gnu.org/licenses/… It will be one reason the default shell is no longer Bash but ZshFredkin
@JasonS but FreeBSD has had readlink -f since sometime between 2010 and 2012, depending on man page date vs. BSD release date, and that's not a GPL implementation. So Apple is just dragging its heels. And actually, readlink -f originated in OpenBSD 2.1 in 1997, as far as I can tell. So the first implementation wasn't even GNU. I can understand it not being in Snow Leopard, but I can't understand it not being in Mavericks/Sierra.Biflagellate
@WyattWard Fair point. The only Mac I have access to (10.11 Big Sur), man readlink date is 2003. So yeah it could be updated. And as you say not GPLv3 at all, so disregard my earlier comment about it being GPLv3. I was running a bit blind with so many people complaining why Mac OS is not GNU. No it's not. Never will be. But point remains you can get the job done though with pwd -P without wishing Mac OS was GNU Linux by using Homebrew. Yes shell scripts are not portable. Not unique to Mac OS.Fredkin
macOS 12.3 supports readlink -f.Ferial
B
207

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.

Bromal answered 12/7, 2009 at 20:51 Comment(14)
As far as I can tell, that won't work if a parent dir of the path is a symlink. Eg. if foo -> /var/cux, then foo/bar won't be resolved, because bar isn't a link, although foo is.Region
Ah. Yes. It's not as simple but you can update the above script to deal with that. I'll edit (rewrite, really) the answer accordingly.Bromal
Well, a link could be anywhere in the path. I guess the script could iterate over each part of the path, but it does become a bit complicated then.Region
A link earlier in the path shouldn't matter. All of the tools and system calls that operate on paths automatically follow symlinks in pathnames. In testing on my system, the above script and "readlink -f" produce the same results when there is a symlink in the middle of a path---either the argument or in another symlink. Can you provide an example where it's a problem?Bromal
Yes: mkdir a; mkdir a/b; mkdir x; ln -s ../a x/y. Now assuming that the above script is canonicalize, running ./canonicalize x/y/b does not give the same output as readlink -f x/y/b.Region
Thanks. The problem is that $PWD is giving us the logical working directory, based in the values in the symlinks that we've followed. We can get the real physical directory with 'pwd -P' It should compute it by chasing ".." up to the root of the file system. I'll update the script in my answer accordingly.Bromal
The answer given duplicates the effort of pwd.Cystine
This answer will not work if the symlink targets "/something/..": the result is "/something/.." (as .. is not a link, we are now in the something directory, and the basename is ..). The result of canonicalization by readlink -f is "/".Tempt
Great solution, but it doesn't deal properly with paths that need escaping, such as file or directory names with embedded spaces. To remedy that, use cd "$(dirname "$TARGET_FILE")" and TARGET_FILE=$(readlink "$TARGET_FILE") and TARGET_FILE=$(basename "$TARGET_FILE") in the appropriate places in the above code.Ronironica
If you want this script to return the path of the file that contains it, you must pass $0 instead of $1 in the first line. ($1 assumes you're trying to get the real path of another file, which you already have a path for).Hy
that assumes that the file or directory exists, which realpath and readlink deals withFinis
Should it cd back to the original working directory before teardown?Unproductive
All paths and filename variables need to be wrapped in quotes, like this: TARGET_FILE="`basename "$TARGET_FILE"`"Hinkle
Need to use cd -P to prevent paths like this/is/a/symlink/.. from breakingColombes
C
487

MacPorts and Homebrew provide a coreutils package containing greadlink (GNU readlink). Credit to Michael Kallweitt post in mackb.com.

brew install coreutils

greadlink -f file.txt
Concordia answered 27/10, 2010 at 9:5 Comment(7)
If you are calling readlink from places other than bash, another solution would be remove /usr/bin/readlink then create a link to /usr/local/bin/greadlink.Turino
Please don't, unless you a) write software for your own b) want to mess with others. Write it in such a way that it "just works" for anybody.Samsara
@ching Do this instead in your .bashrc: export PATH="/usr/local/opt/coreutils/libexec/gnubin:$PATH"Lasala
The question asks for the equivalent on a OS X or BSD system, this doesn't answer that. You'll also have to install Homebrew. You'll also be installing many things apart from GNU Readlink. If you're writing a script for standard OS X installs this answer won't help. You can use a combination of pwd -P and the readlink on OS X, without installing anythingFredkin
I wanted the GNU and other utils on my Mac that I believe do so well, without the prefix, and easily accessible from my user's PATH. Did the above starting with brew install <package> --default-names. Many times a question on this site has been answered slightly, or more so, outside the technical requirements of the asker, but within the similar situation, and has still been very helpful. topbug.net/blog/2013/04/14/…Sohn
@Sohn Not to discredit your choices and what you find useful, and that this answer may be a good answer to a different question, but I would still contend this is not a good answer to this particular question.Fredkin
then do alias readlink=greadlink in your rc file to continue using the name you're used to.Hinkle
B
207

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.

Bromal answered 12/7, 2009 at 20:51 Comment(14)
As far as I can tell, that won't work if a parent dir of the path is a symlink. Eg. if foo -> /var/cux, then foo/bar won't be resolved, because bar isn't a link, although foo is.Region
Ah. Yes. It's not as simple but you can update the above script to deal with that. I'll edit (rewrite, really) the answer accordingly.Bromal
Well, a link could be anywhere in the path. I guess the script could iterate over each part of the path, but it does become a bit complicated then.Region
A link earlier in the path shouldn't matter. All of the tools and system calls that operate on paths automatically follow symlinks in pathnames. In testing on my system, the above script and "readlink -f" produce the same results when there is a symlink in the middle of a path---either the argument or in another symlink. Can you provide an example where it's a problem?Bromal
Yes: mkdir a; mkdir a/b; mkdir x; ln -s ../a x/y. Now assuming that the above script is canonicalize, running ./canonicalize x/y/b does not give the same output as readlink -f x/y/b.Region
Thanks. The problem is that $PWD is giving us the logical working directory, based in the values in the symlinks that we've followed. We can get the real physical directory with 'pwd -P' It should compute it by chasing ".." up to the root of the file system. I'll update the script in my answer accordingly.Bromal
The answer given duplicates the effort of pwd.Cystine
This answer will not work if the symlink targets "/something/..": the result is "/something/.." (as .. is not a link, we are now in the something directory, and the basename is ..). The result of canonicalization by readlink -f is "/".Tempt
Great solution, but it doesn't deal properly with paths that need escaping, such as file or directory names with embedded spaces. To remedy that, use cd "$(dirname "$TARGET_FILE")" and TARGET_FILE=$(readlink "$TARGET_FILE") and TARGET_FILE=$(basename "$TARGET_FILE") in the appropriate places in the above code.Ronironica
If you want this script to return the path of the file that contains it, you must pass $0 instead of $1 in the first line. ($1 assumes you're trying to get the real path of another file, which you already have a path for).Hy
that assumes that the file or directory exists, which realpath and readlink deals withFinis
Should it cd back to the original working directory before teardown?Unproductive
All paths and filename variables need to be wrapped in quotes, like this: TARGET_FILE="`basename "$TARGET_FILE"`"Hinkle
Need to use cd -P to prevent paths like this/is/a/symlink/.. from breakingColombes
P
47

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. :)

Portingale answered 12/7, 2009 at 1:11 Comment(4)
Yes, realpath is indeed what I want. But it seems rather awkward that I have to compile a binary to get this function from a shell script.Region
Why not use the Python one-liner in the shell script then? (Not so different from a one-line call to readlink itself, is it?)Gamekeeper
python -c "import os,sys; print(os.path.realpath(os.path.expanduser(sys.argv[1])))" "${1}" works with paths like '~/.symlink'Kerriekerrigan
@troelskin I didn't realize Perl, Python, etc. were "allowed" !! ... in that case I'm going to addperl -MCwd=abs_path -le 'print abs_path readlink(shift);' to my answer :-)Arquit
N
40

A simple one-liner in perl that's sure to work almost everywhere without any external dependencies:

perl -MCwd -e 'print Cwd::abs_path shift' ~/non-absolute/file

Will dereference symlinks.

Usage in a script could be like this:

readlinkf(){ perl -MCwd -e 'print Cwd::abs_path shift' "$1";}
ABSPATH="$(readlinkf ./non-absolute/file)"
Nodab answered 4/7, 2014 at 10:28 Comment(1)
to get a trailing newline, add -l, like perl -MCwd -le 'print Cwd::abs_path shift' ~/non-absolute/fileEudora
D
32

You might need both a portable, pure shell implementation, and unit-test coverage, as the number of edge-cases for something like this is non-trivial.

See my project on Github for tests and full code. What follows is a synopsis of the implementation:

As Keith Smith astutely points out, readlink -f does two things: 1) resolves symlinks recursively, and 2) canonicalizes the result, hence:

realpath() {
    canonicalize_path "$(resolve_symlinks "$1")"
}

First, the symlink resolver implementation:

resolve_symlinks() {
    local dir_context path
    path=$(readlink -- "$1")
    if [ $? -eq 0 ]; then
        dir_context=$(dirname -- "$1")
        resolve_symlinks "$(_prepend_path_if_relative "$dir_context" "$path")"
    else
        printf '%s\n' "$1"
    fi
}

_prepend_path_if_relative() {
    case "$2" in
        /* ) printf '%s\n' "$2" ;;
         * ) printf '%s\n' "$1/$2" ;;
    esac 
}

Note that this is a slightly simplified version of the full implementation. The full implementation adds a small check for symlink cycles, as well as massages the output a bit.

Finally, the function for canonicalizing a path:

canonicalize_path() {
    if [ -d "$1" ]; then
        _canonicalize_dir_path "$1"
    else
        _canonicalize_file_path "$1"
    fi
}   

_canonicalize_dir_path() {
    (cd "$1" 2>/dev/null && pwd -P) 
}           
        
_canonicalize_file_path() {
    local dir file
    dir=$(dirname -- "$1")
    file=$(basename -- "$1")
    (cd "$dir" 2>/dev/null && printf '%s/%s\n' "$(pwd -P)" "$file")
}

That's it, more or less. Simple enough to paste into your script, but tricky enough that you'd be crazy to rely on any code that doesn't have unit tests for your use cases.

Descendant answered 9/4, 2014 at 18:34 Comment(3)
This is not POSIX - it uses the non-POSIX local keywordLobster
This is not equivalent to readlink -f, nor of realpath. Assuming a Mac with the coreutils formula installed (so with greadlink available), try this with ln -s ~/Documents /tmp/linkeddir; greadlink -f /tmp/linkeddir/.. prints your home directory (it resolved the /tmp/linkeddir link before applying the .. parent dir ref), but your code produces /private/tmp/ as /tmp/linkeddir/.. is not a symlink, but -d /tmp/linkeddir/.. is true and so cd /tmp/linkeddir/.. takes you to /tmp, whose canonical path is /private/tmp. Your code does what realpath -L does, instead.Siward
Above problem can be solved with cd -P in _canonicalize_dir_pathColombes
O
27
  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.

Oxygen answered 29/3, 2012 at 2:35 Comment(2)
I can't see any difference to the answer given in 2010 https://mcmap.net/q/13456/-how-can-i-get-the-behavior-of-gnu-39-s-readlink-f-on-a-mac-closedFredkin
If you need to use these commands with their normal names, you can add a "gnubin" directory to your PATH from your bashrc like: PATH="/usr/local/opt/coreutils/libexec/gnubin:$PATH"Frisk
I
21

I made a script called realpath personally which looks a little something like:

#!/usr/bin/env python
import os, sys
print(os.path.realpath(sys.argv[1]))
Innocence answered 5/11, 2009 at 6:5 Comment(3)
You should change that to sys.argv[1] if you want the script to print the realpath of the first argument. sys.argv[0] just prints the real path of the pyton script itself, which is not very useful.Warchaw
Here's the alias version, for ~/.profile: alias realpath="python -c 'import os, sys; print os.path.realpath(sys.argv[1])'"Scraggy
Great answer, and great one-liner @jwhitlock. Since the OP cited readlink -f, here's a modified version of @Scraggy 's alias to support possible -f flag: alias realpath="python -c 'import os, sys; print os.path.realpath(sys.argv[2] if sys.argv[1] == \"-f\" else sys.argv[1])'"Valona
G
14

What about this?

function readlink() {
  DIR="${1%/*}"
  (cd "$DIR" && echo "$(pwd -P)")
}
Grannia answered 28/2, 2012 at 14:44 Comment(5)
This is the only solution that works for me; I need to parse paths like ../../../ -> /Bitty
this can't work if you have a symlink /usr/bin/ for example like the alternatives system likes to have.Palestine
Does not work for individual files, only directories. GNU readlink works with directories, files, symlinks, etc.Valona
This actually does the job without reinventing the wheel. May not cover 100% cases but easily beats the other solutions for me.Silverware
You probably want to do DIR=$(dirname "$1") instead.Zirkle
C
12

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.

Capercaillie answered 20/5, 2019 at 11:22 Comment(2)
This solution does not work system-wide.Estuary
Thanks @KevinC I expanded my answer to include system wide option and explanation.Capercaillie
T
12

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
Tobey answered 27/1, 2020 at 21:11 Comment(3)
Scroll no further: this is the most concise and best answer on macOS.Carboy
Thank you, does not force me to remove original one. and /user/local/bin/ takes precedence over /user/bin/ file. Easy to undo by deleting /usr/local/bin/readlink linkWallflower
Works system wide.Estuary
A
6

FreeBSD and OSX have a version of statderived from NetBSD.

You can adjust the output with format switches (see the manual pages at the links above).

%  cd  /service
%  ls -tal 
drwxr-xr-x 22 root wheel 27 Aug 25 10:41 ..
drwx------  3 root wheel  8 Jun 30 13:59 .s6-svscan
drwxr-xr-x  3 root wheel  5 Jun 30 13:34 .
lrwxr-xr-x  1 root wheel 30 Dec 13 2013 clockspeed-adjust -> /var/service/clockspeed-adjust
lrwxr-xr-x  1 root wheel 29 Dec 13 2013 clockspeed-speed -> /var/service/clockspeed-speed
% stat -f%R  clockspeed-adjust
/var/service/clockspeed-adjust
% stat -f%Y  clockspeed-adjust
/var/service/clockspeed-adjust

Some OS X versions of stat may lack the -f%R option for formats. In this case -stat -f%Y may suffice. The -f%Y option will show the target of a symlink, whereas -f%R shows the absolute pathname corresponding to the file.

EDIT:

If you're able to use Perl (Darwin/OS X comes installed with recent verions of perl) then:

perl -MCwd=abs_path -le 'print abs_path readlink(shift);' linkedfile.txt

will work.

Arquit answered 17/9, 2014 at 1:1 Comment(5)
This is true on FreeBSD 10, but not on OS X 10.9.Cowgill
Good catch. The stat manual page cited is for OS/X 10.9. The R option to -f% is missing. Possibly stat -f%Y gives the desired output. I will adjust the answer. Note that the stat tool appeared in FreeBSD in version 4.10 and NetBSD in version 1.6.Arquit
Perl command does not work correctly on 10.15. Give it a file in current directory and it gives the current directory (without the file).Valona
stat -f%Y is equivalent to readlink on MacOS: prints target if argument is symlink or prints nothing and exit with error. The GNU readlink -f prints a path no matter if argument is symlink or not. Thus stat -f%Y is no way behaves like readlink -f.Heck
FreeBSD has had readlink -f since 2012, NetBSD since 2007, and OpenBSD since 1997. Apple is just using ten year old utilities and dragging its heels.Biflagellate
T
6

The easiest way to solve this problem and enable the functionality of readlink on Mac w/ Homebrew installed or FreeBSD is to install 'coreutils' package. May also be necessary on certain Linux distributions and other POSIX OS.

For example, in FreeBSD 11, I installed by invoking:

# pkg install coreutils

On MacOS with Homebrew, the command would be:

$ brew install coreutils

Not really sure why the other answers are so complicated, that's all there is to it. The files aren't in a different place, they're just not installed yet.

Tedi answered 6/1, 2017 at 21:28 Comment(3)
brew install coreutils --with-default-names to be more specific. Or you'll end up with "greadlink" instead...Earpiece
I can't see any difference to the answers from 2010 https://mcmap.net/q/13456/-how-can-i-get-the-behavior-of-gnu-39-s-readlink-f-on-a-mac-closed and 2012 https://mcmap.net/q/13456/-how-can-i-get-the-behavior-of-gnu-39-s-readlink-f-on-a-mac-closed The question is actually "... on Mac and possibly BSD based systems. What would the equivalent be?" I don't believe this is a very good answer to that question. Many reasons, but you may be targeting Mac OS machines without ability to install anything. pwd -P is pretty simple but it disappears amongst many other answers, including repeated ones, hence the downvote.Fredkin
FreeBSD had readlink -f since five years before you wrote this answer. So I'd suggest removing that part. Otherwise, good suggestions.Biflagellate
R
5

Here is a portable shell function that should work in ANY Bourne comparable shell. It will resolve the relative path punctuation ".. or ." and dereference symbolic links.

If for some reason you do not have a realpath(1) command, or readlink(1) this can be aliased.

which realpath || alias realpath='real_path'

Enjoy:

real_path () {
  OIFS=$IFS
  IFS='/'
  for I in $1
  do
    # Resolve relative path punctuation.
    if [ "$I" = "." ] || [ -z "$I" ]
      then continue
    elif [ "$I" = ".." ]
      then FOO="${FOO%%/${FOO##*/}}"
           continue
      else FOO="${FOO}/${I}"
    fi

    ## Resolve symbolic links
    if [ -h "$FOO" ]
    then
    IFS=$OIFS
    set `ls -l "$FOO"`
    while shift ;
    do
      if [ "$1" = "->" ]
        then FOO=$2
             shift $#
             break
      fi
    done
    IFS='/'
    fi
  done
  IFS=$OIFS
  echo "$FOO"
}

also, just in case anybody is interested here is how to implement basename and dirname in 100% pure shell code:

## http://www.opengroup.org/onlinepubs/000095399/functions/dirname.html
# the dir name excludes the least portion behind the last slash.
dir_name () {
  echo "${1%/*}"
}

## http://www.opengroup.org/onlinepubs/000095399/functions/basename.html
# the base name excludes the greatest portion in front of the last slash.
base_name () {
  echo "${1##*/}"
}

You can find updated version of this shell code at my google site: http://sites.google.com/site/jdisnard/realpath

EDIT: This code is licensed under the terms of the 2-clause (freeBSD style) license. A copy of the license may be found by following the above hyperlink to my site.

Ruprecht answered 26/4, 2010 at 0:10 Comment(3)
The real_path returns /filename instead of /path/to/filename on Mac OS X Lion in the bash shell.Cystine
@Barry The same happens on OSX Snow Leopard. Worse, successive calls to real_path() concatenate output!Redroot
@Dominique: not surprising, since the inside of the function isn't run in its own subshell ... ugly and fragile indeed.Severalty
M
4

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

Maura answered 6/1, 2022 at 8:58 Comment(3)
This should be the accepted answer as it solves the problem out-of-the-box, without writing complicated functions or installing additional dependencies. man readlink on Linux: -f canonicalize by following every symlink..., man pwd on macOS: -P Display the physical current workign directory (all symbolic links resolved)Innermost
The only portable answer that doesn't involve writing some crazy amount of extra code.Staub
Produces wrong output if file1 is a symlink.Impish
F
3

Begin Update

This is such a frequent problem that we have put together a Bash 4 library for free use (MIT License) called realpath-lib. This is designed to emulate readlink -f by default and includes two test suites to verify (1) that it works for a given unix system and (2) against readlink -f if installed (but this is not required). Additionally, it can be used to investigate, identify and unwind deep, broken symlinks and circular references, so it can be a useful tool for diagnosing deeply-nested physical or symbolic directory and file problems. It can be found at github.com or bitbucket.org.

End Update

Another very compact and efficient solution that does not rely on anything but Bash is:

function get_realpath() {

    [[ ! -f "$1" ]] && return 1 # failure : file does not exist.
    [[ -n "$no_symlinks" ]] && local pwdp='pwd -P' || local pwdp='pwd' # do symlinks.
    echo "$( cd "$( echo "${1%/*}" )" 2>/dev/null; $pwdp )"/"${1##*/}" # echo result.
    return 0 # success

}

This also includes an environment setting no_symlinks that provides the ability to resolve symlinks to the physical system. As long as no_symlinks is set to something, ie no_symlinks='on' then symlinks will be resolved to the physical system. Otherwise they will be applied (the default setting).

This should work on any system that provides Bash, and will return a Bash compatible exit code for testing purposes.

Folksy answered 8/10, 2013 at 14:43 Comment(0)
G
2

There are already a lot of answers, but none worked for me... So this is what I'm using now.

readlink_f() {
  local target="$1"
  [ -f "$target" ] || return 1 #no nofile

  while [ -L "$target" ]; do
    target="$(readlink "$target")" 
  done
  echo "$(cd "$(dirname "$target")"; pwd -P)/$target"
}
Gam answered 13/10, 2014 at 8:44 Comment(1)
This doesn't work if the $target is a path, it only works if it's a file.Selfidentity
K
2

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.

Kuth answered 21/1, 2019 at 7:27 Comment(0)
B
2

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.

Blackwell answered 7/3, 2020 at 17:14 Comment(0)
F
1

Better late than never, I suppose. I was motivated to develop this specifically because my Fedora scripts weren't working on the Mac. The problem is dependencies and Bash. Macs don't have them, or if they do, they are often somewhere else (another path). Dependency path manipulation in a cross-platform Bash script is a headache at best and a security risk at worst - so it's best to avoid their use, if possible.

The function get_realpath() below is simple, Bash-centric, and no dependencies are required. I uses only the Bash builtins echo and cd. It is also fairly secure, as everything gets tested at each stage of the way and it returns error conditions.

If you don't want to follow symlinks, then put set -P at the front of the script, but otherwise cd should resolve the symlinks by default. It's been tested with file arguments that are {absolute | relative | symlink | local} and it returns the absolute path to the file. So far we've not had any problems with it.

function get_realpath() {

if [[ -f "$1" ]]
then
    # file *must* exist
    if cd "$(echo "${1%/*}")" &>/dev/null
    then
        # file *may* not be local
        # exception is ./file.ext
        # try 'cd .; cd -;' *works!*
        local tmppwd="$PWD"
        cd - &>/dev/null
    else
        # file *must* be local
        local tmppwd="$PWD"
    fi
else
    # file *cannot* exist
    return 1 # failure
fi

# reassemble realpath
echo "$tmppwd"/"${1##*/}"
return 0 # success

}

You can combine this with other functions get_dirname, get_filename, get_stemname and validate_path. These can be found at our GitHub repository as realpath-lib (full disclosure - this is our product but we offer it free to the community without any restrictions). It also could serve as a instructional tool - it's well documented.

We've tried our best to apply so-called 'modern Bash' practices, but Bash is a big subject and I'm certain there will always be room for improvement. It requires Bash 4+ but could be made to work with older versions if they are still around.

Folksy answered 3/10, 2013 at 15:48 Comment(0)
S
0

I wrote a realpath utility for OS X which can provide the same results as readlink -f.


Here is an example:

(jalcazar@mac tmp)$ ls -l a
lrwxrwxrwx 1 jalcazar jalcazar 11  8月 25 19:29 a -> /etc/passwd

(jalcazar@mac tmp)$ realpath a
/etc/passwd


If you are using MacPorts, you can install it with the following command: sudo port selfupdate && sudo port install realpath.

Slatternly answered 8/11, 2014 at 14:55 Comment(0)
S
0

Truely platform-indpendent would be also this R-onliner

readlink(){ RScript -e "cat(normalizePath(commandArgs(T)[1]))" "$1";}

To actually mimic readlink -f <path>, $2 instead of $1 would need to be used.

Shopkeeper answered 10/12, 2015 at 20:17 Comment(1)
Except R is hardly on any systems by default. Not on Mac yet (as of 10.15) which is what this question is about.Valona
L
0

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.

Lidalidah answered 9/2, 2021 at 1:57 Comment(0)
R
-2

Perl has a readlink function (e.g. How do I copy symbolic links in Perl?). This works across most platforms, including OS X:

perl -e "print readlink '/path/to/link'"

For example:

$ mkdir -p a/b/c
$ ln -s a/b/c x
$ perl -e "print readlink 'x'"
a/b/c
Rude answered 27/8, 2011 at 11:59 Comment(1)
This does the same as readlink without the -f option - no recursion, no absolute path -, so you might as well use readlink directly.Ronironica
T
-2

The answer from @Keith Smith gives an infinite loop.

Here is my answer, which i use only on SunOS (SunOS miss so much POSIX and GNU commands).

It's a script file you have to put in one of your $PATH directories:

#!/bin/sh
! (($#)) && echo -e "ERROR: readlink <link to analyze>" 1>&2 && exit 99

link="$1"
while [ -L "$link" ]; do
  lastLink="$link"
  link=$(/bin/ls -ldq "$link")
  link="${link##* -> }"
  link=$(realpath "$link")
  [ "$link" == "$lastlink" ] && echo -e "ERROR: link loop detected on $link" 1>&2 && break
done

echo "$link"
Thomasinathomasine answered 24/9, 2014 at 15:53 Comment(0)
M
-3

This is what I use:

stat -f %N $your_path

Mystical answered 23/12, 2015 at 19:23 Comment(1)
This doesn't print the full path.Harbard
A
-6

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

/sw/sbin/readlink -f

Archidiaconal answered 30/6, 2009 at 13:46 Comment(3)
And what - exactly - is the difference between /sw/sbin and /usr/bin? And why do the two binaries differ?Region
Aha .. so fink contains a replacement for readlink that is gnu compatible. That's nice to know, but it doesn't solve my problem, since I need my script to run on other peoples machine, and I can't require them to install fink for that.Region
You are not answering the original question. Please rewrite your answer.Sandglass

© 2022 - 2024 — McMap. All rights reserved.