Как разрешить символические ссылки в сценарии оболочки

StackOverflow https://stackoverflow.com/questions/7665

  •  08-06-2019
  •  | 
  •  

Вопрос

Учитывая абсолютный или относительный путь (в Unix-подобной системе), я хотел бы определить полный путь к цели после разрешения любых промежуточных символических ссылок.Бонусные баллы за одновременное разрешение обозначения ~username.

Если целью является каталог, возможно, можно вызвать chdir() в каталог, а затем вызвать getcwd(), но я действительно хочу сделать это из сценария оболочки, а не писать помощник C.К сожалению, оболочки имеют тенденцию скрывать от пользователя существование символических ссылок (это bash в 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
$

Мне нужна функцияsolve(), которая при выполнении из каталога tmp в приведенном выше примере,solve("foo") == "/Users/greg/tmp/bar".

Это было полезно?

Решение

Согласно стандартам, pwd -P должен вернуть путь с разрешенными символическими ссылками.

Функция C char *getcwd(char *buf, size_t size) от unistd.h должно иметь такое же поведение.

получить cwd страдающий

Другие советы

readlink -f "$path"

Примечание редактора:Вышеописанное работает с ГНУ readlink и FreeBSD/ПК-BSD/OpenBSD readlink, но нет на OS X по состоянию на 10.11.
ГНУ readlink предлагает дополнительные связанные опции, такие как -m для разрешения символической ссылки независимо от того, существует ли конечная цель.

Обратите внимание, что начиная с версии GNU coreutils 8.15 (06.01.2012) существует реальный путь доступна программа, которая менее запутана и более гибка, чем приведенная выше.Он также совместим с одноименной утилитой FreeBSD.Он также включает в себя функцию создания относительного пути между двумя файлами.

realpath $path

[Дополнение администратора ниже из комментария аллилеоДанортон]

Для Mac OS X (по крайней мере до 10.11.x) используйте readlink без -f вариант:

readlink $path

Примечание редактора:Это не разрешит символические ссылки рекурсивно и поэтому не сообщит об окончательный цель;например, учитывая символическую ссылку a это указывает на b, что, в свою очередь, указывает на c, это будет сообщать только b (и не будет гарантировать, что он будет выведен как абсолютный путь).
Используйте следующее perl команда в OS X, чтобы заполнить недостающий пробел readlink -f функциональность:
perl -MCwd -le 'print Cwd::abs_path(shift)' "$path"

«pwd -P», похоже, работает, если вам просто нужен каталог, но если по какой-то причине вам нужно имя фактического исполняемого файла, я не думаю, что это поможет.Вот мое решение:

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

Один из моих любимых 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]

кажется, именно то, о чем вы просите - он принимает арбирарный путь, разрешает все символики и возвращает «реальный» путь - и это «стандартный *nix», что, вероятно, у всех систем уже есть

Другой путь:

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

Объединив некоторые из приведенных решений, зная, что readlink доступен в большинстве систем, но требует разных аргументов, это хорошо работает для меня в OSX и Debian.Я не уверен насчет систем BSD.Возможно, условие должно быть [[ $OSTYPE != darwin* ]] исключать -f только из OSX.

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

Примечание:Я считаю, что это надежное, портативное, готовое решение, которое неизменно длинный именно по этой причине.

Ниже приведен полностью POSIX-совместимый сценарий/функция поэтому это кроссплатформенный (работает и на macOS, чей readlink все еще не поддерживает -f по состоянию на 10.12 (Сьерра)) - используется только Возможности языка оболочки POSIX и только вызовы утилит, совместимые с POSIX.

Это портативная реализация GNU readlink -e (более строгая версия readlink -f).

Ты можешь запустить сценарий с sh или источник функция в bash, ksh, и zsh:

Например, внутри скрипта вы можете использовать его следующим образом, чтобы получить истинный исходный каталог запущенного скрипта с разрешенными символическими ссылками:

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

rreadlink определение скрипта/функции:

Код адаптирован с благодарностью от этот ответ.
Я также создал bash- автономная версия утилиты здесь, который вы можете установить с помощью
npm install rreadlink -g, если у вас установлен Node.js.

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

Касательная безопасность:

Ярно, в отношении функции, гарантирующей, что встроенная command не затеняется псевдонимом или одноименной функцией оболочки, спрашивается в комментарии:

