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

bash shell scripting symlink

149679 观看

18回复

707103 作者的声誉

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

作者: Greg Hewgill 的来源 发布者: 2008 年 8 月 11 日

回应 (18)


88

14284 作者的声誉

决定

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

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

getcwd pwd

作者: kauppi 发布者: 11.08.2008 10:48

1

3398 作者的声誉

常见的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`
作者: Hugo 发布者: 11.08.2008 11:17

377

24620 作者的声誉

readlink -f "$path"

编者注:上述内容适用于GNU readlinkFreeBSD / PC-BSD / OpenBSD readlink,但从10.11开始不适用于OS X.
GNU readlink提供了其他相关选项,例如,-m无论最终目标是否存在,都可以解析符号链接。

请注意,自GNU coreutils 8.15(2012-01-06)以来,有一个可用的实际路径程序比上面的更简单,更灵活。它还与同名的FreeBSD util兼容。它还包括生成两个文件之间的相对路径的功能。

realpath $path

[管理员在下面评论halloleo - danorton]

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

readlink $path

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

作者: pixelbeat 发布者: 04.09.2008 12:33

16

1037 作者的声誉

我最喜欢的一个是 realpath foo

realpath  - 返回规范化的绝对​​路径名

realpath扩展所有符号链接并解析对由path指定的以null结尾的字符串中的'/./','/../'和额外'/'字符的引用,
       将规范化的绝对​​路径名存储在由resolved_pa​​th命名的大小为PATH_MAX的缓冲区中。生成的路径将没有符号链接,'/。/'或
       '/../' 组件。
作者: Gregory 发布者: 04.12.2008 11:39

25

1567 作者的声誉

如果您只是想要目录,“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
作者: tlrobinson 发布者: 30.03.2009 02:54

5

228 作者的声誉

其他方式:

# 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|"; } 
作者: Keymon 发布者: 13.09.2011 10:52

1

451 作者的声誉

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"))/..
作者: Dave 发布者: 10.04.2014 02:52

1

963 作者的声誉

为了解决Mac不兼容问题,我想出了办法

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

不太好,但交叉操作系统

作者: Clemens Tolboom 发布者: 08.08.2014 08:50

1

9132 作者的声誉

试试这个:

cd $(dirname $([ -L $0 ] && readlink -f $0 || echo $0))
作者: diyism 发布者: 29.08.2014 02:41

1

697 作者的声誉

多年来我经历过这么多次,而这次我需要一个可以在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-路径:

%% 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 
作者: keen 发布者: 24.12.2014 09:06

10

1827 作者的声誉

readlink -e [filepath]

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

作者: Chuck Kollars 发布者: 09.07.2015 02:34

2

155168 作者的声誉

注意:我认为这是一个坚固,便携,现成的解决方案,因此非常冗长

下面是一个完全符合POSIX标准的脚本/函数,因此是跨平台的(也适用于macOS,从10.12(Sierra)起readlink仍然不支持-f) - 它只使用POSIX shell语言功能,只使用POSIX兼容的实用程序调用。

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

您可以运行该脚本sh功能bashkshzsh

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

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,参考确保内置command不被同名的别名或shell函数遮蔽的函数,在注释中询问:

如果unalias还是unset[设置为别名或外壳的功能呢?

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

我认为这是肯定地说,除非你正在处理一个不受信任的,恶意的环境,担心unaliasunset-或者,就此而言,whiledo,... -被重新定义是不是一个问题。

一些函数必须依靠有它的本义和行为-有周围没有办法。
类似POSIX的shell允许重新定义内置函数甚至语言关键字本身就存在安全风险(编写偏执代码通常很难)。

为了解决您的疑虑:

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

然而,引用是不是壳的选项关键字whileforifdo,...),并同时外壳的关键字做优先于外壳的功能,在bashzsh别名具有最高的优先级,所以要警惕你必须运行壳关键字重新定义unalias与它们的名称(尽管在非交互式 bash shell(例如脚本)中,默认情况下不会扩展别名- 仅在shopt -s expand_aliases首先显式调用时)。

为了确保unalias- 作为内置 - 具有其原始含义,您必须首先使用\unset它,这需要unset具有其原始含义:

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

因此,除非你可以依靠unset它的原始含义,从我所知道的,没有保证的方法来抵御所有恶意的重新定义。

作者: mklement0 发布者: 21.10.2015 06:39

5

51 作者的声誉

将一些给定的解决方案放在一起,知道readlink在大多数系统上都可用,但需要不同的参数,这对我在OSX和Debian上运行良好。我不确定BSD系统。也许条件需要仅从OSX中[[ $OSTYPE != darwin* ]]排除-f

#!/bin/bash
MY_DIR=$( cd $(dirname $(readlink `[[ $OSTYPE == linux* ]] && echo "-f"` $0)) ; pwd -P)
echo "$MY_DIR"
作者: hpvw 发布者: 14.11.2016 07:47

0

1257 作者的声誉

我相信这是使用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中。

作者: solidsnack 发布者: 23.08.2017 12:49

1

471 作者的声誉

以下是使用内联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'))")
作者: Igor Afanasyev 发布者: 30.01.2018 03:57

1

260947 作者的声誉

Is your path a directory, or might it be a file? If it's a directory, it's simple:

(cd "$DIR"; pwd -P)

However, if it might be a file, then this won't work:

DIR=$(cd $(dirname "$FILE"); pwd -P); echo "${DIR}/$(readlink "$FILE")"

because the symlink might resolve into a relative or full path.

On scripts I need to find the real path, so that I might reference configuration or other scripts installed together with it, I use this:

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

You could set SOURCE to any file path. Basically, for as long as the path is a symlink, it resolves that symlink. The trick is in the last line of the loop. If the resolved symlink is absolute, it will use that as SOURCE. However, if it is relative, it will prepend the DIR for it, which was resolved into a real location by the simple trick I first described.

作者: Daniel C. Sobral 发布者: 28.06.2018 06:00

0

1 作者的声誉

在这里,我提出了我认为是一个跨平台(至少是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
}
作者: RuneImp 发布者: 14.01.2019 10:32

0

391 作者的声誉

我在这里回答Bash:如何获得符号链接的真实路径?

但总之非常方便的脚本:

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

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

为了测试所有内容,我们将symlink放入/ 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
作者: Arunas Bartisius 发布者: 03.09.2019 02:01
32x32