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

Foi útil?

Solução

De acordo com os padrões, pwd -P deve retornar o caminho com links simbólicos resolvidos.

Função C char *getcwd(char *buf, size_t size) de unistd.h deveria ter o mesmo comportamento.

getcwd senha

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 bashversã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 ou unset 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.

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
}
Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top