Comment résoudre les liens symboliques dans un script shell
Question
Étant donné un chemin absolu ou relatif (dans un système de type Unix), je voudrais déterminer le chemin complet de la cible après avoir résolu les liens symboliques intermédiaires.Points bonus pour résoudre également la notation ~ nom d'utilisateur en même temps.
Si la cible est un répertoire, il pourrait être possible de chdir() dans le répertoire puis d'appeler getcwd(), mais je veux vraiment le faire à partir d'un script shell plutôt que d'écrire un assistant C.Malheureusement, les shells ont tendance à essayer de cacher l'existence de liens symboliques à l'utilisateur (c'est bash sur 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
$
Ce que je veux, c'est une fonction solve() telle que lorsqu'elle est exécutée à partir du répertoire tmp dans l'exemple ci-dessus, solve("foo") == "/Users/greg/tmp/bar".
La solution
Selon les normes, pwd -P
devrait renvoyer le chemin avec les liens symboliques résolus.
Fonction C char *getcwd(char *buf, size_t size)
depuis unistd.h
devrait avoir le même comportement.
Autres conseils
readlink -f "$path"
Note de l'éditeur:Ce qui précède fonctionne avec GNOU readlink
et FreeBSD/PC-BSD/OpenBSD readlink
, mais pas sur OS X à partir du 10.11.
GNOU readlink
propose des options supplémentaires connexes, telles que -m
pour résoudre un lien symbolique, que la cible ultime existe ou non.
Notez que depuis GNU coreutils 8.15 (06/01/2012), il existe un chemin réel programme disponible qui est moins obtus et plus flexible que le précédent.Il est également compatible avec l'utilitaire FreeBSD du même nom.Il inclut également des fonctionnalités permettant de générer un chemin relatif entre deux fichiers.
realpath $path
[Ajout de l'administrateur ci-dessous à partir du commentaire de halloléo —Danorton]
Pour Mac OS X (jusqu'à au moins 10.11.x), utilisez readlink
sans le -f
option:
readlink $path
Note de l'éditeur:Cela ne résoudra pas les liens symboliques récursivement et ne signalera donc pas le ultime cible;par exemple, étant donné un lien symbolique a
qui pointe vers b
, ce qui à son tour indique c
, cela ne fera que signaler b
(et ne garantira pas qu'il soit affiché en tant que chemin absolu).
Utilisez le suivant perl
commande sur OS X pour combler le vide des manquants readlink -f
Fonctionnalité:
perl -MCwd -le 'print Cwd::abs_path(shift)' "$path"
"pwd -P" semble fonctionner si vous voulez juste le répertoire, mais si pour une raison quelconque vous voulez le nom de l'exécutable réel, je ne pense pas que cela aide.Voici ma solution :
#!/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
Un de mes favoris est 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]
Semble être exactement ce que vous demandez - il accepte un chemin d'arbiraire, résout tous les liens symboliques et renvoie le "vrai" chemin - et c'est "standard * nix" que tous les systèmes ont déjà déjà
Autrement:
# 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|"; }
En rassemblant certaines des solutions proposées, sachant que readlink est disponible sur la plupart des systèmes, mais nécessite des arguments différents, cela fonctionne bien pour moi sur OSX et Debian.Je ne suis pas sûr des systèmes BSD.Peut-être que la condition doit être [[ $OSTYPE != darwin* ]]
exclure -f
depuis OSX uniquement.
#!/bin/bash
MY_DIR=$( cd $(dirname $(readlink `[[ $OSTYPE == linux* ]] && echo "-f"` $0)) ; pwd -P)
echo "$MY_DIR"
Note:Je crois qu'il s'agit d'une solution solide, portable et prête à l'emploi, qui est invariablement longue pour cette raison même.
Ci-dessous se trouve un Script/fonction entièrement conforme à POSIX c'est donc multiplateforme (fonctionne également sur macOS, dont readlink
ne supporte toujours pas -f
à partir du 10.12 (Sierra)) - il utilise uniquement Fonctionnalités du langage shell POSIX et uniquement les appels utilitaires compatibles POSIX.
C'est un implémentation portable de GNU readlink -e
(la version la plus stricte de readlink -f
).
Tu peux exécuter le scénario avec sh
ou sourcer le fonction dans bash
, ksh
, et zsh
:
Par exemple, dans un script, vous pouvez l'utiliser comme suit pour obtenir le véritable répertoire d'origine du script en cours d'exécution, avec les liens symboliques résolus :
trueScriptDir=$(dirname -- "$(rreadlink "$0")")
rreadlink
définition du script/fonction :
Le code a été adapté avec la gratitude de cette réponse.
J'ai également créé un bash
-version utilitaire autonome basée sur ici, que vous pouvez installer avec
npm install rreadlink -g
, si Node.js est installé.
#!/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 "$@"
Une tangente sur la sécurité :
jarno, en référence à la fonction garantissant que le module intégré command
n'est pas masqué par un alias ou une fonction shell du même nom, demande dans un commentaire :
Et si
unalias
ouunset
et[
sont définis comme des alias ou des fonctions shell ?
La motivation derrière rreadlink
assurer que command
a sa signification originelle est de l'utiliser pour contourner (bénigne) commodité alias et fonctions souvent utilisés pour masquer les commandes standard dans les shells interactifs, telles que la redéfinition ls
pour inclure les options favorites.
Je pense qu'il est prudent de dire qu'à moins que vous n'ayez affaire à un environnement malveillant et non fiable, vous vous inquiétez unalias
ou unset
- ou, d'ailleurs, while
, do
,...- être redéfini n'est pas un souci.
Il y a quelque chose sur lequel la fonction doit s'appuyer pour avoir sa signification et son comportement d'origine - il n'y a aucun moyen de contourner cela.
Le fait que les shells de type POSIX permettent la redéfinition des éléments intégrés et même des mots-clés de langage est intrinsèquement un risque de sécurité (et écrire du code paranoïaque est difficile en général).
Pour répondre spécifiquement à vos préoccupations :
La fonction s'appuie sur unalias
et unset
ayant leur signification originelle.Les faire redéfinir comme fonctions shell d'une manière qui modifie leur comportement constituerait un problème ;redéfinition comme un alias n'est pas nécessairement une préoccupation, car citant (une partie du) nom de la commande (par exemple, \unalias
) contourne les alias.
Cependant, citer est pas une option pour le shell mots clés (while
, for
, if
, do
, ...) et bien que les mots-clés shell aient la priorité sur le shell les fonctions, dans bash
et zsh
les alias ont la priorité la plus élevée, donc pour vous prémunir contre les redéfinitions de mots-clés shell, vous devez exécuter unalias
avec leurs noms (bien que dans non interactif bash
les alias des shells (tels que les scripts) sont pas développé par défaut - seulement si shopt -s expand_aliases
est explicitement appelé en premier).
Pour être sur de unalias
- en tant que module intégré - a sa signification originale, vous devez utiliser \unset
dessus en premier, ce qui nécessite que unset
a sa signification originelle :
unset
est une coquille intégré, donc pour vous assurer qu'il est invoqué en tant que tel, vous devez vous assurer qu'il n'est pas lui-même redéfini comme un fonction.Bien que vous puissiez contourner un formulaire d'alias avec des guillemets, vous ne pouvez pas contourner un formulaire de fonction shell - catch 22.
Ainsi, à moins que vous puissiez compter sur unset
pour avoir son sens originel, d'après ce que je peux dire, il n'existe aucun moyen garanti de se défendre contre toutes les redéfinitions malveillantes.
Les scripts shell courants doivent souvent trouver leur répertoire "home" même s'ils sont invoqués en tant que lien symbolique.Le script doit donc trouver sa « vraie » position à partir de seulement 0 $.
cat `mvn`
sur mon système, imprime un script contenant ce qui suit, ce qui devrait être un bon indice sur ce dont vous avez besoin.
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"))/..
Essaye ça:
cd $(dirname $([ -L $0 ] && readlink -f $0 || echo $0))
Comme j'ai rencontré cela plusieurs fois au fil des ans, et que cette fois-ci j'avais besoin d'une version portable pure bash que je pourrais utiliser sur OSX et Linux, j'ai continué et j'en ai écrit une :
La version vivante se trouve ici :
https://github.com/keen99/shell-functions/tree/master/resolve_path
mais pour le bien de SO, voici la version actuelle (je pense qu'elle est bien testée... mais je suis ouvert aux commentaires !)
Ce n'est peut-être pas difficile de le faire fonctionner pour un simple bourne shell (sh), mais je n'ai pas essayé... J'aime trop $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
}
voici un exemple classique, grâce à 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
utilisez cette fonction et elle renverra le chemin -réel- :
%% 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
Pour contourner l'incompatibilité Mac, j'ai trouvé
echo `php -r "echo realpath('foo');"`
Pas génial mais cross OS
Voici comment obtenir le chemin réel du fichier sous MacOS/Unix à l'aide d'un script Perl en ligne :
FILE=$(perl -e "use Cwd qw(abs_path); print abs_path('$0')")
De même, pour obtenir le répertoire d'un fichier lié symboliquement :
DIR=$(perl -e "use Cwd qw(abs_path); use File::Basename; print dirname(abs_path('$0'))")
Votre chemin est-il un répertoire ou s'agit-il d'un fichier ?Si c'est un répertoire, c'est simple :
(cd "$DIR"; pwd -P)
Cependant, s'il s'agit d'un fichier, cela ne fonctionnera pas :
DIR=$(cd $(dirname "$FILE"); pwd -P); echo "${DIR}/$(readlink "$FILE")"
car le lien symbolique peut se résoudre en un chemin relatif ou complet.
Sur les scripts, je dois trouver le chemin réel, afin de pouvoir référencer la configuration ou d'autres scripts installés avec celui-ci, j'utilise ceci :
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
Vous pourriez définir SOURCE
à n’importe quel chemin de fichier.Fondamentalement, tant que le chemin est un lien symbolique, il résout ce lien symbolique.L'astuce se trouve dans la dernière ligne de la boucle.Si le lien symbolique résolu est absolu, il l'utilisera comme SOURCE
.Cependant, s'il est relatif, il ajoutera le préfixe DIR
pour cela, qui a été transformé en un emplacement réel par le simple truc que j'ai décrit pour la première fois.
Je pense que c'est la "façon véritable et définitive de résoudre un lien symbolique", qu'il s'agisse d'un répertoire ou d'un non-répertoire, en utilisant 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##*/}"
)}
Notez que tous les cd
et set
les choses se déroulent dans un sous-shell.
Je présente ici ce que je crois être une solution multiplateforme (Linux et macOS au moins) à la réponse qui fonctionne bien pour moi actuellement.
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"
}
Voici une solution macOS (uniquement ?).Peut-être mieux adapté à la question initiale.
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
}