كيفية حل الروابط الرمزية في برنامج شل النصي
سؤال
بالنظر إلى المسار المطلق أو النسبي (في نظام يشبه يونكس)، أود تحديد المسار الكامل للهدف بعد حل أي روابط رمزية وسيطة.نقاط إضافية لحل تدوين اسم المستخدم في نفس الوقت.
إذا كان الهدف عبارة عن دليل، فقد يكون من الممكن إدخال chdir() في الدليل ثم الاتصال بـ getcwd()، لكنني أريد حقًا القيام بذلك من برنامج نصي Shell بدلاً من كتابة مساعد C.لسوء الحظ، تميل الأصداف إلى محاولة إخفاء وجود الروابط الرمزية عن المستخدم (وهذا هو الحال في نظام التشغيل OS X):
$ 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
يجب أن يُرجع المسار مع حل الارتباطات الرمزية.
وظيفة ج char *getcwd(char *buf, size_t size)
من unistd.h
يجب أن يكون لها نفس السلوك.
نصائح أخرى
readlink -f "$path"
ملحوظة المحرر:ما سبق يعمل مع جنو readlink
و فري بي إس دي/ بي سي- بي إس دي/ أوبن بي إس دي readlink
, ، لكن لا على OS X اعتبارًا من 10.11.
جنو readlink
يقدم خيارات إضافية ذات صلة، مثل -m
لحل الارتباط الرمزي سواء كان الهدف النهائي موجودًا أم لا.
لاحظ أنه منذ إصدار GNU coreutils 8.15 (2012-01-06)، يوجد com.realpath البرنامج المتاح أقل منفرجة وأكثر مرونة من المذكور أعلاه.كما أنه متوافق مع أداة FreeBSD التي تحمل نفس الاسم.ويتضمن أيضًا وظيفة إنشاء مسار نسبي بين ملفين.
realpath $path
[إضافة المشرف أدناه من التعليق بواسطة مرحبا —دانورتون]
بالنسبة لنظام التشغيل Mac OS X (حتى الإصدار 10.11.x على الأقل)، استخدم readlink
بدون ال -f
خيار:
readlink $path
ملحوظة المحرر:لن يؤدي هذا إلى حل الارتباطات الرمزية بشكل متكرر وبالتالي لن يبلغ عن ذروة هدف؛على سبيل المثال، الارتباط الرمزي المعطى a
الذي يشير إلى b
, ، والذي بدوره يشير إلى c
, ، وهذا سوف يقدم تقريرا فقط b
(ولن نتأكد من أنه تم إخراجه كملف المسار المطلق).
استخدم ما يلي perl
الأمر على OS X لملء الفجوة المفقودة readlink -f
وظائف:
perl -MCwd -le 'print Cwd::abs_path(shift)' "$path"
يبدو أن "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
واحدة من المفضلة هي 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.
readlink -e [filepath]
يبدو أن ما تطلبه بالضبط - إنه يقبل مسارًا أرابراري ، ويحل جميع الارتباطات ، ويعيد المسار "الحقيقي" - وهو "قياسي *nix" الذي من المحتمل أن يكون لدى جميع الأنظمة بالفعل
طريق اخر:
# 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|"; }
بتجميع بعض الحلول المقدمة معًا، مع العلم أن رابط القراءة متاح في معظم الأنظمة، ولكنه يحتاج إلى وسائط مختلفة، فإن هذا يعمل جيدًا بالنسبة لي على OSX وDebian.لست متأكدًا من أنظمة BSD.ربما يجب أن يكون الشرط [[ $OSTYPE != darwin* ]]
لاستبعاد -f
من OSX فقط.
#!/bin/bash
MY_DIR=$( cd $(dirname $(readlink `[[ $OSTYPE == linux* ]] && echo "-f"` $0)) ; pwd -P)
echo "$MY_DIR"
ملحوظة:أعتقد أن هذا حل قوي ومحمول وجاهز، وهو موجود دائمًا طويل لهذا السبب بالذات.
أدناه هو أ البرنامج النصي / الوظيفة المتوافقة تمامًا مع POSIX هذا هو السبب عبر منصة (يعمل على نظام التشغيل macOS أيضًا، والذي readlink
لا يزال لا يدعم -f
اعتبارًا من 10.12 (سييرا)) - يستخدم فقط ميزات لغة الصدفة POSIX ومكالمات المرافق المتوافقة مع POSIX فقط.
إنها التنفيذ المحمول لـ GNU readlink -e
(النسخة الأكثر صرامة من readlink -f
).
أنت تستطيع تشغيل النصي مع sh
أو مصدر وظيفة في bash
, ksh
, ، و zsh
:
على سبيل المثال، داخل البرنامج النصي، يمكنك استخدامه على النحو التالي للحصول على دليل الأصل الحقيقي للبرنامج النصي قيد التشغيل، مع حل الارتباطات الرمزية:
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 "$@"
ظل على الأمن:
جارنو, ، في إشارة إلى الوظيفة التي تضمن ذلك المدمج command
لم يتم تظليله بواسطة اسم مستعار أو وظيفة Shell بنفس الاسم، يسأل في تعليق:
ماذا إذا
unalias
أوunset
و[
يتم تعيينها كأسماء مستعارة أو وظائف شل؟
الدافع وراء rreadlink
هل تضمن ذلك command
معناه الأصلي هو استخدامه للتجاوز (حميد) راحة غالبًا ما تُستخدم الأسماء المستعارة والوظائف لتظليل الأوامر القياسية في الأصداف التفاعلية، مثل إعادة التعريف ls
لتضمين الخيارات المفضلة.
أعتقد أنه من الآمن أن نقول ذلك إلا إذا كنت تتعامل مع بيئة ضارة غير موثوقة، مما يثير القلق unalias
أو unset
- أو، في هذا الصدد، while
, do
, ، ...- إعادة التعريف ليست مصدر قلق.
هنالك شئ ما التي يجب أن تعتمد عليها الوظيفة للحصول على معناها وسلوكها الأصليين - لا توجد طريقة للتغلب على ذلك.
تسمح الأصداف المشابهة لـ POSIX بإعادة تعريف العناصر المدمجة وحتى الكلمات الرئيسية للغة متأصل خطر أمني (وكتابة التعليمات البرمجية بجنون العظمة أمر صعب بشكل عام).
لمعالجة مخاوفك على وجه التحديد:
تعتمد الوظيفة على unalias
و unset
لها معناها الأصلي.إعادة تعريفهم على أنهم وظائف القشرة بطريقة تغير سلوكهم ستكون مشكلة؛إعادة تعريف باعتبارها الاسم المستعار ليس بالضرورة مصدر قلق ، لأن نقلا عن (جزء من) اسم الأمر (على سبيل المثال، \unalias
) يتجاوز الأسماء المستعارة.
ومع ذلك، نقلا عن لا خيار للقذيفة الكلمات الدالة (while
, for
, if
, do
, ، ...) وبينما تكون الكلمات الأساسية لـ Shell لها الأسبقية على Shell المهام, ، في bash
و zsh
الأسماء المستعارة لها الأسبقية القصوى، لذلك يجب عليك تشغيلها للحماية من إعادة تعريف الكلمات الرئيسية unalias
بأسمائهم (على الرغم من وجودها في غير تفاعلية bash
القذائف (مثل البرامج النصية) هي الأسماء المستعارة لا موسعة بشكل افتراضي - فقط إذا shopt -s expand_aliases
يُطلق عليه صراحة أولاً).
لتتآكد من ذلك unalias
- باعتباره مدمجًا - له معناه الأصلي، ويجب عليك استخدامه \unset
عليه أولا، وهو ما يقتضي ذلك unset
لها معناها الأصلي:
unset
هي قذيفة مدمج, ، لذا للتأكد من أنه تم استدعاؤه على هذا النحو، يجب عليك التأكد من عدم إعادة تعريفه على أنه وظيفة.بينما يمكنك تجاوز نموذج الاسم المستعار مع الاقتباس، لا يمكنك تجاوز نموذج دالة الصدفة - التقط 22.
وبالتالي، إلا إذا كنت تستطيع الاعتماد عليها unset
للحصول على معناها الأصلي، حسب ما أستطيع قوله، لا توجد طريقة مضمونة للدفاع ضد كل إعادة التعريفات الخبيثة.
غالبًا ما يتعين على نصوص 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`
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
ولكن من أجل 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
}
هنا مثال كلاسيكي، وذلك بفضل الشراب:
%% 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
استخدم هذه الوظيفة وستعيد المسار -الحقيقي-:
%% 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');"`
ليست رائعة ولكنها متقاطعة مع نظام التشغيل
إليك كيفية الحصول على المسار الفعلي للملف في MacOS/Unix باستخدام برنامج Perl النصي المضمن:
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
لذلك، والذي تم تحويله إلى موقع حقيقي من خلال الحيلة البسيطة التي وصفتها لأول مرة.
أعتقد أن هذه هي "الطريقة الحقيقية والنهائية لحل الارتباط الرمزي" سواء كان دليلاً أو غير دليل، باستخدام 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##*/}"
)}
لاحظ أن جميع cd
و set
الأشياء تحدث في غلاف فرعي.
أقدم هنا ما أعتقد أنه حل مشترك بين الأنظمة الأساسية (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
}