如何在Mac上获取GNU的readlink -f的行为?

时间:2009-06-28 20:26:34

标签: macos freebsd sh

在Linux上,readlink实用程序接受一个跟随其他链接的选项-f。这似乎不适用于Mac和可能基于BSD的系统。相当于什么?

以下是一些调试信息:

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

25 个答案:

答案 0 :(得分:383)

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

brew install coreutils

greadlink -f file.txt

答案 1 :(得分:156)

readlink -f做了两件事:

  1. 它沿着一系列符号链接进行迭代,直到找到实际文件为止。
  2. 返回该文件的规范化名称,即其绝对路径名。
  3. 如果您愿意,您可以构建一个使用vanilla readlink行为的shell脚本来实现相同的功能。这是一个例子。显然,您可以在自己的脚本中插入此内容,以便调用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
    

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

    已编辑以使用pwd -P代替$PWD

    请注意,如果您希望能够与{{1}一起使用,则希望此脚本被调用为./script_name filename,而不是-f,将$1更改为$2像GNU readlink。

答案 2 :(得分:42)

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

$ 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上提供)来包装库调用:< / p>

#!/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版本应该更短。 :)

答案 3 :(得分:25)

我讨厌继续使用另一个实现,但我需要a)便携式,纯shell实现,以及b)单元测试覆盖率,作为数字像这样的边缘案例非平凡

有关测试和完整代码,请参阅my project on Github。以下是实施的概要:

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

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

首先,符号链接解析器实现:

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 
}

请注意,这是the full implementation的略微简化版本。 完整实现会为符号链接周期添加一个小小的检查,并稍微按摩一下输出。

最后,规范化路径的功能:

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")
}

它或多或少。很简单,可以粘贴到您的脚本中,但是非常棘手,以至于依赖任何没有针对您的用例进行单元测试的代码都会让您感到疯狂。

答案 4 :(得分:24)

  1. 安装homebrew
  2. 运行“brew install coreutils”
  3. 运行“greadlink -f path”
  4. greadlink是实现-f的gnu readlink。您也可以使用macports或其他人,我更喜欢自制软件。

答案 5 :(得分:20)

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

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

将取消引用符号链接。

脚本中的用法可能是这样的:

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

答案 6 :(得分:17)

我亲自创建了一个名为realpath的脚本,看起来有点像:

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

答案 7 :(得分:11)

这个怎么样?

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

答案 8 :(得分:8)

FreeBSDOSX的版本为stat,源自NetBSD。

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

%  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

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

修改

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

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

会奏效。

答案 9 :(得分:7)

这是一个便携式shell函数,可以在 ANY Bourne可比shell中使用。 它将解决相对路径标点符号“..或”。和解除引用的符号链接。

如果由于某种原因你没有realpath(1)命令,或者readlink(1)这可能是别名。

which realpath || alias realpath='real_path'

享受:

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:

## 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##*/}"
}

您可以在我的google网站上找到此shell代码的更新版本:http://sites.google.com/site/jdisnard/realpath

编辑: 此代码根据2子句(freeBSD样式)许可条款获得许可。 可以通过以上链接到我的网站找到许可证的副本。

答案 10 :(得分:6)

解决此问题并在Mac上安装Homebrew或FreeBSD时启用readlink功能的最简单方法是安装'coreutils'软件包。在某些Linux发行版和其他POSIX OS上也可能是必需的。

例如,在FreeBSD 11中,我是通过调用:

安装的

# pkg install coreutils

在MacOS with Homebrew上,命令为:

$ brew install coreutils

不确定为什么其他答案如此复杂,这就是它的全部内容。这些文件不在同一个地方,它们还没有安装。

答案 11 :(得分:4)

对我有用的一种懒惰方式,

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

答案 12 :(得分:3)

已经有很多答案,但没有一个对我有用......所以这就是我现在正在使用的。

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"
}

答案 13 :(得分:3)

开始更新

这是一个经常出现的问题,我们将一个名为 realpath-lib 的免费使用的Bash 4库(MIT许可证)放在一起。这是默认情况下模拟 readlink -f ,包括两个测试套件,用于验证(1)它是否适用于给定的unix系统,以及(2)针对 readlink -f 如果安装(但这不是必需的)。此外,它还可用于调查,识别和展开深层,破损的符号链接和循环引用,因此它可以成为诊断深层嵌套的物理或符号目录和文件问题的有用工具。它可以在github.combitbucket.org找到。

结束更新

