给定一个绝对路径或相对路径(在类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"。


当前回答

试试这个:

cd $(dirname $([ -L $0 ] && readlink -f $0 || echo $0))

其他回答

您的路径是目录还是文件?如果它是一个目录,它很简单:

(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之前为它,通过我首先描述的简单技巧将其解析为一个真实位置。

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

C函数char *getcwd(char *buf, size_t size)从unistd.h应该有相同的行为。

getcwd 松材线虫病

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

下面是一个完全兼容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来获得它的原始含义,否则就我所知,没有保证的方法来防御所有恶意的重新定义。

这是Bash中的符号链接解析器,无论链接是目录还是非目录都可以工作:

function readlinks {(
  set -o errexit -o nounset
  declare n=0 limit=1024 link="$1"

  # If it's a directory, just skip all this.
  if cd "$link" 2>/dev/null
  then
    pwd -P
    return 0
  fi

  # Resolve until we are out of links (or recurse too deep).
  while [[ -L $link ]] && [[ $n -lt $limit ]]
  do
    cd "$(dirname -- "$link")"
    n=$((n + 1))
    link="$(readlink -- "${link##*/}")"
  done
  cd "$(dirname -- "$link")"

  if [[ $n -ge $limit ]]
  then
    echo "Recursion limit ($limit) exceeded." >&2
    return 2
  fi

  printf '%s/%s\n' "$(pwd -P)" "${link##*/}"
)}

注意,所有的cd和set都发生在一个子shell中。

普通shell脚本通常必须找到它们的“主”目录,即使它们是作为符号链接调用的。因此脚本必须从$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`