关于bash:如何解析shell脚本中的符号链接

关于bash:如何解析shell脚本中的符号链接

How to resolve symbolic links in a shell script

给定绝对或相对路径(在类Unix系统中),我想在解析任何中间符号链接之后确定目标的完整路径。同时解析~username符号的奖励积分。

如果目标是一个目录,那么可以先将chdir()放入该目录,然后调用getcwd(),但我确实希望从shell脚本中执行此操作,而不是编写一个C助手。不幸的是,shell有一种倾向,试图向用户隐藏符号链接的存在(这是OS X上的bash):

1
2
3
4
5
6
7
$ 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"。


1
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兼容。它还包括在两个文件之间生成相对路径的功能。

1
realpath $path

[以下由Halloeo-Danorton评论的管理员添加]

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

1
readlink $path

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


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

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

GETCWD随钻测井


如果您只需要目录,"pwd-p"似乎可以工作,但是如果出于某种原因您需要实际可执行文件的名称,我认为这没有帮助。我的解决方案是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/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

我最喜欢的是realpath foo

1
2
3
4
5
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.

1
readlink -e [filepath]

似乎正是你想要的-它接受一个仲裁路径,解析所有符号链接,并返回"真实"路径。-它是"标准*尼克斯",可能所有系统都已经有了。


把一些给定的解决方案放在一起,知道readlink在大多数系统上都可用,但需要不同的参数,这对我在OSX和Debian上很有效。我不确定BSD系统。可能情况需要是[[ $OSTYPE != darwin* ]]才能将-f排除在OSX之外。

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


另一种方式:

1
2
3
4
5
6
7
8
# 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|"; }

注:我认为这是一个坚实的,可移植的,现成的解决方案,因为这个原因总是很长的。

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

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

您可以使用sh运行脚本,也可以在bashkshzsh中源代码该函数:

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

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

rreadlink脚本/函数定义:

出于对这个答案的感激,代码被修改了。我还创建了一个基于bash的独立实用程序版本,您可以使用如果安装了node.js,则为npm install rreadlink -g

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
#!/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
'
"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
'
"${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
'
"$(command dirname --"${targetDir}")"
  else
    command printf '%s
'
"${targetDir%/}/$fname"
  fi
)

rreadlink"$@"

安全性的切线:

Jarno引用了确保内置command不被同名别名或shell函数隐藏的函数,在注释中问道:

What if unalias or unset and [ are set as aliases or shell functions?

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

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

函数必须依赖某些东西才能有其原始含义和行为——这是不可能的。像posix这样的shell允许对内置关键字甚至语言关键字进行重新定义,这在本质上是一种安全风险(一般来说,编写偏执代码很困难)。

具体解决您的问题:

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

但是,对shell关键字(whileforifdo…)来说,引用不是一种选择;虽然shell关键字优先于shell函数,但在bashzsh别名中,它们的优先级最高,因此为了防止shell关键字重新定义,必须使用它们的n运行unalias。Ames(尽管在非交互的bashshell(如脚本)中,别名在默认情况下不会扩展—仅当明确地首先调用shopt -s expand_aliases时才扩展)。

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

unset是一个shell内置函数,因此为了确保它被这样调用,您必须确保它本身没有被重新定义为函数。虽然可以通过引用绕过别名窗体,但不能绕过shell函数窗体-catch 22。

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


因为我在过去的几年中遇到过很多次,而这一次我需要一个可以在OSX和Linux上使用的纯bash可移植版本,所以我继续写了一个:

活生生的版本生活在这里:

https://github.com/keen99/shell-functions/tree/master/resolve_路径

但是为了这个,这是最新的版本(我觉得它经过了很好的测试……但是我乐于接受反馈!)

对于普通的BourneShell(sh),这可能并不难,但我没有尝试……我太喜欢$funcname了。:)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
#!/bin/bash

resolve_path() {
    #I'm bash only, please!
    # usage:  resolve_path  
    # 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:

1
2
%% 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-路径:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
%% 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

常用的shell脚本通常必须找到它们的"home"目录,即使它们是作为符号链接调用的。因此脚本必须从0美元中找到他们的"真实"位置。

1
cat `mvn`

在我的系统上打印一个包含以下内容的脚本,这应该是您需要什么的一个很好的提示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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`

1
2
3
4
5
6
7
8
9
10
11
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"))/..


试试这个:

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

下面介绍如何使用内联Perl脚本获取macos/unix中文件的实际路径:

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

同样,要获取符号链接文件的目录:

1
DIR=$(perl -e"use Cwd qw(abs_path); use File::Basename; print dirname(abs_path('$0'))")

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

1
(cd"$DIR"; pwd -P)

但是,如果它可能是一个文件,那么这将不起作用:

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

因为符号链接可能解析为相对路径或完整路径。

在脚本上,我需要找到真正的路径,以便引用配置或与之一起安装的其他脚本,我使用此路径:

1
2
3
4
5
6
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设置为任何文件路径。基本上,只要路径是symlink,它就解析该symlink。技巧在循环的最后一行。如果解析的符号链接是绝对的,它将使用它作为SOURCE。然而,如果它是相对的,它将为它预先准备好DIR,这是通过我第一次描述的简单技巧解决的,成为一个真实的位置。


为了解决mac的不兼容性,我想出了

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

不是很好,但交叉操作系统


在这里,我介绍了我所认为的跨平台(至少是Linux和MacOS)解决方案,以解决目前对我来说工作良好的问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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(只有?)解决方案。可能更适合最初的问题。

1
2
3
4
5
6
7
8
9
10
11
12
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
}


我相信这是一个真正和明确的"解决符号链接的方法",不管它是目录还是非目录,使用bash:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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
'
"$(pwd -P)""${link##*/}"
)}

请注意,所有的cdset都发生在一个子壳中。


推荐阅读