如何在shell脚本中解决符号链接问题

263

给定一个在类Unix系统中的绝对或相对路径,我想确定在解析任何中间符号链接后的目标的完整路径。同时解析~username表示方法将获得额外奖励。

如果目标是一个目录,则可能可以chdir()进入该目录然后调用getcwd(),但我真的想从shell脚本中执行此操作,而不是编写一个C辅助程序。不幸的是,shell往往会试图隐藏符号链接的存在(这是OS X上的bash):

$ ls -ld foo bar
drwxr-xr-x   2 greg  greg  68 Aug 11 22:36 bar
lrwxr-xr-x   1 greg  greg   3 Aug 11 22:36 foo -> bar
$ cd foo
$ pwd
/Users/greg/tmp/foo
$

我想要的是一个resolve()函数,当从上述示例中的tmp目录执行时,resolve("foo")应返回"/Users/greg/tmp/bar"。

21个回答

469
readlink -f "$path"
编辑备注:上述内容适用于 GNU readlinkFreeBSD/PC-BSD/OpenBSD readlink,但截至 10.11 版本,不适用于 OS X。

请注意,自 GNU coreutils 8.15(2012-01-06)以来,有一个名为realpath的程序可用,比上述方法更易懂且更加灵活。它还与 FreeBSD 的实用工具同名。它还包括生成两个文件之间相对路径的功能。

realpath $path

[管理员从评论区添加的内容,由halloleo提供。——danorton]

对于Mac OS X(至少到10.11.x),请使用不带-f选项的readlink命令:

readlink $path

编辑备注:该方法无法递归解析符号链接,因此无法报告最终目标;例如,如果有一个指向b的符号链接a,而b又指向c,则仅会报告b(并不保证其作为绝对路径输出)。

在OS X上使用以下perl命令来填补缺失的readlink -f功能:

perl -MCwd -le 'print Cwd::abs_path(shift)' "$path"

6
这在Mac OS X上不起作用 - 请参见https://dev59.com/UnNA5IYBdhLWcg3wL62x - Bkkbrad
13
在OS X上,您可以使用Homebrew安装coreutils。它将安装为“grealpath”。 - Kief
11
readlink 在 OSX 上可以使用,但需要另一种语法:readlink $path不要 加上 -f - halloleo
2
readlink无法解除多层符号链接,它只能一次解除一层。 - Magnus
2
不,Solaris没有这些工具。您需要为您的系统构建GNU coreutils。 - pixelbeat
显示剩余11条评论

102

根据标准,pwd -P 应该返回已解析符号链接的路径。

unistd.h 中获得的 C 函数 char *getcwd(char *buf, size_t size) 应具有相同的行为。

getcwd pwd


30
这只适用于(当前)目录吗?如果目标是一个文件,它将不会给出任何结果... - dala
5
如果链接已经失效,你就无法将其作为当前路径。 - Tom Howard
8
回顾一下,需要说明的是:这个答案只适用于非常有限的情况,即当所关注的符号链接指向一个实际存在的_目录_时;另外,在调用pwd -P之前,你必须先cd到该目录。 换句话说,它不能让你解析(查看)指向文件的符号链接或者无效的符号链接的目标,并且对于解析已存在目录符号链接,你需要做额外的工作(恢复以前的工作目录或将cdpwd -P调用局部化到子shell中)。 - mklement0
很遗憾,我在寻找解决文件而非目录的方法。 - Martin
正如其他人所指出的那样,这并没有真正回答问题。@pixelbeat在下面的回答中做到了。 - erstaples
显示剩余2条评论

28

"pwd -P" 看起来可以用于获取当前目录,但如果您需要实际可执行文件的名称,则我认为这并没有帮助。以下是我的解决方案:

#!/bin/bash

# get the absolute path of the executable
SELF_PATH=$(cd -P -- "$(dirname -- "$0")" && pwd -P) && SELF_PATH=$SELF_PATH/$(basename -- "$0")

# resolve symlinks
while [[ -h $SELF_PATH ]]; do
    # 1) cd to directory of the symlink
    # 2) cd to the directory of where the symlink points
    # 3) get the pwd
    # 4) append the basename
    DIR=$(dirname -- "$SELF_PATH")
    SYM=$(readlink "$SELF_PATH")
    SELF_PATH=$(cd "$DIR" && cd "$(dirname -- "$SYM")" && pwd)/$(basename -- "$SYM")
done

23

我最喜欢的命令之一是realpath foo

realpath - 返回规范化的绝对路径名
realpath 将null结尾的字符串path中所有符号链接展开并解析'/./'、'/../'和额外的'/'字符,并将规范化的绝对路径名存储在大小为PATH_MAX的缓冲区resolved_path中。所得到的路径将不包含任何符号链接、'/./'或'/../'组件。

