Надежный способ для Bash-скрипта получить полный путь к себе

449

У меня есть bash script, который должен знать свой полный путь. Я пытаюсь найти широко совместимый способ сделать это, не заканчивая относительными или напуганными способами. Мне нужно поддерживать bash, а не sh, csh и т.д.

Что я нашел до сих пор:

  • Принятый ответ на Получение исходной директории bash script изнутри" адреса, получающего путь от script через dirname $0, это нормально, но может возвращать относительный путь (например, .), что является проблемой, если вы хотите сменить каталоги в script и указате путь в каталог script. Тем не менее, dirname будет частью головоломки.

  • Принятый ответ на "Bash script абсолютный путь с OSX" (конкретный OS X, но ответ работает независимо) дает функцию, который будет проверять, будет ли выглядеть $0 относительным, и если это так, то предварительно подкроет $PWD. Но результат все равно может иметь относительные биты (хотя в целом он абсолютный) — например, если script находится t в каталоге /usr/bin, и вы находитесь в /usr, и вы вводите bin/../bin/t для запуска (да, это запутанно), вы получаете /usr/bin/../bin как путь к каталогу script. Какой работает, но...

  • Решение readlink на этой странице, которое выглядит следующим образом:

    # Absolute path to this script. /home/user/bin/foo.sh
    SCRIPT=$(readlink -f $0)
    # Absolute path this script is in. /home/user/bin
    SCRIPTPATH=`dirname $SCRIPT`
    

    Но readlink не является POSIX и, по-видимому, решение полагается на GNU readlink, где BSD по какой-то причине не работает (у меня нет доступа к BSD-подобной системе для проверки).

Итак, различные способы сделать это, но все они имеют свои оговорки.

Что было бы лучше? Где "лучше" означает:

  • Дает мне абсолютный путь.
  • Выбрасывает фанковые биты даже при вызове запутанным способом (см. комментарий к № 2 выше). (Например, по меньшей мере, умеренно канонизирует путь.)
  • Опирается только на bash -изм или вещи, которые почти наверняка будут на самых популярных аксессуарах * nix-систем (GNU/Linux, BSD и BSD-подобных системах, таких как OS X и т.д.).
  • Избегает вызывать внешние программы, если это возможно (например, предпочитает bash встроенные модули).
  • (Обновлено, спасибо за головы, wich) Не нужно разрешать символические ссылки (на самом деле, я предпочитаю, чтобы он оставил их в покое, но это не требование).
  • 14
    Пожалуйста, смотрите BashFAQ / 028 .
  • 1
    Ссылка в решении № 3 выше не работает. У кого-нибудь есть обновленный?
Показать ещё 4 комментария
Теги:
path

23 ответа

321
Лучший ответ

Вот что я придумал (отредактируйте: плюс некоторые хитрости, предоставленные sfstewman, levigroker, Кайл Стрэнд и Роб Кеннеди), что, похоже, в основном соответствует моему "лучшему" критерии:

SCRIPTPATH="$( cd "$(dirname "$0")" ; pwd -P )"

Эта строка SCRIPTPATH кажется особенно круговой, но нам она нужна, а не SCRIPTPATH=`pwd`, чтобы правильно обрабатывать пробелы и символические ссылки.

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

  • 0
    все еще нет способа узнать, как называется сам файл?
  • 8
    Это не работает, если скрипт находится в каталоге из $PATH и вы называете его bash scriptname . В таком случае $0 не содержит никакого пути, только имя scriptname .
Показать ещё 4 комментария
134

Я удивлен, что команда realpath здесь не упоминается. Я понимаю, что он широко переносится/переносится.

Ваше начальное решение будет:

SCRIPT=`realpath $0`
SCRIPTPATH=`dirname $SCRIPT`

И чтобы оставить символические ссылки неразрешенными в соответствии с вашими предпочтениями:

SCRIPT=`realpath -s $0`
SCRIPTPATH=`dirname $SCRIPT`
  • 2
    @bolinfest: realpath является частью GNU coreutils с января 2012 г. / v8.15. Если вы используете недавний дистрибутив Linux, который включает в себя coreutils (который должен быть в основном всем) и отсутствует realpath , упаковщики наверняка постарались разделить realpath в другой пакет. (текущая версия coreutils - 8.21, выпущенная 2013-02-04. Версия, которую я использую (для Arch Linux), выглядит как эта, 8.21, и пакет включает в себя realpath , как и любой правильный пакет;)
  • 9
    Использование realpath и даже короче: SCRIPT_PATH=$(dirname $(realpath -s $0))
