关于MacOS:如何在Mac上获取GNU的readlink-f的行为?

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

在Linux上,readlink实用程序接受附加链接后面的选项-f。这似乎不适用于Mac和可能基于BSD的系统。等价物是什么?

以下是一些调试信息:

1
2
3
4
$ which readlink; readlink -f
/usr/bin/readlink
readlink: illegal option -f
usage: readlink [-n] [file ...]


MacPorts和自制提供了包含greadlink的coreutils包(GNU readlink)。感谢Michael Kallweitt在mackb.com上的帖子。

1
2
3
brew install coreutils

greadlink -f file.txt


readlink -f做了两件事:

  • 它沿着一系列符号链接迭代,直到找到一个实际文件。
  • 它返回文件的规范化名称,即绝对路径名。
  • 如果您愿意,您可以构建一个shell脚本,它使用普通的readlink行为来实现相同的功能。下面是一个例子。很明显,您可以在自己的脚本中插入这个命令,您可以在其中调用readlink -f

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    #!/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

    请注意,这不包括任何错误处理。特别重要的是,它不检测符号链接循环。一个简单的方法是计算你在循环中循环的次数,如果你碰到一个不可能大的数字,比如1000,就会失败。

    编辑为使用pwd -P而不是$PWD

    请注意,如果您希望能够像gnu readlink那样使用-f filename,那么这个脚本应该像./script_name filename,而不是-f,将$1更改为$2


    您可能对realpath(3)或python的os.path.realpath感兴趣。两者并不完全相同;C库调用要求存在中间路径组件,而Python版本则不存在。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    $ 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

    我知道你说过你喜欢比其他脚本语言更轻量级的语言,但是为了防止编译二进制代码是不可忍受的,你可以使用python和ctypes(在Mac OS X 10.5上可用)来包装库调用:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    #!/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])

    讽刺的是,这个脚本的C版本应该更短。:)


    我不想再继续使用另一个实现,但是我需要a)一个可移植的纯shell实现,和b)单元测试覆盖率,因为这样的边缘案例数量是不平凡的。

    有关测试和完整代码,请参阅我在GitHub上的项目。以下是实施概要:

    正如基思·史密斯敏锐地指出的那样,readlink -f做了两件事:1)递归地解析符号链接,2)将结果规范化,因此:

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

    首先,symlink解析器实现:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    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
    '"$1"
        fi
    }

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

    注意,这是一个稍微简化的完整实现版本。完整的实现增加了对symlink循环的小检查,并稍微按摩输出。

    最后,用于规范化路径的函数:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    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
    '"$(pwd -P)""$file")
    }

    差不多就是这样。简单到可以粘贴到您的脚本中,但是复杂到可以让您疯狂地依赖任何没有单元测试的代码来处理您的用例。


  • 安装自制
  • 运行"BREW安装coreutils"
  • 运行"greadlink-f path"
  • greadlink是实现-f的GNU readlink。您也可以使用MacPorts或其他,我更喜欢自制。


    我制作了一个名为realpath的脚本,看起来有点像:

    1
    2
    3
    #!/usr/bin/env python
    import os.sys
    print os.path.realpath(sys.argv[1])


    Perl中的一个简单的一行程序,在没有任何外部依赖项的情况下几乎可以在任何地方工作:

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

    将取消引用符号链接。

    脚本中的用法如下:

    1
    2
    readlinkf(){ perl -MCwd -e 'print Cwd::abs_path shift'"$1";}
    ABSPATH="$(readlinkf ./non-absolute/file)"


    这个怎么样?

    1
    2
    3
    4
    function readlink() {
      DIR="${1%/*}"
      (cd"$DIR" && echo"$(pwd -P)")
    }


    freebsd和osx有一个源自netbsd的stat版本。

    您可以使用格式开关调整输出(请参阅上面链接处的手册页面)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    %  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

    stat的一些OS X版本可能缺少格式的-f%R选项。在这种情况下,-stat -f%Y就足够了。-f%Y选项将显示符号链接的目标,而-f%R选项将显示与文件对应的绝对路径名。

    编辑:

    如果您能够使用Perl(Darwin/OSX安装了最新版本的perl),那么:

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

    会工作。


    这里有一个可移植的shell函数,它可以在任何Bourne类似的shell中工作。它将解析相对路径标点"…或者"取消引用符号链接。

    如果出于某种原因,您没有realpath(1)命令或readlink(1),则可以使用别名。

    1
    which realpath || alias realpath='real_path'

    享受:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    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"
    }

    另外,如果有人感兴趣,这里还介绍了如何用100%纯shell代码实现basename和dirname:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    ## 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##*/}"
    }

    你可以在我的谷歌网站上找到这个shell代码的更新版本:http://sites.google.com/site/jdisnard/realpath

    编辑:此代码是根据2条款(freebsd样式)许可证的条款授权的。通过访问上述"我的网站"超链接,可以找到许可证的副本。


    要解决此问题并启用Mac w/homebrew installed或freebsd上的readlink功能,最简单的方法是安装"coreutils"包。在某些Linux发行版和其他POSIX操作系统上也可能是必需的。

    例如,在FreeBsd 11中,我通过调用以下命令来安装:

    # pkg install coreutils

    在带有自制的MacOS上,命令为:

    $ brew install coreutils

    不太清楚为什么其他的答案如此复杂,这就是全部。这些文件不在另一个地方,只是还没有安装。


    开始更新

    这是一个非常常见的问题,我们已经构建了一个免费使用的bash 4库(MIT许可证),称为realpath lib。这是为了在默认情况下模拟readlink-f而设计的,包括两个测试套件来验证(1)它对给定的UNIX系统有效,(2)如果安装了readlink-f(但这不是必需的)。此外,它还可以用于调查、识别和展开深度、中断的符号链接和循环引用,因此它可以成为诊断深度嵌套的物理或符号目录和文件问题的有用工具。它可以在github.com或bitback.org上找到。

    结束更新

    另一个非常紧凑和高效的解决方案是:

    1
    2
    3
    4
    5
    6
    7
    8
    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

    }

    这还包括设置no_symlinks的环境,该环境提供了解析物理系统的符号链接的能力。只要将no_symlinks设置为某个值,即no_symlinks='on',则符号链接将解析为物理系统。否则将应用它们(默认设置)。

    这应该适用于任何提供bash的系统,并将返回与bash兼容的退出代码以进行测试。


    已经有很多答案了,但没有一个对我有用…这就是我现在使用的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    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"
    }


    由于我的工作被非BSD Linux和MacOS用户使用,我选择在构建脚本中使用这些别名(包括sed,因为它有类似的问题):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    ##
    # 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
    }

    可选检查您可以添加以自动安装homebrew+coreutils依赖项:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    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

    我认为这是真正的"全球性",它需要检查其他人……但这可能接近80/20分。


    真正的平台独立也将是这个R-onliner

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

    要真正模仿readlink -f ,需要使用$2而不是$1。


    我想迟到总比不迟到好。我有动力专门开发这个,因为我的Fedora脚本不在Mac上工作。问题是依赖性和bash。Mac没有,或者如果有,它们通常在其他地方(另一条路径)。跨平台bash脚本中的依赖路径操作充其量是一件令人头疼的事情,最坏的情况下是一个安全风险——因此,如果可能的话,最好避免使用它们。

    下面的get realpath()函数很简单,以bash为中心,不需要依赖项。我只使用bash内置的echo和cd。它也是相当安全的,因为在方法的每个阶段都要测试所有内容,并且它返回错误条件。

    如果您不想遵循symlinks,那么将set-p放在脚本的前面,否则cd默认应该解析symlinks。它通过文件参数绝对相对符号链接本地进行测试,并返回文件的绝对路径。到目前为止,我们还没有遇到任何问题。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    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

    }

    您可以将此函数与其他函数结合使用:get u dirname、get u filename、get u stemname和validate u path。这些可以在我们的Github存储库中找到,作为realpath lib(完全公开-这是我们的产品,但我们免费提供给社区,没有任何限制)。它还可以作为一种教学工具——它有很好的文档记录。

    我们已经尽力应用所谓的"现代bash"实践,但是bash是一个大主题,我相信总会有改进的空间。它需要bash 4+,但是如果旧版本仍然存在的话,可以使用它们。


    我为OSX编写了一个realpath实用程序,它可以提供与readlink -f相同的结果。

    下面是一个例子:

    1
    2
    3
    4
    5
    (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

    如果您使用的是MacPorts,则可以使用以下命令进行安装:sudo port selfupdate && sudo port install realpath


    这是我使用的:

    stat -f %N $your_path


    我的系统和您的系统之间的readlink路径是不同的。请尝试指定完整路径:

    /sw/sbin/readlink -f


    Perl有一个readlink函数(例如,如何在Perl中复制符号链接?)。这适用于大多数平台,包括OS X:

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

    例如:

    1
    2
    3
    4
    $ mkdir -p a/b/c
    $ ln -s a/b/c x
    $ perl -e"print readlink 'x'"
    a/b/c


    @keith smith给出了一个无限循环。

    这是我的答案,我只在sunos上使用(sunos会错过很多posix和gnu命令)。

    这是一个脚本文件,您必须将其放入$path目录之一:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    #!/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"