Comment résoudre des liens symboliques dans un script shell

Étant donné un chemin absolu ou relatif (dans un système de type Unix), j’aimerais déterminer le chemin complet de la cible après avoir résolu les liens symboliques intermédiaires. Points bonus pour résoudre aussi ~ la notation du nom d’utilisateur en même temps.

Si la cible est un répertoire, il peut ê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 à cacher l’existence de liens symboliques à l’utilisateur (ceci 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 resolve () telle que lorsqu’elle est exécutée à partir du répertoire tmp dans l’exemple ci-dessus, resolus (“foo”) == “/ Users / greg / tmp / bar”.

Selon les normes, pwd -P devrait renvoyer le chemin avec les liens symboliques résolus.

C fonction char *getcwd(char *buf, size_t size) de unistd.h devrait avoir le même comportement.

getcwd pwd

 readlink -f "$path" 

Note de l’éditeur: Ce qui précède fonctionne avec GNU readlink et FreeBSD / PC-BSD / OpenBSD readlink , mais pas sous OS X à partir de 10.11.
GNU readlink offre des options supplémentaires, telles que -m pour résoudre un lien symbolique, que la cible finale existe ou non.

Notez que depuis GNU coreutils 8.15 (2012-01-06), il existe un programme realpath moins obtus et plus flexible que ci-dessus. 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 

[Admin add ci-dessous du commentaire de halloleo – danorton]

Pour Mac OS X (via au moins 10.11.x), utilisez readlink sans l’option -f :

 readlink $path 

Note de l’éditeur: Cela ne résoudra pas les liens symboliques de manière récursive et ne signalera donc pas la cible finale . Par exemple, si vous donnez a lien symbolique a qui pointe vers b , qui pointe à son tour vers c , cela ne signalera que b (et ne fera pas en sorte qu’il soit généré sous la forme d’ un chemin absolu ).
Utilisez la commande perl suivante sur OS X pour combler la lacune de la fonctionnalité readlink -f manquante:
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, 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 - retourne le chemin absolu canonicalisé

 realpath développe tous les liens symboliques et résout les références aux caractères '/./', '/../' et extra '/' dans la chaîne terminée par un caractère nul nommée par chemin et
        stocke le nom de chemin absolu canonicalisé dans la mémoire tampon de taille PATH_MAX nommée parpath_path.  Le chemin résultant n'aura aucun lien symbolique, '/./' ou
        '/../' Composants.
 readlink -e [filepath] 

semble être exactement ce que vous demandez – il accepte un chemin arbitraire, résout tous les liens symboliques et renvoie le “vrai” chemin – et il est “standard * nix” que tous les systèmes ont probablement déjà

En combinant 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* ]] pour exclure -f uniquement d’OSX.

 #!/bin/bash MY_DIR=$( cd $(dirname $(readlink `[[ $OSTYPE == linux* ]] && echo "-f"` $0)) ; pwd -P) echo "$MY_DIR" 

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

Note: Je pense qu’il s’agit d’une solution solide, portable et prête à l’emploi, qui est invariablement longue pour cette raison.

Vous trouverez ci-dessous un script / une fonction entièrement compatible avec POSIX, qui est donc multi-plateforme (fonctionne également sur macOS, dont readlink ne supporte toujours pas -f partir de 10.12 (Sierra)) – il utilise uniquement des fonctionnalités POSIX. appels d’utilité conformes.

C’est une implémentation portable de readlink -e de GNU (la version plus ssortingcte de readlink -f ).

Vous pouvez exécuter le script avec sh ou la source 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 la résolution des liens symboliques:

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

rreadlink script / fonction rreadlink :

Le code a été adapté avec gratitude de cette réponse .
J’ai également créé une version utilitaire autonome basée sur bash , que vous pouvez installer avec
npm install rreadlink -g , si vous avez installé Node.js.

 #!/bin/sh # SYNOPSIS # rreadlink  # DESCRIPTION # Resolves  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 ssortingng ' -> '. # 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 "$@" 

Une tangente sur la sécurité:

jarno , en référence à la fonction assurant que la command intégrée n’est pas masquée par un alias ou une fonction shell du même nom, demande dans un commentaire:

Que se passe-t-il si unalias ou unset et [ sont définis comme des alias ou des fonctions shell?

La motivation de rreadlink qui garantit que la command a son sens originel, est de l’utiliser pour contourner les alias de commodité (bénins) et les fonctions souvent utilisées pour masquer les commandes standard dans les shells interactifs, comme redéfinir ls pour inclure les options favorites.

Je pense qu’il est prudent de dire que si vous ne traitez pas avec un environnement malveillant et non sécurisé, vous inquiétez pas de la unalias de l’ unalias ou de l’ unalias – ou, en fait, while do

Il y a quelque chose sur lequel la fonction doit compter pour avoir son sens et son comportement originaux – il n’y a pas moyen de contourner cela.
Le fait que des shells de type POSIX permettent de redéfinir les mots-clés intégrés et même les mots-clés linguistiques est insortingnsèquement un risque de sécurité (et écrire du code paranoïaque est généralement difficile).

Pour répondre spécifiquement à vos préoccupations:

La fonction repose sur unalias et unset ayant leur signification originale. Les redéfinir en tant que fonctions de shell d’une manière qui modifie leur comportement serait un problème; la redéfinition en tant qu’alias n’est pas nécessairement un problème, car citer (en partie) le nom de la commande (par exemple, \unalias ) \unalias alias.

Cependant, les guillemets ne sont pas une option pour les mots-clés shell ( while , for , if , do , …) et les mots-clés shell ont préséance sur les fonctions shell, mais les alias bash et zsh ont la priorité la plus élevée. redéfinitions des mots clés Vous devez exécuter unalias avec leurs noms (bien que dans les shell bash non interactifs (tels que les scripts), les alias ne soient pas développés par défaut – seulement si shopt -s expand_aliases est explicitement appelé en premier).

Pour vous assurer que unalias – en tant que commande intégrée – a sa signification d’origine, vous devez d’abord utiliser \unset , ce qui nécessite que unset ait sa signification d’origine:

unset est un shell intégré , donc pour vous assurer qu’il est appelé en tant que tel, vous devez vous assurer qu’il n’est pas redéfini en tant que fonction . Bien que vous puissiez ignorer un formulaire d’alias avec des guillemets, vous ne pouvez pas ignorer un formulaire de fonction de shell – catch 22.

Ainsi, à moins que vous ne puissiez vous fier à l’ unset pour avoir son sens originel, de ce que je peux dire, il n’existe aucun moyen garanti de se défendre contre toutes les redéfinitions malveillantes.

Les scripts shell communs doivent souvent trouver leur répertoire “home” même s’ils sont appelé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 les éléments suivants, ce qui devrait constituer une bonne indication de 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)) 

Depuis que j’ai rencontré ce problème plusieurs fois au cours des années, et cette fois-ci, j’avais besoin d’une version portable bash pure que je pouvais utiliser sur OSX et Linux. J’en ai écrit une:

La version vivante vit ici:

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

mais pour le bien de SO, voici la version actuelle (je pense que c’est bien testé .. mais je suis ouvert aux commentaires!)

Peut-être pas difficile à faire fonctionner pour un shell ordinaire (sh), mais je n’ai pas essayé … J’aime trop $ FUNCNAME. 🙂

 #!/bin/bash resolve_path() { #I'm bash only, please! # usage: resolve_path  # 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, merci à brew:

 %% ls -l `which mvn` lrwxr-xr-x 1 draissortingck 502 29 Dec 17 10:50 /usr/local/bin/mvn@ -> ../Cellar/maven/3.2.3/bin/mvn 

utilisez cette fonction et elle retournera le chemin -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 

Pour contourner l’incompatibilité Mac, j’ai trouvé

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

Pas génial mais cross OS

Votre chemin est-il un répertoire ou peut-être un fichier? Si c’est un répertoire, c’est simple:

 (cd "$DIR"; pwd -P) 

Cependant, si cela peut être un fichier, cela ne fonctionnera pas:

 DIR=$(cd $(dirname "$FILE"); pwd -P); echo "${DIR}/$(readlink "$FILE")" 

car le lien symbolique peut se transformer en un chemin relatif ou complet.

Sur les scripts, je dois trouver le vrai chemin, afin que je puisse faire référence à la configuration ou à d’autres scripts installés avec lui, 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 pouvez définir SOURCE sur n’importe quel chemin de fichier. Fondamentalement, tant que le chemin est un lien symbolique, il résout ce lien symbolique. Le tour est 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 appenda le DIR à ce dernier, qui a été résolu en un emplacement réel par le simple truc que j’ai décrit pour la première fois.

Je crois que c’est la véritable “façon de résoudre un lien symbolique” que ce soit un répertoire ou 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 éléments du cd et du set lieu dans un sous-shell.

Voici comment on peut obtenir le chemin d’access au fichier dans MacOS / Unix en utilisant 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 symlinked:

 DIR=$(perl -e "use Cwd qw(abs_path); use File::Basename; print dirname(abs_path('$0'))")