在Debian(etch及以后版本)中,该命令可在realpath包中使用。 - Phil Ross
2
realpath现在(2012年1月)是coreutils的一部分,并且向后兼容debian和BSD变体。 - pixelbeat
1
我在Centos 6上使用GNU coreutils 8.4.31,但没有realpath。我在Unix & Linux上遇到了几个其他人,他们的GNU coreutils包中没有 realpath。因此,它似乎不仅取决于版本。 - toxalot
我更喜欢使用realpath而不是readlink,因为它提供了选项标志,例如--relative-to - arr_sea

10
readlink -e [filepath]

看起来正是你所需要的-它接受任意路径,解析所有符号链接,并返回“真实”路径-而且它是“标准*nix”,可能所有系统都已经拥有


刚被它咬了一口。它在 Mac 上不起作用,我正在寻找替代品。 - dhill

6

另一种方法:

# Gets the real path of a link, following all links
myreadlink() { [ ! -h "$1" ] && echo "$1" || (local link="$(expr "$(command ls -ld -- "$1")" : '.*-> \(.*\)$')"; cd $(dirname $1); myreadlink "$link" | sed "s|^\([^/].*\)\$|$(dirname $1)/\1|"); }

# Returns the absolute path to a command, maybe in $PATH (which) or not. If not found, returns the same
whereis() { echo $1 | sed "s|^\([^/].*/.*\)|$(pwd)/\1|;s|^\([^/]*\)$|$(which -- $1)|;s|^$|$1|"; } 

# Returns the realpath of a called command.
whereis_realpath() { local SCRIPT_PATH=$(whereis $1); myreadlink ${SCRIPT_PATH} | sed "s|^\([^/].*\)\$|$(dirname ${SCRIPT_PATH})/\1|"; } 

我需要在myreadlink()函数中使用cd命令,因为它是一个递归函数,会进入到每个目录直到找到链接。如果找到链接,就返回实际路径,然后sed将替换该路径。 - Keymon

5

将给定的解决方案结合起来,知道readlink在大多数系统上都可用,但需要不同的参数,在OSX和Debian上对我很有效。我不确定BSD系统。也许条件需要是[[ $OSTYPE != darwin* ]],仅从OSX中排除-f

#!/bin/bash
MY_DIR=$( cd $(dirname $(readlink `[[ $OSTYPE == linux* ]] && echo "-f"` $0)) ; pwd -P)
echo "$MY_DIR"

4

以下是如何使用内联Perl脚本获取MacOS / Unix中文件的实际路径:

FILE=$(perl -e "use Cwd qw(abs_path); print abs_path('$0')")

同样地,要获取符号链接文件的目录:
DIR=$(perl -e "use Cwd qw(abs_path); use File::Basename; print dirname(abs_path('$0'))")

3

你的路径是一个目录,还是可能是一个文件?如果它是一个目录,那么就很简单:

(cd "$DIR"; pwd -P)

然而,如果它可能是一个文件,那么这种方法就不起作用了:
DIR=$(cd $(dirname "$FILE"); pwd -P); echo "${DIR}/$(readlink "$FILE")"

因为符号链接可能会解析为相对或完整路径。
在脚本中,我需要找到真实路径,以便我可以引用与其一起安装的配置或其他脚本,我使用以下方法:
SOURCE="${BASH_SOURCE[0]}"
while [ -h "$SOURCE" ]; do # resolve $SOURCE until the file is no longer a symlink
  DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )"
  SOURCE="$(readlink "$SOURCE")"
  [[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE" # if $SOURCE was a relative symlink, we need to resolve it relative to the path where the symlink file was located
done

你可以将SOURCE设置为任何文件路径。基本上,只要该路径是符号链接,它就会解析该符号链接。技巧在于循环的最后一行。如果解析的符号链接是绝对路径,它将使用该路径作为SOURCE。但是,如果它是相对路径,它将在其前面添加DIR,这是通过我首先描述的简单技巧解析为实际位置的。

3

常见的Shell脚本经常需要找到它们的“home”目录,即使它们被调用为符号链接。因此,脚本必须从$0中找到它们的“真实”位置。

cat `mvn`

在我的系统上,打印出一个包含以下内容的脚本,这应该是你所需要的良好提示。
if [ -z "$M2_HOME" ] ; then
  ## resolve links - $0 may be a link to maven's home
  PRG="$0"

  # need this for relative symlinks
  while [ -h "$PRG" ] ; do
    ls=`ls -ld "$PRG"`
    link=`expr "$ls" : '.*-> \(.*\)$'`
    if expr "$link" : '/.*' > /dev/null; then
      PRG="$link"
    else
      PRG="`dirname "$PRG"`/$link"
    fi
  done

  saveddir=`pwd`

  M2_HOME=`dirname "$PRG"`/..

  # make it fully qualified
  M2_HOME=`cd "$M2_HOME" && pwd`

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接