Показать ещё 8 комментариев
123

Самый простой способ, которым я нашел полный канонический путь в bash, - использовать cd и pwd:

ABSOLUTE_PATH="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/$(basename "${BASH_SOURCE[0]}")"

Использование ${BASH_SOURCE[0]} вместо $0 создает такое же поведение независимо от того, вызывается ли script как <name> или source <name>

  • 3
    Это не работает, если изначально указанный файл является символической ссылкой. Я думаю, что вам нужно использовать что-то вроде readlink (или ls) в цикле, чтобы убедиться, что вы нашли окончательный файл, не являющийся символической ссылкой. Я искал что-то более лаконичное, но в любом случае вы можете найти последнюю версию решения, которое я использовал в базе кода Android, в dalvik/dx/etc/dx .
  • 0
    @danfuzz см. комментарий Денниса Уильямсона относительно использования -P для pwd. Это должно делать то, что вы хотите.
Показать ещё 5 комментариев
33

Мне просто пришлось пересмотреть этот вопрос сегодня и нашел https://stackoverflow.com/questions/59895/get-the-source-directory-of-a-bash-script-from-within-the-script-itself. Он описывает решение, которое Я использовал и в прошлом.

DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"

Там больше вариантов при связанном ответе, например. для случая, когда сам script является символической ссылкой.

28

Получить абсолютный путь оболочки script

Не использует параметр -f в readlink, поэтому должен работать в bsd/mac-osx

Поддержка

  • source./ script (Когда вызывается оператором . dot)
  • Абсолютный путь/путь/в/script
  • Относительный путь, как. /script
  • /path/dir1/../dir2/dir3/../script
  • При вызове из символической ссылки
  • Когда символическая ссылка вложенна, например) foo->dir1/dir2/bar bar->./../doe doe->script
  • Когда вызывающий абонент меняет имя скрипта

Я ищу угловые случаи, когда этот код не работает. Пожалуйста, дайте мне знать.

код

pushd . > /dev/null
SCRIPT_PATH="${BASH_SOURCE[0]}";
while([ -h "${SCRIPT_PATH}" ]); do
    cd "`dirname "${SCRIPT_PATH}"`"
    SCRIPT_PATH="$(readlink "`basename "${SCRIPT_PATH}"`")";
done
cd "`dirname "${SCRIPT_PATH}"`" > /dev/null
SCRIPT_PATH="`pwd`";
popd  > /dev/null
echo "srcipt=[${SCRIPT_PATH}]"
echo "pwd   =[`pwd`]"

Известный issuse

Script должен быть на диске где-то, пусть он будет по сети. Если вы попытаетесь запустить этот script из PIPE, он не будет работать

wget -o /dev/null -O - http://host.domain/dir/script.sh |bash

С технической точки зрения, это undefined.
Практически говоря, нет разумного способа обнаружить это. (совлокальный процесс не может получить доступ к env родителя)

  • 2
    Как указано в другом месте, и я думаю, что это не совсем крайний случай, но readlink -f не является стандартным параметром и очень хорошо не доступен, например, на моем BSD.
  • 0
    Я столкнулся с угловым случаем, где это не сработало. У меня был скрипт в ~/scripts/myscript и символическая ссылка в ~/bin/myscript которая указывала на ../scripts/myscript . Запуск ~/bin/myscript из ~/ заставил его думать, что местоположение скрипта было ~/ . Решение здесь работало нормально, которое выглядит очень похоже на ваше решение
21

Как насчет использования:

SCRIPT_PATH=$(dirname `which $0`)

which выводит на печать полный путь исполняемого файла, который был бы выполнен, когда переданный аргумент был введен в приглашении оболочки (в котором содержится $0)

dirname разделяет суффиксы без каталогов из имени файла

Следовательно, в итоге вы получаете полный путь к script, независимо от того, был ли указан путь или нет.

  • 1
    Просто, и это работает для меня. Хорошее решение.
  • 6
    dirname ./myscript возвращает . , Это может быть не то, что вы хотите.
Показать ещё 4 комментария
19

Поскольку по умолчанию для моей Linux-системы не установлен realpath, для меня работает следующее:

