给定一个绝对路径或相对路径(在类unix系统中),我希望在解析任何中间符号链接之后确定目标的完整路径。同时还可以解析~用户名符号。

如果目标是一个目录,则可以将chdir()放入该目录,然后调用getcwd(),但我确实希望从shell脚本而不是编写C helper来完成此操作。不幸的是,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"。


当前回答

注意:我相信这是一个可靠的、可移植的、现成的解决方案,但由于这个原因,它总是很冗长。

下面是一个完全兼容POSIX的脚本/函数,因此是跨平台的(也适用于macOS,其readlink在10.12 (Sierra)仍然不支持-f) -它只使用POSIX shell语言特性和只使用POSIX兼容的实用程序调用。

它是GNU readlink -e (readlink -f的严格版本)的可移植实现。

您可以使用sh运行脚本,也可以在bash、ksh和zsh中获取该函数:

例如,在一个脚本中,你可以像下面这样使用它来获取运行脚本的真实源目录,并解析符号链接:

trueScriptDir=$(dirname -- "$(rreadlink "$0")")

Rreadlink脚本/函数定义:

出于感激,代码改编自这个回答。 我还在这里创建了一个基于bash的独立实用程序版本,您可以使用它进行安装 npm install rreadlink -g,如果你已经安装了Node.js。

#!/bin/sh

