如何parsingshell脚本中的符号链接

给定绝对path或相对path(在类Unix系统中),我想在parsing任何中间符号链接后确定目标的完整path。 加分也同时parsing〜用户名表示法。

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

根据标准, pwd -P应该返回parsing了符号链接的path。

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

getcwd pwd

 readlink -f "$path" 

编者按:上面的代码是GNU readlinkFreeBSD / PC-BSD / OpenBSD的 readlink ,但在10.11版本的OS X上没有
GNU readlink提供了额外的相关选项,如-m用于parsing符号链接,而不pipe最终目标是否存在。

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

 realpath $path 

[下面的pipe理添加评论由halloleo – danorton]

对于Mac OS X(通过至less10.11.x),使用不带-f选项的readlink

 readlink $path 

编者按:这不会解决符号链接recursion ,因此不会报告最终目标; 例如,给定的符号链接a指向b ,而b又指向c ,这将只报告b (并且不会确保它被输出为绝对path )。
在OS X上使用下面的perl命令来填补缺失的readlink -ffunction的缺口:
perl -MCwd -le 'print Cwd::abs_path(shift)' "$path"

“pwd -P”似乎工作,如果你只是想要的目录,但如果由于某种原因,你想要的实际可执行文件的名称,我不认为这有帮助。 这是我的解决scheme:

 #!/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

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

 realpath扩展了所有符号链接,并parsing了由path指定的以null结尾的string中的“/./”,“/../”和额外的“/”字符的引用,
       将规范化的绝对​​path名存储在由resolved_pa​​th命名的大小为PATH_MAX的缓冲区中。 得到的path将没有符号链接,“/./”或
        '/../' 组件。
 readlink -e [filepath] 

似乎正是你所要求的 – 它接受一个arbirarypath,解决所有的符号链接,并返回“真正的”path – 它是“标准* nix”,可能所有的系统已经有

把一些给定的解决scheme放在一起,知道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" 

其他方式:

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

注:我相信这是一个坚实的,可移植的,现成的解决scheme,这是非常漫长的原因。

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

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

您可以使用sh运行脚本或者bashkshzshinput函数

例如,在一个脚本中,你可以像下面这样使用它来获得运行脚本的真正的原始目录,并parsing符号链接:

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

rreadlink脚本/函数定义:

代码是从这个答案感谢。
我也在这里创build了一个基于bash的独立实用程序版本,您可以使用它进行安装
npm install rreadlink -g ,如果你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 (eg, 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), ie the '..' is applied # AFTER canonicalization. command printf '%s\n' "$(command dirname -- "${targetDir}")" else command printf '%s\n' "${targetDir%/}/$fname" fi ) rreadlink "$@" 

安全切线:

jarno参考了确保内buildcommand不被相同名称的别名或shell函数影响的函数,在评论中提出:

如果unaliasunset[被设置为别名或shell函数?

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

我认为可以肯定地说,除非你处理不可信任的恶意环境,否则担心unaliasunset – 或者重新定义的while ,重新定义不是一个问题。

一些function必须依赖于它的原始意义和行为 – 这是没有办法的。
类似POSIX的shell允许重新定义内置的语言,甚至语言关键字本身就是一个安全风险(并且写偏执的代码一般很难)。

具体解决您的疑虑:

这个函数依赖于unalias并且unset它们原来的意思。 把它们重新定义为shell的function ,改变它们的行为将是一个问题; 重定义作为别名不一定是一个问题,因为引用 (部分)命令名称(例如\unalias )会绕过别名。

但是,对于shell 关键字whileforifdo ,…),引用不是一个选项,虽然shell关键字优先于shell 函数 ,但bashzsh别名具有最高的优先级,所以为了防止shell-关键字重定义您必须运行unalias与他们的名字(虽然在非交互式的 bash shell(如脚本)别名默认情况下扩展 – 只有首先显式调用shopt -s expand_aliases )。

为了确保作为内buildunalias具有它的原始含义,首先必须使用\unset ,这要求unset具有其原始含义:

unset是一个shell 内build的 ,所以为了确保它被调用,你必须确保它本身不被重新定义为一个函数 。 虽然可以用引号绕过别名表单,但不能绕过shell函数forms – catch 22。

因此,除非你可以依靠不固定的原始意义,否则我无法确定如何防范所有恶意的重新定义。

即使被调用为符号链接,常见的shell脚本通常也必须find它们的“主目录”目录。 因此脚本必须从$ 0中find它们的“真实”位置。

 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` 
 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"))/.. 

尝试这个:

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

自从我经历了这么多年以来,这次我需要一个纯粹的bash便携版本,我可以在OSX和linux上使用,我继续写下了一个:

生活版本住在这里:

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

但为了这个目的,这里是目前的版本(我觉得它已经很好的testing了..但我很乐意回应!)

可能并不难,让它适用于朴素的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 } 

这是一个经典的例子,感谢酿造:

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

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

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

不是很好,但跨OS

我相信这是真正的和权威的“解决符号链接的方式”无论是目录还是非目录,使用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。