Что, если unalias или unset и [ установлены как псевдонимы или функции оболочки?

Мотивация, лежащая в основе rreadlink обеспечение того, чтобы command имеет свое первоначальное значение - использовать его для обхода (мягкое) удобство псевдонимы и функции, часто используемые для скрытия стандартных команд в интерактивных оболочках, например переопределение ls чтобы включить любимые варианты.

Я думаю, можно с уверенностью сказать, что, если только вы не имеете дело с ненадежной вредоносной средой, беспокоясь о unalias или unset - или, если уж на то пошло, while, do, ...- переопределение не является проблемой.

Есть что-нибудь что функция должна полагаться на свое первоначальное значение и поведение - обойти это невозможно.
То, что POSIX-подобные оболочки позволяют переопределять встроенные функции и даже ключевые слова языка, по своей сути угроза безопасности (а писать параноидальный код вообще сложно).

Чтобы конкретно решить ваши проблемы:

Функция опирается на unalias и unset имеющие свое первоначальное значение.Их переопределение как функции оболочки таким образом, чтобы изменить их поведение, это было бы проблемой;переопределение как псевдоним не обязательно проблема, потому что цитирование (часть) имени команды (например, \unalias) обходит псевдонимы.

Однако цитирование нет вариант для оболочки ключевые слова (while, for, if, do, ...) и хотя ключевые слова оболочки имеют приоритет над оболочкой функции, в bash и zsh псевдонимы имеют наивысший приоритет, поэтому для защиты от переопределения ключевых слов оболочки вы должны запустить unalias с их именами (хотя в неинтерактивный bash псевдонимы оболочек (таких как скрипты) нет расширено по умолчанию - только если shopt -s expand_aliases явно вызывается первым).

Чтобы гарантировать, что unalias - как встроенный - имеет исходное значение, необходимо использовать \unset об этом в первую очередь, что требует, чтобы unset имеют первоначальный смысл:

unset это оболочка встроенный, поэтому, чтобы гарантировать, что он вызывается как таковой, вам необходимо убедиться, что он сам не переопределен как функция.Хотя вы можете обойти форму псевдонима с помощью кавычек, вы не можете обойти форму функции оболочки — ловушка 22.

Таким образом, если вы не можете положиться на unset Чтобы иметь свое первоначальное значение, насколько я могу судить, не существует гарантированного способа защититься от всех злонамеренных переопределений.

Обычным сценариям оболочки часто приходится находить свой «домашний» каталог, даже если они вызываются как символическая ссылка.Таким образом, сценарий должен найти их «настоящую» позицию всего от 0 долларов.

cat `mvn`

в моей системе печатается сценарий, содержащий следующее, что должно быть хорошим намеком на то, что вам нужно.

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

Попробуй это:

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

Поскольку я сталкивался с этим много раз на протяжении многих лет, и на этот раз мне нужна была чистая портативная версия bash, которую я мог бы использовать в OSX и Linux, я пошел дальше и написал ее:

Живая версия живет здесь:

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

но ради SO вот текущая версия (я считаю, что она хорошо протестирована... но я открыт для обратной связи!)

Возможно, не составит труда заставить его работать для простой оболочки (sh), но я не пробовал... Мне слишком нравится $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
}

вот классический пример, благодаря 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

используйте эту функцию, и она вернет -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 

Чтобы обойти несовместимость Mac, я придумал

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

Не очень хорошо, но кросс-ОС

Вот как можно получить фактический путь к файлу в MacOS/Unix, используя встроенный скрипт Perl:

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

Аналогично, чтобы получить каталог файла с символической ссылкой:

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

Ваш путь — это каталог или это может быть файл?Если это каталог, все просто:

(cd "$DIR"; pwd -P)

Однако, если это может быть файл, это не сработает:

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

потому что символическая ссылка может разрешиться в относительный или полный путь.

В сценариях мне нужно найти реальный путь, чтобы я мог ссылаться на конфигурацию или другие сценарии, установленные вместе с ним, я использую это:

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

Вы можете установить SOURCE к любому пути к файлу.По сути, пока путь является символической ссылкой, он разрешает эту символическую ссылку.Хитрость заключается в последней строке цикла.Если разрешенная символическая ссылка является абсолютной, она будет использовать ее как SOURCE.Однако, если он относительный, он будет добавлять DIR для него, которое было преобразовано в реальную локацию с помощью простого трюка, который я описал вначале.

Я считаю, что это истинный и окончательный «способ разрешения символической ссылки», независимо от того, является ли она каталогом или некаталогом, с использованием 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##*/}"
)}

Обратите внимание, что все cd и set все происходит в подоболочке.

Здесь я представляю то, что я считаю кроссплатформенным (по крайней мере, для Linux и macOS) решением для ответа, который в настоящее время работает для меня хорошо.

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

Вот решение для macOS (только?).Возможно, лучше подходит для исходного вопроса.

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
}
Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top