如何解析shell脚本中的符号链接

时间:2008-08-11 10:40:41

标签: bash shell scripting symlink

给定绝对或相对路径(在类Unix系统中),我想在解析任何中间符号链接后确定目标的完整路径。奖励还可以同时解析〜用户名表示法。

如果目标是目录,则可以将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目录执行时,解析(“foo”)==“/ Users / greg / tmp / bar”。

19 个答案:

答案 0 :(得分:366)

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),请使用readlink而不使用-f选项:

readlink $path

编者注:这不会递归地解析符号链接 ,因此不会报告终极目标;例如,给定符号链接a指向b,而c指向b,这只会报告perl(并且不会确保将其输出为readlink -f 绝对路径) 在OS X上使用以下perl -MCwd -le 'print Cwd::abs_path(shift)' "$path"命令填补缺少的{{1}}功能的空白:
{{1}}

答案 1 :(得分:86)

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

来自char *getcwd(char *buf, size_t size)

C函数unistd.h应具有相同的行为。

getcwd pwd

答案 2 :(得分:24)

如果你只是想要这个目录,那么“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

答案 3 :(得分:16)

我最喜欢的一个是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.

答案 4 :(得分:10)

readlink -e [filepath]

似乎正是你所要求的 - 它接受一个arbirary路径,解析所有符号链接,并返回“真实”路径 - 它可能是所有系统已经拥有的“标准* nix”

