首页 文章

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

提问于
浏览
191

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

16 回答

  • 2

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

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

    getcwd pwd

  • 15
    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]](/users/65889/danorton)

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

    readlink $path
    

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

  • 0

    如果您只是想要目录,“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
    
  • 1

    我最喜欢的一个是 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.
    
  • 1
    readlink -e [filepath]
    

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

  • 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|"; }
    
  • 21

    将一些给定的解决方案放在一起,知道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"
    
  • 82

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

    下面是 fully POSIX-compliant script / function ,因此 cross-platform (也适用于macOS,其 readlink 仍然不支持 -f ,截至10.12(Sierra)) - 它仅使用POSIX shell language features且仅使用POSIX兼容的实用程序调用 .

    它是 portable implementation of GNU's readlink -ereadlink -f 的更严格版本) .

    你可以 run the script with shsource the function in bash, ksh, and zsh

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

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

    rreadlink script / function definition:

    代码改编了这个答案的感激之情 . 我还在这里创建了一个基于bash的独立实用程序版本,如果安装了Node.js,可以使用npm install rreadlink -g进行安装 .

    #!/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 "$@"
    

    A tangent on security:

    jarno,参考确保内置 command 没有被同名的别名或shell函数遮蔽的函数,请在注释中询问:

    如果unalias或unset并且[设置为别名或shell函数?

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

    我认为's safe to say that unless you'正在处理一个不受信任的恶意环境,担心 unaliasunset - 或者,就此而言, whiledo ,...... - 被重新定义并不是一个问题 .

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

    为了解决您的疑虑:

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

    但是,引用不是shell关键字的选项( whileforifdo ,...),而shell关键字确实优先于shell函数,在 bashzsh 别名中具有最高优先级,因此要防止shell-关键字重定义必须使用它们的名称运行 unalias (尽管在非交互式 bash shell(例如脚本)中,默认情况下不会扩展别名 - 仅当首先显式调用 shopt -s expand_aliases 时) .

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

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

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

  • 356

    常见的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`
    
  • 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"))/..
    
  • 0

    试试这个:

    cd $(dirname $([ -L $0 ] && readlink -f $0 || echo $0))
    
  • 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-路径:

    %% 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
    
  • 1

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

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

    不太好,但跨OS

  • 5

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

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

  • 10

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

  • 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'))")
    

相关问题