Como resolver links simbólicos em um script de shell
Pergunta
Dado um caminho absoluto ou relativo (em um sistema semelhante ao Unix), gostaria de determinar o caminho completo do alvo após resolver quaisquer links simbólicos intermediários.Pontos de bônus por resolver também a notação ~username ao mesmo tempo.
Se o destino for um diretório, pode ser possível inserir chdir() no diretório e depois chamar getcwd(), mas eu realmente quero fazer isso a partir de um script de shell em vez de escrever um auxiliar C.Infelizmente, os shells tendem a tentar ocultar a existência de links simbólicos do usuário (isso é bash no 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
$
O que eu quero é uma função resolve() tal que, quando executada a partir do diretório tmp no exemplo acima, resolve("foo") == "/Users/greg/tmp/bar".
Outras dicas
readlink -f "$path"
Nota do editor:O acima funciona com GNU readlink
e FreeBSD/PC-BSD/OpenBSD readlink
, mas não no OS X a partir de 10.11.
GNU readlink
oferece opções adicionais relacionadas, como -m
para resolver um link simbólico, independentemente de o alvo final existir ou não.
Observe que desde GNU coreutils 8.15 (06/01/2012), há um caminho real programa disponível que é menos obtuso e mais flexível do que o acima.Também é compatível com o utilitário FreeBSD de mesmo nome.Também inclui funcionalidade para gerar um caminho relativo entre dois arquivos.
realpath $path
[Adição do administrador abaixo do comentário de olá —danorton]
Para Mac OS X (até pelo menos 10.11.x), use readlink
sem o -f
opção:
readlink $path
Nota do editor:Isso não resolverá links simbólicos recursivamente e, portanto, não relatará o final alvo;por exemplo, dado link simbólico a
que aponta para b
, que por sua vez aponta para c
, isso só reportará b
(e não garantirá que seja gerado como um caminho absoluto).
Use o seguinte perl
comando no OS X para preencher a lacuna da falta readlink -f
funcionalidade:
perl -MCwd -le 'print Cwd::abs_path(shift)' "$path"
"pwd -P" parece funcionar se você quiser apenas o diretório, mas se por algum motivo quiser o nome do executável real, não acho que isso ajude.Aqui está minha solução:
#!/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
Um dos meus favoritos é 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]
Parece ser exatamente o que você está pedindo - ele aceita um caminho arbirário, resolve todos os links simbólicos e retorna o caminho "real" - e é "padrão *nix" que provavelmente todos os sistemas já têm
Outra maneira:
# 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|"; }
Juntando algumas das soluções fornecidas, sabendo que o readlink está disponível na maioria dos sistemas, mas precisa de argumentos diferentes, isso funciona bem para mim no OSX e no Debian.Não tenho certeza sobre os sistemas BSD.Talvez a condição precise ser [[ $OSTYPE != darwin* ]]
excluir -f
apenas do OSX.
#!/bin/bash
MY_DIR=$( cd $(dirname $(readlink `[[ $OSTYPE == linux* ]] && echo "-f"` $0)) ; pwd -P)
echo "$MY_DIR"
Observação:Acredito que esta seja uma solução sólida, portátil, pronta para uso, que é invariavelmente demorado por isso mesmo.
Abaixo está um script/função totalmente compatível com POSIX isso é portanto multiplataforma (funciona também no macOS, cujo readlink
ainda não suporta -f
a partir de 10.12 (Sierra)) - usa apenas Recursos de linguagem shell POSIX e apenas chamadas de utilitários compatíveis com POSIX.
É um implementação portátil do GNU readlink -e
(a versão mais estrita de readlink -f
).
Você pode execute o roteiro com sh
ou fonte do função em bash
, ksh
, e zsh
:
Por exemplo, dentro de um script você pode usá-lo da seguinte maneira para obter o verdadeiro diretório de origem do script em execução, com links simbólicos resolvidos:
trueScriptDir=$(dirname -- "$(rreadlink "$0")")
rreadlink
definição de script/função:
O código foi adaptado com gratidão de esta resposta.
Eu também criei um bash
versão de utilitário independente baseada em aqui, com o qual você pode instalar
npm install rreadlink -g
, se você tiver o Node.js instalado.
#!/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 "$@"
Uma tangente à segurança:
jarno, em referência à função que garante que o builtin command
não é obscurecido por um alias ou função shell com o mesmo nome, pergunta em um comentário:
E se
unalias
ouunset
e[
são definidos como aliases ou funções shell?
A motivação por trás rreadlink
garantindo que command
tem seu significado original é usá-lo para contornar (benigno) conveniência aliases e funções frequentemente usados para ocultar comandos padrão em shells interativos, como redefinir ls
para incluir opções favoritas.
Acho que é seguro dizer que, a menos que você esteja lidando com um ambiente malicioso e não confiável, se preocupando com unalias
ou unset
- ou, nesse caso, while
, do
, ...- ser redefinido não é uma preocupação.
Há algo em que a função deve confiar para ter seu significado e comportamento originais - não há como evitar isso.
Que os shells do tipo POSIX permitem a redefinição de componentes internos e até mesmo de palavras-chave de linguagem é inerentemente um risco de segurança (e escrever código paranóico é difícil em geral).
Para abordar suas preocupações especificamente:
A função depende unalias
e unset
tendo seu significado original.Tê-los redefinidos como funções de shell de uma forma que altere o seu comportamento seria um problema;redefinição como apelido não é necessariamente uma preocupação, porque citando (parte do) nome do comando (por exemplo, \unalias
) ignora aliases.
Contudo, citar é não uma opção para shell palavras-chave (while
, for
, if
, do
, ...) e embora as palavras-chave do shell tenham precedência sobre o shell funções, em bash
e zsh
aliases têm a precedência mais alta, portanto, para se proteger contra redefinições de palavras-chave do shell, você deve executar unalias
com seus nomes (embora em não interativo bash
aliases de shells (como scripts) são não expandido por padrão - somente se shopt -s expand_aliases
é explicitamente chamado primeiro).
Para garantir que unalias
- como embutido - tem seu significado original, você deve usar \unset
primeiro, o que exige que unset
tem seu significado original:
unset
é uma concha construídas em, portanto, para garantir que ele seja invocado como tal, você precisará garantir que ele não seja redefinido como um função.Embora você possa ignorar um formulário de alias com aspas, não pode ignorar um formulário de função shell - captura 22.
Assim, a menos que você possa confiar unset
para ter seu significado original, pelo que posso dizer, não há uma maneira garantida de defesa contra todas as redefinições maliciosas.
Os scripts shell comuns geralmente precisam encontrar seu diretório "inicial", mesmo que sejam invocados como um link simbólico.O script, portanto, precisa encontrar sua posição "real" a partir de apenas US$ 0.
cat `mvn`
no meu sistema imprime um script contendo o seguinte, que deve ser uma boa dica do que você precisa.
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"))/..
Experimente isto:
cd $(dirname $([ -L $0 ] && readlink -f $0 || echo $0))
Como já me deparei com isso muitas vezes ao longo dos anos, e desta vez eu precisava de uma versão portátil pura do bash que pudesse usar no OSX e no Linux, fui em frente e escrevi uma:
A versão viva mora aqui:
https://github.com/keen99/shell-functions/tree/master/resolve_path
mas por uma questão de SO, aqui está a versão atual (acho que está bem testada... mas estou aberto a comentários!)
Pode não ser difícil fazê-lo funcionar para o bourne shell (sh) simples, mas não tentei... Gosto muito de $ 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
}
aqui está um exemplo clássico, graças ao 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
use esta função e ela retornará o caminho -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 contornar a incompatibilidade do Mac, criei
echo `php -r "echo realpath('foo');"`
Não é ótimo, mas cruza o sistema operacional
Veja como obter o caminho real para o arquivo no MacOS/Unix usando um script Perl embutido:
FILE=$(perl -e "use Cwd qw(abs_path); print abs_path('$0')")
Da mesma forma, para obter o diretório de um arquivo com link simbólico:
DIR=$(perl -e "use Cwd qw(abs_path); use File::Basename; print dirname(abs_path('$0'))")
O seu caminho é um diretório ou pode ser um arquivo?Se for um diretório, é simples:
(cd "$DIR"; pwd -P)
No entanto, se for um arquivo, isso não funcionará:
DIR=$(cd $(dirname "$FILE"); pwd -P); echo "${DIR}/$(readlink "$FILE")"
porque o link simbólico pode ser resolvido em um caminho relativo ou completo.
Nos scripts eu preciso encontrar o caminho real, para poder referenciar a configuração ou outros scripts instalados junto com ele, eu uso isto:
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
Você poderia definir SOURCE
para qualquer caminho de arquivo.Basicamente, desde que o caminho seja um link simbólico, ele resolve esse link simbólico.O truque está na última linha do loop.Se o link simbólico resolvido for absoluto, ele usará isso como SOURCE
.No entanto, se for relativo, precederá o DIR
para ele, que foi resolvido em um local real pelo truque simples que descrevi primeiro.
Acredito que esta seja a verdadeira e definitiva "maneira de resolver um link simbólico", seja ele um diretório ou não, 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##*/}"
)}
Observe que todos os cd
e set
as coisas acontecem em um subshell.
Aqui apresento o que acredito ser uma solução multiplataforma (pelo menos Linux e macOS) para a resposta que está funcionando bem para mim atualmente.
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"
}
Aqui está uma solução macOS (apenas?).Possivelmente mais adequado à pergunta original.
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
}