Unix shell script выяснит, в каком каталоге находится файл скрипта?

511

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

Уильям Юнг
источник
12
Это действительно дубликат? Этот вопрос о "сценарии оболочки Unix", другой - о Bash.
Майкл
2
@BoltClock: Этот вопрос был неправильно закрыт. Связанный вопрос о Bash. Этот вопрос касается программирования оболочки Unix. Обратите внимание, что принятые ответы совершенно разные!
Дитрих Эпп
@Dietrich Epp: Ты прав. Похоже, что ответчик выбрал принятый ответ и добавление тега [bash] (вероятно, в ответ на это) привело меня к тому, что я пометил вопрос как дубликат в ответ на флаг.
BoltClock
2
Я думаю, что этот ответ лучше: получить исходный каталог скрипта Bash изнутри
Алан Чжилян Фэн

Ответы:

568

В Bash вы должны получить то, что вам нужно, вот так:

#!/usr/bin/env bash

BASEDIR=$(dirname "$0")
echo "$BASEDIR"
TheMarko
источник
17
Это не сработает, если вы вызвали скрипт по символической ссылке в другом каталоге. Чтобы сделать эту работу, вы должны использовать readlinkтакже (см. Ответ
Ала
44
В Баше безопаснее использовать $BASH_SOURCEвместо $0, потому что $0не всегда содержит путь к скрипту вызываются, например, когда «поиск» сценария.
mklement0
2
$BASH_SOURCEспецифичен для Bash, вопрос в целом о скрипте оболочки.
Ха-Дуонг Нгуен
7
@auraham: CUR_PATH=$(pwd)или pwdверните текущий каталог (который не обязательно должен быть родительским каталогом сценариев)!
Андреас Дитрих
5
Я попробовал метод @ mklement0, рекомендованный, используя $BASH_SOURCE, и он возвращает то, что мне нужно. Мой скрипт вызывается из другого скрипта и $0возвращает, .пока $BASH_SOURCEвозвращает правильный подкаталог (в моем случае scripts).
Дэвид Риссато Круз
402

Оригинальный пост содержит решение (игнорируйте ответы, они не добавляют ничего полезного). Интересную работу выполняет упомянутая команда unix readlinkс опцией -f. Работает, когда скрипт вызывается как по абсолютному, так и по относительному пути.

Для bash, sh, ksh:

#!/bin/bash 
# Absolute path to this script, e.g. /home/user/bin/foo.sh
SCRIPT=$(readlink -f "$0")
# Absolute path this script is in, thus /home/user/bin
SCRIPTPATH=$(dirname "$SCRIPT")
echo $SCRIPTPATH

Для tcsh, csh:

#!/bin/tcsh
# Absolute path to this script, e.g. /home/user/bin/foo.csh
set SCRIPT=`readlink -f "$0"`
# Absolute path this script is in, thus /home/user/bin
set SCRIPTPATH=`dirname "$SCRIPT"`
echo $SCRIPTPATH

Смотрите также: https://stackoverflow.com/a/246128/59087

и др.
источник
12
Примечание: не все системы имеют readlink. Вот почему я рекомендовал использовать pushd / popd (встроенные в bash).
docwhat
23
-fВариант readlinkделает что - то другое на OS X (Lion) и , возможно , BSD. stackoverflow.com/questions/1055671/…
Эргвун
9
Чтобы уточнить комментарий @ Ergwun: -fвообще не поддерживается в OS X (с Lion); там вы можете либо отказаться от -fразрешения не более одного уровня косвенного обращения, например pushd "$(dirname "$(readlink "$BASH_SOURCE" || echo "$BASH_SOURCE")")", либо вы можете запустить свой собственный рекурсивный скрипт, следующий за символической ссылкой, как показано в связанном посте.
mklement0
2
Я до сих пор не понимаю, зачем ОП нужен абсолютный путь. Составление отчетов "." должно работать нормально, если вы хотите получить доступ к файлам, относящимся к пути сценариев, и вы назвали сценарий как ./myscript.sh
Стефан Хаберл
10
@ StefanHaberl Я думаю, что это будет проблемой, если вы запустите скрипт, когда ваш текущий рабочий каталог будет отличаться от местоположения скрипта (например sh /some/other/directory/script.sh), в данном случае .это будет ваш pwd, а не/some/other/directory
Jon z
51

Более ранний комментарий к ответу сказал это, но это легко пропустить среди всех других ответов.

При использовании bash:

echo this file: "$BASH_SOURCE"
echo this dir: "$(dirname "$BASH_SOURCE")"

Справочное руководство Bash, 5.2 Переменные Bash

Даниил
источник
3
Только этот работает со сценарием в пути к среде, большинство проголосовавших не работают. благодарю вас!
июля
Вы должны использовать dirname "$BASH_SOURCE"вместо этого для обработки пробелов в $ BASH_SOURCE.
Mygod
1
Более точный способ печати каталога, согласно инструменту ShellCheck, будет выглядеть следующим образом: "$ (dirname" $ ​​{BASH_SOURCE {0}} ")", потому что BASH_SOURCE является массивом, и без нижнего индекса первый элемент берется по умолчанию ,
Адриан М.
1
@AdrianM. Вы хотите скобки, а не фигурные скобки, для индекса:"$(dirname "${BASH_SOURCE[0]}")"
Hashbrown
Плохо для меня .. печатает только точку (т.е. текущий каталог, предположительно)
JL_SO
40

Предполагая, что вы используете Bash

#!/bin/bash

current_dir=$(pwd)
script_dir=$(dirname $0)

echo $current_dir
echo $script_dir

Этот скрипт должен напечатать каталог, в котором вы находитесь, а затем каталог, в котором находится скрипт. Например, при вызове его /с помощью скрипта /home/mez/он выводит

/
/home/mez

Помните, что при назначении переменных из выходных данных команды оберните команду $(и )- или вы не получите желаемого результата.

Mez
источник
3
Это не будет работать, когда я вызываю скрипт из текущего каталога.
Эрик Ван
1
@EricWang вы всегда в текущем каталоге.
Ctrl-Alt-Delor
Для меня $ current_dir - это путь, с которого я вызываю скрипт. Однако $ script_dir - это не директория скрипта, а просто точка.
Майкл
31

Если вы используете Bash ....

#!/bin/bash

pushd $(dirname "${0}") > /dev/null
basedir=$(pwd -L)
# Use "pwd -P" for the path without links. man bash for more info.
popd > /dev/null

echo "${basedir}"
docwhat
источник
4
Вы можете заменить pushd/ popdна cd $(dirname "${0}")и cd -заставить его работать на других оболочках, если они есть pwd -L.
docwhat
почему вы используете здесь pushd и popd?
qodeninja
1
Поэтому мне не нужно хранить исходный каталог в переменной. Это шаблон, который я часто использую в функциях и тому подобное. Он очень хорошо гнездится, и это хорошо.
docwhat
Он все еще хранится в памяти - в переменной - независимо от того, на какую переменную ссылается ваш скрипт или нет. Кроме того, я считаю, что затраты на выполнение pushd и popd намного перевешивают экономию от отсутствия локальной переменной Bash в вашем скрипте, как в циклах ЦП, так и в удобочитаемости.
здесь
20

Как Марко предлагает:

BASEDIR=$(dirname $0)
echo $BASEDIR

Это работает, если вы не выполните скрипт из той же директории, где находится скрипт, и в этом случае вы получите значение '.'

Чтобы обойти эту проблему, используйте:

current_dir=$(pwd)
script_dir=$(dirname $0)

if [ $script_dir = '.' ]
then
script_dir="$current_dir"
fi

Теперь вы можете использовать переменную current_dir в вашем скрипте для ссылки на каталог скриптов. Однако это может все еще иметь проблему символической ссылки.

ranamalo
источник
20

Лучший ответ на этот вопрос был дан здесь:
Получение исходного каталога скрипта Bash изнутри

И это:

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

Одна строка, которая даст вам полное имя каталога скрипта независимо от того, откуда он вызывается.

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

#!/bin/bash

SOURCE="${BASH_SOURCE[0]}"
while [ -h "$SOURCE" ]; do # resolve $SOURCE until the file is no longer a symlink
  TARGET="$(readlink "$SOURCE")"
  if [[ $TARGET == /* ]]; then
    echo "SOURCE '$SOURCE' is an absolute symlink to '$TARGET'"
    SOURCE="$TARGET"
  else
    DIR="$( dirname "$SOURCE" )"
    echo "SOURCE '$SOURCE' is a relative symlink to '$TARGET' (relative to '$DIR')"
    SOURCE="$DIR/$TARGET" # if $SOURCE was a relative symlink, we need to resolve it relative to the path where the symlink file was located
  fi
done
echo "SOURCE is '$SOURCE'"
RDIR="$( dirname "$SOURCE" )"
DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )"
if [ "$DIR" != "$RDIR" ]; then
  echo "DIR '$RDIR' resolves to '$DIR'"
fi
echo "DIR is '$DIR'"
Александро де Оливейра
источник
10

Давайте сделаем это POSIX oneliner:

a="/$0"; a=${a%/*}; a=${a#/}; a=${a:-.}; BASEDIR=$(cd "$a"; pwd)

Протестировано на многих Bourne-совместимых оболочках, включая BSD.

Насколько я знаю, я являюсь автором и выложил его в общественное достояние. Для получения дополнительной информации см .: https://www.jasan.tk/posts/2017-05-11-posix_shell_dirname_replacement/

Ян Шареник
источник
1
как написано, cd: too many argumentsесли пробелы в пути, и возвращает $PWD. (очевидное исправление, но показывает, сколько на самом деле существует крайних случаев)
Майкл,
1
Был бы upvote. За исключением комментария от @michael, что он терпит неудачу с пробелами в пути ... Есть ли решение для этого?
spechter
1
@spechter да, есть решение для этого. Смотрите обновленный jasan.tk/posix/2017/05/11/posix_shell_dirname_replacement
Ян Шареник
@Der_Meister - пожалуйста, будьте более конкретны. Или напишите (и, возможно, зашифруйте) электронное письмо Джасану по адресу jasan.tk
Ян Шареник
2
ln -s / home / der / 1 / test / home / der / 2 / test && / home / der / 2 / test => / home / der / 2 (вместо этого следует указать путь к исходному сценарию)
Der_Meister
10
BASE_DIR="$(cd "$(dirname "$0")"; pwd)";
echo "BASE_DIR => $BASE_DIR"
searchrome
источник
3
самый надежный способ, не связанный с bash, который я знаю.
Eddygeek
10

Если вы хотите получить фактический каталог скриптов (независимо от того, вызываете ли вы скрипт с использованием символической ссылки или напрямую), попробуйте:

BASEDIR=$(dirname $(realpath "$0"))
echo "$BASEDIR"

Это работает как на Linux, так и на MacOS. Я не мог видеть никого здесь упоминание оrealpath . Не уверен, есть ли какие-либо недостатки в этом подходе.

на MacOS, вам нужно установить, coreutilsчтобы использовать realpath. Например: brew install coreutils.

Rohith
источник
6

ВВЕДЕНИЕ

Этот ответ исправляет очень испорченный, но шокирующе лучший голос этой ветки (написанный TheMarko):

#!/usr/bin/env bash

BASEDIR=$(dirname "$0")
echo "$BASEDIR"

ПОЧЕМУ ИСПОЛЬЗУЕТСЯ dirname "$ 0" НА ЭТОМ СОБСТВЕННОМ НЕ РАБОТАЕТ?

имя $ 0 будет работать только в том случае, если пользователь запускает скрипт очень специфическим образом. Мне удалось найти несколько ситуаций, когда этот ответ не удается и происходит сбой сценария.

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

dirname "$0"

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

/some/path/./script argument1 argument2

$ 0 = "/ некоторые / путь /./ сценарий"

dirname в основном находит последний / в строке и обрезает его там. Так что если вы делаете:

  dirname /usr/bin/sha256sum

вы получите: / usr / bin

Этот пример хорошо работает, потому что / usr / bin / sha256sum - это правильно отформатированный путь, но

  dirname "/some/path/./script"

не будет работать хорошо и даст вам:

  BASENAME="/some/path/." #which would crash your script if you try to use it as a path

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

./script   

$ 0 в этой ситуации будет ./script и dirname $ 0 даст:

. #or BASEDIR=".", again this will crash your script

С помощью:

sh script

Без ввода полного пути также получим BASEDIR = "."

Использование относительных каталогов:

 ../some/path/./script

Дает имя $ 0 из:

 ../some/path/.

Если вы находитесь в каталоге / some и вызываете скрипт таким образом (обратите внимание на отсутствие / в начале, снова относительный путь):

 path/./script.sh

Вы получите это значение для dirname $ 0:

 path/. 

и ./path/./script (другая форма относительного пути) дает:

 ./path/.

Единственные две ситуации, когда basedir $ 0 будет работать, - это если пользователь использует sh или touch для запуска скрипта, потому что оба приведут к $ 0:

 $0=/some/path/script

который даст вам путь, который вы можете использовать с dirname.

РЕШЕНИЕ

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

#!/bin/bash
#this script will only work in bash, make sure it's installed on your system.

#set to false to not see all the echos
debug=true

if [ "$debug" = true ]; then echo "\$0=$0";fi


#The line below detect script's parent directory. $0 is the part of the launch command that doesn't contain the arguments
BASEDIR=$(dirname "$0") #3 situations will cause dirname $0 to fail: #situation1: user launches script while in script dir ( $0=./script)
                                                                     #situation2: different dir but ./ is used to launch script (ex. $0=/path_to/./script)
                                                                     #situation3: different dir but relative path used to launch script
if [ "$debug" = true ]; then echo 'BASEDIR=$(dirname "$0") gives: '"$BASEDIR";fi                                 

if [ "$BASEDIR" = "." ]; then BASEDIR="$(pwd)";fi # fix for situation1

_B2=${BASEDIR:$((${#BASEDIR}-2))}; B_=${BASEDIR::1}; B_2=${BASEDIR::2}; B_3=${BASEDIR::3} # <- bash only
if [ "$_B2" = "/." ]; then BASEDIR=${BASEDIR::$((${#BASEDIR}-1))};fi #fix for situation2 # <- bash only
if [ "$B_" != "/" ]; then  #fix for situation3 #<- bash only
        if [ "$B_2" = "./" ]; then
                #covers ./relative_path/(./)script
                if [ "$(pwd)" != "/" ]; then BASEDIR="$(pwd)/${BASEDIR:2}"; else BASEDIR="/${BASEDIR:2}";fi
        else
                #covers relative_path/(./)script and ../relative_path/(./)script, using ../relative_path fails if current path is a symbolic link
                if [ "$(pwd)" != "/" ]; then BASEDIR="$(pwd)/$BASEDIR"; else BASEDIR="/$BASEDIR";fi
        fi
fi

if [ "$debug" = true ]; then echo "fixed BASEDIR=$BASEDIR";fi
thebunnyrules
источник
4

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

dir=$(dirname $(test -L "$BASH_SOURCE" && readlink -f "$BASH_SOURCE" || echo "$BASH_SOURCE"))

Кстати, я полагаю, вы используете / bin / bash .

Ричард Гомес
источник
2

Так много ответов, все правдоподобно, каждый с за и против и слегка отличающимися целями (которые, вероятно, должны быть указаны для каждого). Вот еще одно решение, которое отвечает основной задаче: быть понятным и работать во всех системах, на всех bash (без предположений о версиях bash readlinkилиpwd опциях ) и разумно делать то, что вы ожидаете (например, разрешение символических ссылок - это интересная проблема, но обычно это не то, что вам действительно нужно), обрабатывать крайние случаи, такие как пробелы в путях и т. д., игнорирует любые ошибки и использует вменяемое значение по умолчанию, если есть какие-либо проблемы.

Каждый компонент хранится в отдельной переменной, которую вы можете использовать индивидуально:

# script path, filename, directory
PROG_PATH=${BASH_SOURCE[0]}      # this script's name
PROG_NAME=${PROG_PATH##*/}       # basename of script (strip path)
PROG_DIR="$(cd "$(dirname "${PROG_PATH:-$PWD}")" 2>/dev/null 1>&2 && pwd)"
Майкл
источник
-4

Это должно делать свое дело:

echo `pwd`/`dirname $0`

Это может выглядеть уродливо в зависимости от того, как он был вызван и cwd, но должно привести вас туда, куда вам нужно (или вы можете настроить строку, если вам все равно, как она выглядит).

kenorb
источник
1
Здесь проблема с выходом из стека overoverflow: она, безусловно, должна выглядеть следующим образом: `pwd`/`dirname $0`но она все равно может потерпеть неудачу в символических ссылках
Андреас Дитрих