Cómo resolver enlaces simbólicos en un script de shell

Dada una ruta absoluta o relativa (en un sistema tipo Unix), me gustaría determinar la ruta completa del objective después de resolver cualquier enlace simbólico intermedio. Puntos de bonificación por resolver ~ notación de nombre de usuario al mismo tiempo.

Si el destino es un directorio, podría ser posible chdir () en el directorio y luego llamar a getcwd (), pero realmente quiero hacer esto desde un script de shell en lugar de escribir un C helper. Desafortunadamente, las shells tienden a tratar de ocultar la existencia de enlaces simbólicos del usuario (esto es bash en 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 $ 

Lo que quiero es una función resolve () tal que cuando se ejecute desde el directorio tmp en el ejemplo anterior, resuelva (“foo”) == “/ Users / greg / tmp / bar”.

De acuerdo con los estándares, pwd -P debería devolver la ruta con enlaces simbólicos resueltos.

La función C char *getcwd(char *buf, size_t size) de unistd.h debe tener el mismo comportamiento.

getcwd pwd

 readlink -f "$path" 

Nota del editor: Lo anterior funciona con GNU readlink y FreeBSD / PC-BSD / OpenBSD readlink , pero no en OS X a partir de 10.11.
El readlink GNU ofrece opciones adicionales relacionadas, como -m para resolver un enlace simbólico, ya sea que exista o no el objective final.

Nota desde GNU coreutils 8.15 (2012-01-06), hay disponible un progtwig realpath que es menos obtuso y más flexible que el anterior. También es compatible con la utilidad FreeBSD del mismo nombre. También incluye funcionalidad para generar una ruta relativa entre dos archivos.

 realpath $path 

[ Adición de administrador a continuación del comentario de halloleo – danorton]

Para Mac OS X (a través de al menos 10.11.x), use readlink sin la opción -f :

 readlink $path 

Nota del editor: Esto no resolverá los enlaces simbólicos recursivamente y por lo tanto no informará el objective final ; por ejemplo, dado el enlace simbólico a que apunta a b , que a su vez apunta a c , esto solo informará b (y no asegurará que se muestre como una ruta absoluta ).
Utilice el siguiente comando perl en OS X para llenar el espacio de la faltante de readlink -f funcionalidad:
perl -MCwd -le 'print Cwd::abs_path(shift)' "$path"

“pwd -P” parece funcionar si solo quieres el directorio, pero si por alguna razón quieres el nombre del ejecutable real, no creo que eso ayude. Aquí está mi solución:

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

Uno de mis favoritos es realpath foo

 realpath - devuelve el nombre de ruta absoluto canonicalizado

 realpath expande todos los enlaces simbólicos y resuelve las referencias a los caracteres '/./', '/../' y extra '/' en la cadena terminada en nulo nombrada por ruta y
        almacena el nombre de ruta absoluto canonicalizado en el almacenamiento intermedio de tamaño PATH_MAX nombrado por vía_resolución.  La ruta resultante no tendrá un enlace simbólico, '/./' o
        '/../' componentes.
 readlink -e [filepath] 

parece ser exactamente lo que estás pidiendo: acepta una ruta arbirary, resuelve todos los enlaces simbólicos, y devuelve la ruta “real”, y es “estándar * nix” que probablemente todos los sistemas ya tienen

Al juntar algunas de las soluciones dadas, sabiendo que readlink está disponible en la mayoría de los sistemas, pero necesita diferentes argumentos, esto funciona bien para mí en OSX y Debian. No estoy seguro acerca de los sistemas BSD. Tal vez la condición debe ser [[ $OSTYPE != darwin* ]] para excluir -f de OSX solamente.

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

De otra manera:

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

Nota: Creo que se trata de una solución sólida, portátil y preparada, que es invariablemente prolongada por esa misma razón.

A continuación se muestra un script / función completamente POSIX que es, por lo tanto , multiplataforma (funciona también en macOS, cuyo readlink aún no es compatible con -f partir de 10.12 (Sierra)) – usa solo características de lenguaje de shell POSIX y solo POSIX- llamadas de servicios compatibles.

Es una implementación portátil del readlink -e de GNU (la versión más estricta de readlink -f ).

Puede ejecutar el script con sh o fuente de la función en bash , ksh y zsh :

Por ejemplo, dentro de un script puede usarlo de la siguiente manera para obtener el verdadero directorio de origen del script en ejecución, con los enlaces simbólicos resueltos:

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

rreadlink secuencia de comandos / función rreadlink :

El código fue adaptado con gratitud de esta respuesta .
También he creado una versión de la utilidad autónoma basada en bash , que puede instalar con
npm install rreadlink -g , si tienes Node.js instalado.

 #!/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 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 "$@" 

Una tangente a la seguridad:

jarno , en referencia a la función que asegura que el command incorporado no está sombreado por una función de alias o shell del mismo nombre, pregunta en un comentario:

¿Qué unalias si unalias o unset y [ se establecen como alias o funciones de shell?

La motivación detrás de rreadlink garantizar que el command tenga su significado original es usarlo para eludir (benigno) los alias y funciones de uso frecuente que se utilizan para sombrear comandos estándar en shells interactivos, como la redefinición de ls para incluir las opciones favoritas.

Creo que es seguro decir que a menos que estés lidiando con un entorno no confiable y malicioso, preocuparte por unalias o unset , o por el hecho de que, while do , redefinirlo no sea una preocupación.

Hay algo en lo que la función debe confiar para tener su significado y comportamiento original: no hay forma de evitarlo.
Que los shells similares a POSIX permiten redefinir los builtins e incluso las palabras clave del lenguaje es intrínsecamente un riesgo de seguridad (y escribir un código paranoico es difícil en general).

Para abordar sus inquietudes específicamente:

La función se basa en unalias y no tiene su significado original. Tenerlos redefinidos como funciones de shell de una manera que altera su comportamiento sería un problema; la redefinición como un alias no es necesariamente una preocupación, ya que al citar (parte del) nombre del comando (por ejemplo, \unalias ) se \unalias alias.

Sin embargo, las cotizaciones no son una opción para palabras clave de shell ( while , for , if , do , …) y mientras que las palabras clave de shell tienen prioridad sobre las funciones de shell, en bash y zsh alias tienen la mayor precedencia, por lo que se debe evitar Las redefiniciones de palabras clave deben ejecutar unalias con sus nombres (aunque en los shell bash no interactivos (como los scripts) los alias no se expanden de manera predeterminada, solo si shopt -s expand_aliases se llama explícitamente primero).

Para garantizar que las unalias , como una unalias interna, tengan su significado original, primero debe usar \unset en ella, lo que requiere que unset tenga su significado original:

unset es un shell incorporado , por lo que para garantizar que se invoque como tal, debe asegurarse de que no se redefina como una función . Aunque puede omitir un formulario de alias con comillas, no puede omitir un formulario de función de shell – catch 22.

Por lo tanto, a menos que pueda confiar en que unset tiene su significado original, por lo que puedo decir, no hay una manera garantizada de defenderse contra todas las redefiniciones maliciosas.

Los scripts de shell comunes a menudo tienen que encontrar su directorio “home” incluso si se invocan como un enlace simbólico. El script debe encontrar su posición “real” desde solo $ 0.

 cat `mvn` 

en mi sistema imprime un script que contiene lo siguiente, que debería ser una buena pista de lo que necesita.

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

Prueba esto:

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

Como me he encontrado con esto muchas veces a lo largo de los años, y esta vez necesitaba una versión portátil de bash pura que pudiera usar en OSX y Linux, continué y escribí una:

La versión viva vive aquí:

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

pero por el bien de SO, aquí está la versión actual (creo que está bien probado … ¡pero estoy abierto a comentarios!)

Puede que no sea difícil hacer que funcione para bourne shell (sh), pero no lo intenté … me gusta demasiado $ 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 } 

he aquí un ejemplo clásico, gracias a 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 

usa esta función y devolverá la ruta real -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 

Para evitar la incompatibilidad de Mac, se me ocurrió

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

No es genial, pero cruz OS

¿Es su ruta un directorio, o podría ser un archivo? Si es un directorio, es simple:

 (cd "$DIR"; pwd -P) 

Sin embargo, si puede ser un archivo, entonces esto no funcionará:

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

porque el enlace simbólico podría resolverse en una ruta relativa o completa.

En las secuencias de comandos, necesito encontrar la ruta real, para poder hacer referencia a la configuración u otras secuencias de comandos instaladas con ella, utilizo esto:

 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 

Puede establecer SOURCE en cualquier ruta de archivo. Básicamente, siempre que la ruta sea un enlace simbólico, resuelve ese enlace simbólico. El truco está en la última línea del bucle. Si el enlace simbólico resuelto es absoluto, lo usará como SOURCE . Sin embargo, si es relativo, antepondrá el DIR para él, que se resolvió en una ubicación real mediante el simple truco que describí por primera vez.

Creo que esta es la verdadera y definitiva “forma de resolver un enlace simbólico” ya sea un directorio o no directorio, usando 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##*/}" )} 

Tenga en cuenta que todo el material de cd y de set tiene lugar en una subcamada.

Aquí se explica cómo se puede obtener la ruta real al archivo en MacOS / Unix usando una secuencia de comandos de Perl en línea:

 FILE=$(perl -e "use Cwd qw(abs_path); print abs_path('$0')") 

Del mismo modo, para obtener el directorio de un archivo de enlace simbólico:

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