答案 5 :(得分: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"

答案 6 :(得分:5)

另一种方式:

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

答案 7 :(得分:2)

注意:我认为这是一个可靠的,可移植的,现成的解决方案,由于这个原因,它总是冗长

以下是完全符合POSIX标准的脚本/功能,因此跨平台(也适用于macOS,其readlink仍然不支持截至10.12(Sierra)的-f - 它仅使用POSIX shell language features且仅使用符合POSIX的实用程序调用。

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

您可以使用sh 运行脚本来源bash中的功能,{ {1}}和ksh

例如,在脚本内部,您可以按如下方式使用它来获取正在运行的脚本的真实目录,并解析符号链接:

zsh

trueScriptDir=$(dirname -- "$(rreadlink "$0")") 脚本/功能定义:

代码改编为this answer的感激之情 我还创建了基于rreadlink的独立实用程序版本here,您可以使用它安装 bash,如果您安装了Node.js.

npm install rreadlink -g

安全相切:

jarno,参考确保内置#!/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 "$@" 没有被同名的别名或shell函数遮蔽的函数,请在注释中提问:

  

如果将commandunaliasunset设置为别名或shell函数,该怎么办?

确保[具有其原始含义的rreadlink背后的动机是使用它来绕过(良性)便利别名和通常用于影响交互式标准命令的函数shell,例如重新定义command以包含喜欢的选项。

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

该函数必须依赖某些东西才能拥有其原始含义和行为 - 没有办法解决这个问题。
类似POSIX的shell允许重新定义内置函数甚至语言关键字本质上存在安全风险(并且编写偏执代码通常很难)。

具体解决您的疑虑:

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

但是,引用是shell 关键字的选项unset\unaliaswhile,{{1}虽然shell关键字确实优先于shell 函数,但在forif别名中具有最高优先级,因此要防止shell-keyword重定义您必须使用其名称运行do(尽管在非交互式 bash shell(例如脚本)中,默认情况下扩展别名 - 仅限如果首先显式调用zsh

要确保unalias - 作为内置 - 具有其原始含义,您必须首先使用bash,这要求shopt -s expand_aliases具有其原始含义:

unalias是一个shell builtin ,所以为了确保它本身被调用,你必须确保它本身没有被重新定义为函数< / em>的。虽然您可以使用引号绕过别名表单,但您无法绕过shell函数表单 - catch 22。

因此,除非你能依靠\unset具有其原始含义,否则我无法保证能够抵御所有恶意重新定义。

答案 8 :(得分:1)

由于多年来我遇到过这么多次,这次我需要一个可以在OSX和Linux上使用的纯bash便携版本,我继续编写了一个:

活着的版本住在这里:

https://github.com/keen99/shell-functions/tree/master/resolve_path

但是为了SO,这是当前的版本(我觉得它经过了很好的测试......但我愿意接受反馈!)

可能不难让普通的bourne shell(sh)工作,但我没试过......我太喜欢$ FUNCNAME了。 :)

#!/bin/bash

resolve_path() {
    #I'm bash only, please!
    # usage:  resolve_path <a file or directory> 
    # follows symlinks and relative paths, returns a full real path
    #
    local owd="$PWD"
    #echo "$FUNCNAME for $1" >&2
    local opath="$1"
    local npath=""
    local obase=$(basename "$opath")
    local odir=$(dirname "$opath")
    if [[ -L "$opath" ]]
    then
    #it's a link.
    #file or directory, we want to cd into it's dir
        cd $odir
    #then extract where the link points.
        npath=$(readlink "$obase")
        #have to -L BEFORE we -f, because -f includes -L :(
        if [[ -L $npath ]]
         then
        #the link points to another symlink, so go follow that.
            resolve_path "$npath"
            #and finish out early, we're done.
            return $?
            #done
        elif [[ -f $npath ]]
        #the link points to a file.
         then
            #get the dir for the new file
            nbase=$(basename $npath)
            npath=$(dirname $npath)
            cd "$npath"
            ndir=$(pwd -P)
            retval=0
            #done
        elif [[ -d $npath ]]
         then
        #the link points to a directory.
            cd "$npath"
            ndir=$(pwd -P)
            retval=0
            #done
        else
            echo "$FUNCNAME: ERROR: unknown condition inside link!!" >&2
            echo "opath [[ $opath ]]" >&2
            echo "npath [[ $npath ]]" >&2
            return 1
        fi
    else
        if ! [[ -e "$opath" ]]
         then
            echo "$FUNCNAME: $opath: No such file or directory" >&2
            return 1
            #and break early
        elif [[ -d "$opath" ]]
         then 
            cd "$opath"
            ndir=$(pwd -P)
            retval=0
            #done
        elif [[ -f "$opath" ]]
         then
            cd $odir
            ndir=$(pwd -P)
            nbase=$(basename "$opath")
            retval=0
            #done
        else
            echo "$FUNCNAME: ERROR: unknown condition outside link!!" >&2
            echo "opath [[ $opath ]]" >&2
            return 1
        fi
    fi
    #now assemble our output
    echo -n "$ndir"
    if [[ "x${nbase:=}" != "x" ]]
     then
        echo "/$nbase"
    else 
        echo
    fi
    #now return to where we were
    cd "$owd"
    return $retval
}

这是一个典型的例子,感谢brew:

%% ls -l `which mvn`
lrwxr-xr-x  1 draistrick  502  29 Dec 17 10:50 /usr/local/bin/mvn@ -> ../Cellar/maven/3.2.3/bin/mvn

使用此功能,它将返回-real- path:

%% cat test.sh
#!/bin/bash
. resolve_path.inc
echo
echo "relative symlinked path:"
which mvn
echo
echo "and the real path:"
resolve_path `which mvn`


%% test.sh

relative symlinked path:
/usr/local/bin/mvn

and the real path:
/usr/local/Cellar/maven/3.2.3/libexec/bin/mvn 

答案 9 :(得分:1)

要解决Mac不兼容问题,我想出了

echo `php -r "echo realpath('foo');"`

不太好,但跨越OS

答案 10 :(得分:1)

试试这个:

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

答案 11 :(得分:1)

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

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

答案 12 :(得分:1)

以下是如何使用内联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'))")

答案 13 :(得分:1)

如果无法使用pwd(例如,从其他位置调用脚本),请使用realpath(带或不带目录名):

$(dirname $(realpath $PATH_TO_BE_RESOLVED))

在通过(多个)符号链接进行调用时或从任何位置直接调用脚本时都可以使用。

答案 14 :(得分:1)

function realpath {
    local r=$1; local t=$(readlink $r)
    while [ $t ]; do
        r=$(cd $(dirname $r) && cd $(dirname $t) && pwd -P)/$(basename $t)
        t=$(readlink $r)
    done
    echo $r
}

#example usage
SCRIPT_PARENT_DIR=$(dirname $(realpath "$0"))/..

答案 15 :(得分:1)

常见的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`

答案 16 :(得分:0)

我的回答Bash: how to get real path of a symlink?

但是简而言之在脚本中非常方便:

script_home=$( dirname $(realpath "$0") )
echo Original script home: $script_home

这些是GNU coreutils的一部分,适用于Linux系统。

要测试所有内容,我们将符号链接放到/ home / test2 /中,修改一些其他内容,然后从根目录运行/调用它:

/$ /home/test2/symlink
/home/test
Original script home: /home/test

哪里

Original script is: /home/test/realscript.sh
Called script is: /home/test2/symlink

答案 17 :(得分:0)

我相信这是使用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 "$link"
    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##*/}"
)}

请注意,所有cdset内容都在子shell中进行。

答案 18 :(得分:0)

在这里,我提出我认为是一种跨平台(至少是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
}