SCRIPT="$(readlink --canonicalize-existing "$0")"
SCRIPTPATH="$(dirname "$SCRIPT")"

$SCRIPT будет содержать путь к реальному пути к script и $SCRIPTPATH реальному пути к каталогу, содержащему script.

Прежде чем использовать это, прочитайте комментарии этого ответа.

  • 3
    ОП уже отметил это решение и проигнорировал его за то, что он не является POSIX - но, по крайней мере, это хорошо и аккуратно для систем на основе GNU. Ключевой особенностью этого является то, что он разрешает символические ссылки .
9

Ответ на этот вопрос очень поздно, но я использую:

SCRIPT=$( readlink -m $( type -p $0 ))      # Full path to script
BASE_DIR=`dirname ${SCRIPT}`                # Directory script is run in
NAME=`basename ${SCRIPT}`                   # Actual name of script even if linked
  • 0
    Полный путь - это именно то, что я искал. Компактный, эффективный, надежный. Отлично сработано!
  • 0
    Не все реализации readlink имеют -m Я полагаю, что OP ищет решение, которое не зависит от расширенной функциональности GNU readlink.
5

Мы разместили наш собственный продукт realpath-lib в GitHub для бесплатного и свободного использования сообщества.

Бесстыдный плагин, но с этой библиотекой Bash вы можете:

get_realpath <absolute|relative|symlink|local file>

Эта функция является ядром библиотеки:

function get_realpath() {

if [[ -f "$1" ]]
then 
    # file *must* exist
    if cd "$(echo "${1%/*}")" &>/dev/null
    then 
        # file *may* not be local
        # exception is ./file.ext
        # try 'cd .; cd -;' *works!*
        local tmppwd="$PWD"
        cd - &>/dev/null
    else 
        # file *must* be local
        local tmppwd="$PWD"
    fi
else 
    # file *cannot* exist
    return 1 # failure
fi

# reassemble realpath
echo "$tmppwd"/"${1##*/}"
return 0 # success

}

Он не требует каких-либо внешних зависимостей, просто Bash 4+. Также содержит функции get_dirname, get_filename, get_stemname и validate_path validate_realpath. Он бесплатный, чистый, прост и хорошо документирован, поэтому его также можно использовать в учебных целях, и, без сомнения, его можно улучшить. Попробуйте его на разных платформах.

Обновление. После некоторого пересмотра и тестирования мы заменили указанную выше функцию тем, что достигает одного и того же результата (без использования dirname, только pure Bash), но с большей эффективностью:

function get_realpath() {

    [[ ! -f "$1" ]] && return 1 # failure : file does not exist.
    [[ -n "$no_symlinks" ]] && local pwdp='pwd -P' || local pwdp='pwd' # do symlinks.
    echo "$( cd "$( echo "${1%/*}" )" 2>/dev/null; $pwdp )"/"${1##*/}" # echo result.
    return 0 # success

}

Это также включает настройку среды no_symlinks, которая предоставляет возможность разрешать символические ссылки на физическую систему. По умолчанию он сохраняет неизменяемые символические ссылки.

  • 0
    Спасибо, Бен. Новичок на сайте (как участник), и я изменил запись по мере необходимости. Я не думал, что здесь есть какие-либо коммерческие проблемы, так как мы раздаем код без ограничений. Мы также считаем его хорошим инструментом обучения и тщательно документировали его для этой цели.
  • 0
    Спасибо @Asym; очень признателен.
Показать ещё 2 комментария
4

sh совместимый способ:

SCRIPT_HOME=`dirname $0 | while read a; do cd $a && pwd && break; done`
  • 0
    Зачем нам нужно while read a части? Почему бы просто не cd $(dirname $a) && pwd ?
  • 2
    Потому что $( ... ) не будет работать в оболочке без bash. Я видел плохой скрипт, который использует выражение $( ... ) и начинает #!/bin/sh Bin #!/bin/sh много раз. Я рекомендую написать #!/bin/bash в начале или прекратить использование выражения $( ... ) . Этот пример для 1-й рекомендации.
Показать ещё 1 комментарий
3

Вы можете попытаться определить следующую переменную:

CWD="$(cd -P -- "$(dirname -- "$0")" && pwd -P)"

или вы можете попробовать следующую функцию в bash:

realpath () {
  [[ $1 = /* ]] && echo "$1" || echo "$PWD/${1#./}"
}

Эта функция принимает 1 аргумент. Если аргумент уже имеет абсолютный путь, напечатайте его как есть, иначе напечатайте $PWD variable + имя файла (без префикса ./).

по теме:

  • 0
    Ни один из ваших методов не может следовать рекурсивным символическим ссылкам
2

Рассмотрение этой проблемы еще раз: есть очень популярное решение, которое упоминается в этом потоке, имеющем начало здесь:

DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"

Я остался в стороне от этого решения из-за использования имени dirname - он может представлять кросс-платформенные трудности, особенно если script необходимо заблокировать по соображениям безопасности. Но как чистая альтернатива Bash, как насчет использования:

DIR="$( cd "$( echo "${BASH_SOURCE[0]%/*}" )" && pwd )"

Будет ли это вариант?

  • 0
    BASH_SOURCE, по-видимому, необходим для запуска сценария с использованием PATH=/path/to/your/script:$PATH yourscript.sh . К сожалению, это нужно Баш
1

Легко читается? альтернатива. Игнорирует символические ссылки

#!/bin/bash
currentDir=$(
  cd $(dirname "$0")
  pwd
) 

echo -n "current "
pwd
echo script $currentDir 
  • 0
    Я получаю bash: currentDir: command not found
  • 1
    Это разрешит только символическую ссылку на каталоги. Не удастся разрешить символическую ссылку для пути. Рассмотрим, когда $ 0 - это ссылка на скрипт в том же каталоге.
1

Попробуйте следующее:

cd $(dirname $([ -L $0 ] && readlink -f $0 || echo $0))
  • 0
    readlink не работает таким образом в BSD (и, следовательно, в OS X), и вопрос касался альтернативы.
  • 0
    В Linux CentOS 7 это прекрасно работает, когда скрипт выполняется с полным путем ( /home/me/script.sh ), но возвращает только . когда скрипт выполняется с помощью sh script.sh или ./script.sh
1

Если мы используем Bash, я считаю, что это самый удобный способ, поскольку он не требует вызовов для любых внешних команд:

THIS_PATH="${BASH_SOURCE[0]}";
THIS_DIR=$(dirname $THIS_PATH)
  • 0
    $(dirname вызывает dirname (1).
1

просто: BASEDIR=$(readlink -f $0 | xargs dirname)

нет фантастических операторов

Е.И.В..

1

Принятое решение имеет неудобное (для меня) не "исходное":
если вы называете это "source ../../yourScript", $0 будет "bash"!

Следующая функция (для bash >= 3.0) дает мне правильный путь, однако script может быть вызван (напрямую или через source с абсолютным или относительным путем):
(по "правильному пути", я имею в виду полный абсолютный путь script, который называется, даже при вызове с другого пути, напрямую или с помощью "source" )

#!/bin/bash
echo $0 executed

function bashscriptpath() {
  local _sp=$1
  local ascript="$0"
  local asp="$(dirname $0)"
  #echo "b1 asp '$asp', b1 ascript '$ascript'"
  if [[ "$asp" == "." && "$ascript" != "bash" && "$ascript" != "./.bashrc" ]] ; then asp="${BASH_SOURCE[0]%/*}"
  elif [[ "$asp" == "." && "$ascript" == "./.bashrc" ]] ; then asp=$(pwd)
  else
    if [[ "$ascript" == "bash" ]] ; then
      ascript=${BASH_SOURCE[0]}
      asp="$(dirname $ascript)"
    fi  
    #echo "b2 asp '$asp', b2 ascript '$ascript'"
    if [[ "${ascript#/}" != "$ascript" ]]; then asp=$asp ;
    elif [[ "${ascript#../}" != "$ascript" ]]; then
      asp=$(pwd)
      while [[ "${ascript#../}" != "$ascript" ]]; do
        asp=${asp%/*}
        ascript=${ascript#../}
      done
    elif [[ "${ascript#*/}" != "$ascript" ]];  then
      if [[ "$asp" == "." ]] ; then asp=$(pwd) ; else asp="$(pwd)/${asp}"; fi
    fi  
  fi  
  eval $_sp="'$asp'"
}

bashscriptpath H
export H=${H}

Ключ должен обнаружить случай "source" и использовать ${BASH_SOURCE[0]}, чтобы вернуть фактический script.

1

Возможно, приемлемый ответ на следующий вопрос может помочь.

Как я могу получить поведение readlink -f GNU на Mac?

Учитывая, что вы просто хотите канонизировать имя, которое вы получаете от конкатенации $PWD и $0 (если предположить, что $0 не является абсолютным для начала) Просто используйте серию регулярных выражений вдоль строки abs_dir=${abs_dir//\/.\//\/} и т.д.

Да, я знаю, что это выглядит ужасно, но он будет работать и будет чистым bash.

  • 0
    Благодарю. Что касается связанного вопроса, то он все еще зависит от изменения каталога и использования pwd , что я считаю неуклюжим в своем решении. Регулярное выражение интересно. Я не могу не беспокоиться о крайних случаях, хотя.
  • 0
    Да, для этого потребуется хорошее тестирование, но, насколько я знаю, не существует портативного одноразового решения, которое канонизировало бы имя для вас.
Показать ещё 1 комментарий
0

Еще один способ сделать это:

shopt -s extglob

selfpath=$0
selfdir=${selfpath%%+([!/])}

while [[ -L "$selfpath" ]];do
  selfpath=$(readlink "$selfpath")
  if [[ ! "$selfpath" =~ ^/ ]];then
    selfpath=${selfdir}${selfpath}
  fi
  selfdir=${selfpath%%+([!/])}
done

echo $selfpath $selfdir
0

Я использовал следующий подход успешно некоторое время (не на OSX, хотя), и он использует только встроенную оболочку и обрабатывает случай "source foobar.sh", насколько я видел.

Одной из проблем с приведенным ниже примером кода (hastly together) является то, что функция использует $PWD, которая может быть или не быть правильной во время вызова функции. Таким образом, это должно быть обработано.

#!/bin/bash

function canonical_path() {
  # Handle realtive vs absolute path
  [ ${1:0:1} == '/' ] && x=$1 || x=$PWD/$1
  # Change to dirname of x
  cd ${x%/*}
  # Combine new pwd with basename of x
  echo $(pwd -P)/${x##*/}
  cd $OLDPWD
}

echo $(canonical_path "${BASH_SOURCE[0]}")

type [
type cd
type echo
type pwd
0

Просто, черт возьми, я немного взломал на script, который делает вещи чисто текстовыми, чисто в bash. Надеюсь, я поймал все крайние случаи. Обратите внимание, что ${var//pat/repl}, о котором я упоминал в другом ответе, не работает, поскольку вы не можете заставить его заменять только кратчайшее совпадение, что является проблемой для замены /foo/../, например. /*/../ возьмет все перед собой, а не только одну запись. И поскольку эти шаблоны не являются действительно регулярными выражениями, я не вижу, как это можно сделать для работы. Итак, вот красиво запутанное решение, с которым я придумал, наслаждаюсь.;)

Кстати, дайте мне знать, если вы найдете какие-либо необработанные случаи ребер.

#!/bin/bash

canonicalize_path() {
  local path="$1"
  OIFS="$IFS"
  IFS=$'/'
  read -a parts < <(echo "$path")
  IFS="$OIFS"

  local i=${#parts[@]}
  local j=0
  local back=0
  local -a rev_canon
  while (($i > 0)); do
    ((i--))
    case "${parts[$i]}" in
      ""|.) ;;
      ..) ((back++));;
      *) if (($back > 0)); then
           ((back--))
         else
           rev_canon[j]="${parts[$i]}"
           ((j++))
         fi;;
    esac
  done
  while (($j > 0)); do
    ((j--))
    echo -n "/${rev_canon[$j]}"
  done
  echo
}

canonicalize_path "/.././..////../foo/./bar//foo/bar/.././bar/../foo/bar/./../..//../foo///bar/"
-1

Один вкладыш

`dirname $(realpath $0)`
  • 0
    Это, кажется, работает для меня, я думаю, что это могло быть опущено из-за отсутствия каких-либо дополнительных комментариев, объясняющих, как это работает.
  • 0
    FWIW Мне нравится краткость этого решения, и оно работает из коробки на Ubuntu 16.04 LTS.
Показать ещё 1 комментарий
-4

Проще говоря, это то, что работает для меня:

MY_DIR=`dirname $0`
source $MY_DIR/_inc_db.sh

Ещё вопросы

Сообщество Overcoder
Наверх
Меню