另一个非常紧凑和高效的解决方案,除了Bash之外不依赖于任何东西:

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兼容的退出代码以进行测试。

答案 14 :(得分:1)

真正与平台相关的也是这个R-onliner

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

要实际模仿readlink -f <path>,需要使用$ 2而不是$ 1.

答案 15 :(得分:1)

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

##
# 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依赖项的可选检查:

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标记。

答案 16 :(得分:1)

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

下面的函数get_realpath()很简单,以Bash为中心,不需要依赖项。我只使用Bash builtins echo cd 。它也相当安全,因为在每个阶段都会对所有内容进行测试,并返回错误条件。

如果您不想关注符号链接,请将 set -P 放在脚本的前面,否则 cd 应默认解析符号链接。它已经使用{absolute |的文件参数进行了测试亲戚|符号链接| local},它返回文件的绝对路径。到目前为止,我们没有任何问题。

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_dirname,get_filename,get_stemname和validate_path结合使用。这些可以在我们的GitHub存储库中找到realpath-lib(完全披露 - 这是我们的产品,但我们向社区免费提供它,没有任何限制)。它也可以作为一种教学工具 - 它有很好的记录。

我们已尽力应用所谓的“现代Bash”做法,但Bash是一个很大的主题,我相信总会有改进的余地。它需要Bash 4+,但如果它们还在,可以使用旧版本。

答案 17 :(得分:1)

POSIX Shell脚本的符合POSIX的readlink -f实现

https://github.com/ko1nksm/readlinkf

这是POSIX兼容的(无bashism)。它既不使用readlink也不使用realpath。通过与GNU readlink -f(请参阅test results)进行比较,我已经证实它是完全相同的。它具有错误处理和良好的性能。您可以从readlink -f安全地进行替换。许可证为CC0,因此您可以将其用于任何项目。

bats-core项目中采用了此代码。

# 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
}

请参考最新代码。可能有些固定。

答案 18 :(得分:0)

我写了a realpath utility for OS X,它可以提供与readlink -f相同的结果。


这是一个例子:

(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

答案 19 :(得分:0)

说明

coreutils是一个brew软件包,用于安装与Mac OSX实现相对应的GNU / Linux核心实用程序,以便您可以使用这些

您可能会在mac osx系统上找到与Linux coreutils(“ Core Utilities”)相似的程序或实用程序,但它们在某些方面有所不同(例如,具有不同的标志)。

这是因为这些工具的Mac OSX实现不同。要获得原始的类似GNU / Linux的行为,您可以通过coreutils软件包管理系统安装brew软件包。

这将安装相应的核心实用程序,前缀为g。例如。对于readlink,您将找到一个相应的greadlink程序。

为了使readlink的执行类似于GNU readlinkgreadlink)的实现,可以在安装coreutils之后创建一个简单的别名。

实施

  1. 安装brew

按照https://brew.sh/

上的说明进行操作
  1. 安装coreutils软件包

brew install coreutils

  1. 创建别名

您可以将别名放在〜/ .bashrc,〜/ .bash_profile或任何用于保留bash别名的位置。我个人将我的档案保存在〜/ .bashrc

alias readlink=greadlink

您可以为其他coreutils(例如gmv,gdu,gdf等)创建相似的别名。但是请注意,Mac机器上的GNU行为可能会与过去使用本机coreutils的其他人混淆,或者可能在您的mac系统上以意外的方式出现。

答案 20 :(得分:0)

我只是将以下内容粘贴到我的 bash 脚本顶部:

folder3

并删除了 #!/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 的所有实例。 readlink -f$scriptDir 然后将可用于脚本的其余部分。

虽然这并不遵循所有符号链接,但它适用于所有系统并且对于大多数用例似乎已经足够好,它将目录切换到包含文件夹,然后执行 $script 以获取真正的那个目录的路径,然后最后切换回原来的。

答案 21 :(得分:-1)

这就是我使用的:

stat -f %N $your_path

答案 22 :(得分:-2)

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

  

/sw/sbin/readlink -f

答案 23 :(得分:-2)

@Keith Smith的答案给出了一个无限循环。

这是我的答案,我只在SunOS上使用(SunOS错过了太多的POSIX和GNU命令)。

这是一个脚本文件,你必须放在一个$ PATH目录中:

#!/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"

答案 24 :(得分:-2)

Perl具有readlink功能(例如How do I copy symbolic links in Perl?)。这适用于大多数平台,包括OS X:

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

例如:

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