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

给定一个绝对路径或相对路径(在类unix系统中),我希望在解析任何中间符号链接之后确定目标的完整路径。同时还可以解析~用户名符号。

如果目标是一个目录,则可以将chdir()放入该目录,然后调用getcwd(),但我确实希望从shell脚本而不是编写C helper来完成此操作。不幸的是,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"。

204302 次浏览

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

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

getcwd 松材线虫病 < / p >

普通shell脚本通常必须找到它们的“主”目录,即使它们是作为符号链接调用的。因此脚本必须从$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`
readlink -f "$path"
编者注:上述方法适用于GNU readlinkFreeBSD / PC-BSD / OpenBSD readlink,但在10.11版本的OS X上适用于 GNU readlink提供了额外的相关选项,例如-m用于解析符号链接,无论最终目标是否存在

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

realpath $path

[Admin from comment by halloleo - danorton] < / > < / p >

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

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

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

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

另一种方法:

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

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

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

不是很好,但是跨越了操作系统

试试这个:

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

因为我在过去的几年里遇到过很多次这样的情况,这一次我需要一个纯粹的bash可移植版本,可以在OSX和linux上使用,我继续写了一个:

活生生的版本就在这里:

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
readlink -e [filepath]

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

注:我认为这是一个可靠的、可移植的、现成的解决方案,因此总是 long

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

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

你可以使用sh运行脚本在__ABC1, __ABC2和zsh中创建函数:

例如,在一个脚本中,你可以像下面这样使用它来获取运行脚本的真实源目录,并解析符号链接:

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

rreadlink脚本/函数定义:

代码改编自这个答案 我还创建了一个基于__abc0的独立实用程序版本在这里,您可以使用
安装它 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 "$@"

关于安全问题:

雅诺引用确保内置command不会被同名的别名或shell函数遮蔽的函数,在注释中询问:

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

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

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

函数必须依赖某物来获得其原始含义和行为-没有其他方法 类似posix的shell允许重新定义内置程序甚至语言关键字,这是本质上一个安全风险(并且编写偏执的代码通常是困难的)

为了解决您的问题:

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

然而,引用是shell for0 (whileforifdo,…)的一个选项,虽然shell关键字确实优先于shell for1,但在bashzsh中别名具有最高优先级,因此为了防止shell关键字的重新定义,你必须运行unalias和它们的名称(尽管在for2 bash中,shell(如脚本)别名在默认情况下是扩展的-只有在shopt -s expand_aliases首先显式调用时)。

为了确保unalias -作为一个内置的-有它的原始含义,你必须首先在它上面使用\unset,这要求unset有它的原始含义:

unset是一个shell 内装式,所以为了确保它被这样调用,你必须确保它本身没有被重新定义为函数。虽然可以通过引用绕过别名形式,但不能绕过shell函数形式——catch 22。

因此,除非你可以依赖unset来获得它的原意,否则据我所知,没有保证的方法来防御所有恶意的重新定义。

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

这是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
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中。

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

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

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

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

我的答案在这里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

在不能使用pwd的情况下(例如从不同的位置调用脚本),使用realpath(带dirname或不带dirname):

$(dirname $(realpath $PATH_TO_BE_RESOLVED))

工作时,通过(多个)符号链接(s)或直接调用脚本-从任何位置。

我的2分钱。这个函数是POSIX兼容的,源和目标都可以包含->。然而,我还没有让它与容器换行符或制表符的文件名一起工作,因为ls通常有这些问题。

resolve_symlink() {
test -L "$1" && ls -l "$1" | awk -v SYMLINK="$1" '{ SL=(SYMLINK)" -> "; i=index($0, SL); s=substr($0, i+length(SL)); print s }'
}

我相信这里的解决方案是file命令,使用一个自定义魔术文件,只输出所提供的符号链接的目标。

这是在Bash 3.2.57中测试过的最佳解决方案:

# Read a path (similar to `readlink`) recursively, until the physical path without any links (like `cd -P`) is found.
# Accepts any existing path, prints its physical path and exits `0`, exits `1` if some contained links don't exist.
# Motivation: `${BASH_SOURCE[0]}` often contains links; using it directly to extract your project's path may fail.
#
# Example: Safely `source` a file located relative to the current script
#
#     source "$(dirname "$(rreadlink "${BASH_SOURCE[0]}")")/relative/script.sh"
#Inspiration: https://stackoverflow.com/a/51089005/6307827
rreadlink () {
declare p="$1" d l
while :; do
d="$(cd -P "$(dirname "$p")" && pwd)" || return $? #absolute path without symlinks
p="$d/$(basename "$p")"
if [ -h "$p" ]; then
l="$(readlink "$p")" || break


#A link must be resolved from its fully resolved parent dir.
d="$(cd "$d" && cd -P "$(dirname "$l")" && pwd)" || return $?
p="$d/$(basename "$l")"
else
break
fi
done
printf '%s\n' "$p"
}