如何在Mac上获得GNU's readlink -f的行为?

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

下面是一些调试信息:

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

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

/sw/sbin/readlink -f

你可能对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上可用)来包装库调用:

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

readlink -f做两件事:

  1. 它沿着符号链接序列迭代,直到找到一个实际的文件。
  2. 它返回该文件的规范化名称—,它的绝对路径名。

如果您愿意,您可以构建一个shell脚本,使用普通readlink行为来实现相同的功能。举个例子。显然,你可以在自己的脚本中插入它,在那里调用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),就会失败。

EDITED使用pwd -P代替$PWD

注意,这个脚本希望被调用为./script_name filename,而不是-f,如果你想能够像GNU readlink一样与-f filename一起使用,将$1更改为$2

我个人做了一个叫realpath的脚本,看起来有点像:

#!/usr/bin/env python
import os, sys
print(os.path.realpath(sys.argv[1]))
这是一个可移植的shell函数,应该在任何 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##*/}"
}

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

< p >编辑: 此代码是根据2-clause (freeBSD风格)许可的条款授权的。 通过上面的超链接到我的网站,可以找到许可证的副本

MacPorts和Homebrew提供了包含greadlink (GNU readlink)的coreutils包。这要归功于Michael Kallweitt在mackb.com上的帖子。

brew install coreutils


greadlink -f file.txt

Perl有一个readlink函数(例如如何在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

这个呢?

function readlink() {
DIR="${1%/*}"
(cd "$DIR" && echo "$(pwd -P)")
}
  1. 安装自酿酒
  2. 运行“brew install coreutils”
  3. 执行" grelink -f path"

Greadlink是实现-f的gnu readlink。你也可以使用macports或其他工具,我更喜欢自制的。

我想,迟到总比不到好。我之所以特别开发它,是因为我的Fedora脚本无法在Mac上运行。问题在于依赖关系和Bash。mac没有,或者如果有,它们通常在其他地方(另一条路径)。跨平台Bash脚本中的依赖路径操作在最好的情况下是令人头痛的,在最坏的情况下是存在安全风险的——因此最好尽可能避免使用它们。

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

如果你不想遵循符号链接,那么将设置- p放在脚本的前面,否则cd应该默认解析符号链接。它已经用文件参数{absolute | relative | symlink | 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+,但如果旧版本仍然存在,则可以使其与旧版本一起工作。

开始更新

这是一个经常出现的问题,因此我们将Bash 4库(MIT许可证)放在一起免费使用,称为realpath-lib。它被设计为默认模拟指向- f,包括两个测试套件,以验证(1)它适用于给定的unix系统,(2)如果安装了指向- 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兼容的退出代码用于测试。

你可能既需要一个可移植的纯shell实现,也需要单元测试覆盖率,因为这样的边例的数量是非平凡的

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

正如Keith Smith敏锐地指出的那样,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
}

注意,这是全面实施的一个稍微简化的版本。完整的实现增加了一个符号链接循环的小检查,以及按摩输出一点。

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

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

差不多就是这样。简单到可以粘贴到您的脚本中,但也非常棘手,如果您依赖于任何用例中没有单元测试的代码,那就太疯狂了。

一个简单的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)"

FreeBSDOSX有一个来自NetBSD的__abc0版本。

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

%  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

将工作。

来自@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"

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

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

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

< br >

下面是一个例子:

(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

< br >

如果您正在使用MacPorts,您可以使用以下命令安装它:sudo port selfupdate && sudo port install realpath

真正的平台独立也是r -online

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

要真正模拟readlink -f <path>,需要使用$2而不是$1。

这是我所使用的:

stat -f %N $your_path

解决这个问题并在安装了Homebrew或FreeBSD的Mac上启用readlink功能的最简单方法是安装“coreutils”包。在某些Linux发行版和其他POSIX操作系统上可能也是必需的。

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

# pkg install coreutils

在使用Homebrew的MacOS上,命令将是:

$ brew install coreutils

不太确定为什么其他答案如此复杂,这就是它的全部。文件并没有在不同的地方,只是还没有安装。

由于我的工作被非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
}

可选检查,您可以添加自动安装家酿 + 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的标记。

实现

  1. 安装酿造

按照https://brew.sh/的说明操作

  1. 安装coreutils包

brew install coreutils

  1. 创建别名或符号链接

3 a。创建一个别名(每个用户)

您可以将别名放在~/中。bashrc,(~ /。Bash_profile,或者任何您习惯保存bash别名的地方。我个人把我的文件保存在~/.bashrc中

alias readlink=greadlink

3 b。创建一个符号链接(系统范围)

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

这将在/usr/local/bin中创建一个符号链接,同时保留原始的readlink二进制文件。它可以工作,因为搜索readlink将返回2个结果。但是/usr/local/bin中的第二个将优先。

例如 which readlink

要撤消此更改,只需unlink /usr/local/bin/readlink

额外的工具

您可以为其他coreutil创建类似的别名或符号链接,例如gmv、gdu、gdf等等。但是要注意,GNU在mac机器上的行为可能会让其他习惯使用本机coreutils的人感到困惑,或者在mac系统上可能会以意想不到的方式表现。

解释

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

你可能会发现你的mac osx系统上的程序或实用程序看起来类似于Linux coreutils(“核心实用程序”),但它们在某些方面有所不同(例如具有不同的标志)。

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

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

为了使readlink像GNU readlink (greadlink)实现一样执行,您可以在安装coreutils后创建一个简单的别名或符号链接。

一种适合我的偷懒方式,

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

POSIX兼容的readlink -f实现POSIX shell脚本

https://github.com/ko1nksm/readlinkf

这是POSIX兼容的(没有bashism)。它既不使用readlink也不使用realpath。我已经通过与GNU readlink -f(见测试结果)比较验证了它是完全相同的。它具有错误处理和良好的性能。你可以安全地从readlink -f替换。许可证是CC0,因此您可以将其用于任何项目。

本代码在bat -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
}

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

我已经简单地粘贴以下到我的bash脚本的顶部:

#!/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将可用于脚本的其余部分。

虽然这并不适用于所有符号链接,但它适用于所有系统,并且似乎对大多数用例足够好,它将目录切换到包含文件夹,然后执行pwd -P以获得该目录的真实路径,然后最终切换回原始目录。

Echo $(cd $(dirname file1);pwd - p)