# SYNOPSIS
#   rreadlink <fileOrDirPath>
# DESCRIPTION
#   Resolves <fileOrDirPath> to its ultimate target, if it is a symlink, and
#   prints its canonical path. If it is not a symlink, its own canonical path
#   is printed.
#   A broken symlink causes an error that reports the non-existent target.
# LIMITATIONS
#   - Won't work with filenames with embedded newlines or filenames containing 
#     the string ' -> '.
# COMPATIBILITY
#   This is a fully POSIX-compliant implementation of what GNU readlink's
#    -e option does.
# EXAMPLE
#   In a shell script, use the following to get that script's true directory of origin:
#     trueScriptDir=$(dirname -- "$(rreadlink "$0")")
rreadlink() ( # Execute the function in a *subshell* to localize variables and the effect of `cd`.

  target=$1 fname= targetDir= CDPATH=

  # Try to make the execution environment as predictable as possible:
  # All commands below are invoked via `command`, so we must make sure that
  # `command` itself is not redefined as an alias or shell function.
  # (Note that command is too inconsistent across shells, so we don't use it.)
  # `command` is a *builtin* in bash, dash, ksh, zsh, and some platforms do not 
  # even have an external utility version of it (e.g, Ubuntu).
  # `command` bypasses aliases and shell functions and also finds builtins 
  # in bash, dash, and ksh. In zsh, option POSIX_BUILTINS must be turned on for
  # that to happen.
  { \unalias command; \unset -f command; } >/dev/null 2>&1
  [ -n "$ZSH_VERSION" ] && options[POSIX_BUILTINS]=on # make zsh find *builtins* with `command` too.

  while :; do # Resolve potential symlinks until the ultimate target is found.
      [ -L "$target" ] || [ -e "$target" ] || { command printf '%s\n' "ERROR: '$target' does not exist." >&2; return 1; }
      command cd "$(command dirname -- "$target")" # Change to target dir; necessary for correct resolution of target path.
      fname=$(command basename -- "$target") # Extract filename.
      [ "$fname" = '/' ] && fname='' # !! curiously, `basename /` returns '/'
      if [ -L "$fname" ]; then
        # Extract [next] target path, which may be defined
        # *relative* to the symlink's own directory.
        # Note: We parse `ls -l` output to find the symlink target
        #       which is the only POSIX-compliant, albeit somewhat fragile, way.
        target=$(command ls -l "$fname")
        target=${target#* -> }
        continue # Resolve [next] symlink target.
      fi
      break # Ultimate target reached.
  done
  targetDir=$(command pwd -P) # Get canonical dir. path
  # Output the ultimate target's canonical path.
  # Note that we manually resolve paths ending in /. and /.. to make sure we have a normalized path.
  if [ "$fname" = '.' ]; then
    command printf '%s\n' "${targetDir%/}"
  elif  [ "$fname" = '..' ]; then
    # Caveat: something like /var/.. will resolve to /private (assuming /var@ -> /private/var), i.e. the '..' is applied
    # AFTER canonicalization.
    command printf '%s\n' "$(command dirname -- "${targetDir}")"
  else
    command printf '%s\n' "${targetDir%/}/$fname"
  fi
)

rreadlink "$@"

关于安全问题:

Jarno在引用确保内置命令不会被同名的别名或shell函数所遮蔽的函数时,在注释中询问:

如果unalias或unset和[被设置为别名或shell函数会怎样?

rreadlink确保命令具有其原始含义的动机是使用它来绕过(良性的)方便别名和函数,这些别名和函数通常用于掩盖交互式shell中的标准命令,例如重新定义ls以包括最喜欢的选项。

我认为可以肯定地说,除非您正在处理一个不受信任的、恶意的环境,担心unalias或unset -或者,就此而言,do,…-被重新定义不是一个问题。

有一些函数必须依赖于它的原始意义和行为-没有办法绕过它。 类似posix的shell允许重新定义内置程序甚至语言关键字,这本质上是一种安全风险(而且编写偏执的代码通常是困难的)。

为了解决您的问题:

该函数依赖于unalias和unset具有其原始含义。以改变它们行为的方式将它们重新定义为shell函数将是一个问题;重新定义为别名 不必担心,因为引用(部分)命令名(例如\unalias)会绕过别名。

然而,shell关键字不能引用(while, for, if, do,…),虽然shell关键字优先于shell函数,但在bash和zsh中别名具有最高优先级,因此为了防止shell关键字重新定义,您必须使用它们的名称运行别名(尽管在非交互式bash shell(如脚本)中,默认情况下别名不会扩展-只有在首先显式调用shopt -s expand_aliases时)。

为了确保unalias(作为内置程序)具有其原始含义,您必须首先在其上使用\unset,这要求unset具有其原始含义:

Unset是一个内置的shell,所以为了确保它被这样调用,你必须确保它本身没有被重新定义为一个函数。虽然可以通过引用绕过别名形式,但不能绕过shell函数形式——catch 22。

因此,除非你可以依靠unset来获得它的原始含义,否则就我所知,没有保证的方法来防御所有恶意的重新定义。

其他回答

我最喜欢的一个是realpath foo

realpath - return the canonicalized absolute pathname

realpath  expands  all  symbolic  links  and resolves references to '/./', '/../' and extra '/' characters in the null terminated string named by path and
       stores the canonicalized absolute pathname in the buffer of size PATH_MAX named by resolved_path.  The resulting path will have no symbolic link, '/./' or
       '/../' components.
readlink -f "$path"

编者注:上述内容适用于GNU readlink和FreeBSD/PC-BSD/OpenBSD readlink,但不适用于10.11版本的OS X。 GNU readlink提供了额外的相关选项,例如-m用于解析符号链接,无论最终目标是否存在。

注意,自从GNU coreutils 8.15(2012-01-06)以来,有一个realpath程序可用,它不那么迟钝,而且比上面的程序更灵活。它还兼容同名的FreeBSD util。它还包括在两个文件之间生成相对路径的功能。

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"

在这里,我提出了一个我认为是跨平台(至少Linux和macOS)的解决方案,目前对我来说效果很好。

crosspath()
{
    local ref="$1"
    if [ -x "$(which realpath)" ]; then
        path="$(realpath "$ref")"
    else
        path="$(readlink -f "$ref" 2> /dev/null)"
        if [ $? -gt 0 ]; then
            if [ -x "$(which readlink)" ]; then
                if [ ! -z "$(readlink "$ref")" ]; then
                    ref="$(readlink "$ref")"
                fi
            else
                echo "realpath and readlink not available. The following may not be the final path." 1>&2
            fi
            if [ -d "$ref" ]; then
                path="$(cd "$ref"; pwd -P)"
            else
                path="$(cd $(dirname "$ref"); pwd -P)/$(basename "$ref")"
            fi
        fi
    fi
    echo "$path"
}

这里有一个macOS(仅?)解决方案。也许更适合最初的问题。

mac_realpath()
{
    local ref="$1"
    if [[ ! -z "$(readlink "$ref")" ]]; then
        ref="$(readlink "$1")"
    fi
    if [[ -d "$ref" ]]; then
        echo "$(cd "$ref"; pwd -P)"
    else
        echo "$(cd $(dirname "$ref"); pwd -P)/$(basename "$ref")"
    fi
}
readlink -e [filepath]

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

另一种方法:

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