Как лучше включить другие скрипты?

353

Обычно вы включаете скрипт с помощью «source»

например:

main.sh:

#!/bin/bash

source incl.sh

echo "The main script"

incl.sh:

echo "The included script"

Результат выполнения "./main.sh":

The included script
The main script

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

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

Аарон Х.
источник
2
смотрите это: stackoverflow.com/questions/59895/…
Lluís
1
Ваш вопрос настолько хорош и информативен, что на мой вопрос ответили еще до того, как вы задали свой вопрос! Хорошая работа!
Габриэль Стейплс

Ответы:

229

Я стараюсь, чтобы все мои сценарии были связаны друг с другом. Таким образом, я могу использовать dirname:

#!/bin/sh

my_dir="$(dirname "$0")"

"$my_dir/other_script.sh"
Крис Боран
источник
5
Это не будет работать, если скрипт выполняется через $ PATH. тогда which $0будет полезно
Хьюго
41
Не существует надежного способа определения местоположения сценария оболочки, см. Mywiki.wooledge.org/BashFAQ/028
Филипп,
12
@ Филипп, автор этой записи правильный, он сложный, и есть ошибки. Но ему не хватает некоторых ключевых моментов, во-первых, автор предполагает много вещей о том, что вы собираетесь делать со своим скриптом bash. Я бы не ожидал, что скрипт на python будет работать без его зависимостей. Bash - это клейкий язык, который позволяет вам быстро делать вещи, которые в противном случае были бы сложными. Когда вам нужно, чтобы ваша система сборки работала, побеждает прагматизм (и хорошее предупреждение о том, что скрипт не может найти зависимости).
Аарон Х.
11
Только что узнал о BASH_SOURCEмассиве и о том, как первый элемент в этом массиве всегда указывает на текущий источник.
haridsv
6
Это не будет работать, если скрипты находятся в разных местах. Например, /home/me/main.sh вызывает /home/me/test/inc.sh, так как dirname вернет / home / me. SACI ответ с использованием BASH_SOURCE является лучшим решением stackoverflow.com/a/12694189/1000011
opticyclic
187

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

DIR="${BASH_SOURCE%/*}"
if [[ ! -d "$DIR" ]]; then DIR="$PWD"; fi
. "$DIR/incl.sh"
. "$DIR/main.sh"

.(точка) - это псевдоним source, $PWDпуть к рабочему каталогу , BASH_SOURCEпеременная массива, членами которой являются исходные имена файлов, ${string%substring}удаляет кратчайшее совпадение $ substring из задней части $ string

SacII
источник
7
Это единственный ответ в теме, который последовательно работал для меня
Джастин
3
@sacii Могу я узнать, когда if [[ ! -d "$DIR" ]]; then DIR="$PWD"; fiнужна линия ? Я могу найти в этом необходимость, если команды будут вставлены в приглашение bash для запуска. Тем не менее, если он работает в контексте файла скрипта, я не вижу в этом необходимости ...
Джонни Вонг
2
Также стоит отметить, что он работает, как и ожидалось, для нескольких sources (я имею в виду, если вы создаете sourceскрипт, который находится sourceв другом каталоге и т. Д., Он все еще работает).
Чиро Коста
4
Разве это не должно быть так, ${BASH_SOURCE[0]}как вы хотите только последний звонок? Также использование DIR=$(dirname ${BASH_SOURCE[0]})позволит вам избавиться от if-условия
кшеной
1
Готовы ли вы сделать этот фрагмент доступным под лицензией без атрибутов, такой как CC0, или просто опубликовать его в открытом доступе? Я хотел бы использовать этот дословно, но это раздражает, чтобы поставить атрибуцию в верхней части каждого сценария!
BeeOnRope
52

Альтернатива:

scriptPath=$(dirname $0)

является:

scriptPath=${0%/*}

.. преимущество заключается в отсутствии зависимости от dirname, которое не является встроенной командой (и не всегда доступно в эмуляторах)

tardate
источник
2
basePath=$(dirname $0)дал мне пустое значение, когда содержащий файл сценария получен.
prayagupd
41

Если он находится в том же каталоге, вы можете использовать dirname $0:

#!/bin/bash

source $(dirname $0)/incl.sh

echo "The main script"
ДСМ
источник
2
Две подводные камни: 1) $0является ./t.shи Dirname возвращается .; 2) после cd binвернул .не правильно. $BASH_SOURCEне лучше
18446744073709551615
другая ловушка: попробуйте это с пробелами в имени каталога.
Hubert Grzeskowiak
source "$(dirname $0)/incl.sh"работает в тех случаях
ДСМ
27

Я думаю, что лучший способ сделать это - использовать метод Криса Борана, НО вы должны вычислять MY_DIR следующим образом:

#!/bin/sh
MY_DIR=$(dirname $(readlink -f $0))
$MY_DIR/other_script.sh

Чтобы процитировать man-страницы для readlink:

readlink - display value of a symbolic link

...

  -f, --canonicalize
        canonicalize  by following every symlink in every component of the given 
        name recursively; all but the last component must exist

Я никогда не сталкивался с случаем использования, где MY_DIRнеправильно вычисляется. Если вы обращаетесь к своему сценарию через символическую ссылку, $PATHон работает.

Mat131
источник
Хорошее и простое решение, и отлично работает для меня во всех возможных вариантах вызова скрипта. Спасибо.
Брайан Клайн
Помимо проблем с отсутствующими кавычками, существует ли какой-либо фактический вариант использования, в котором вы хотели бы разрешить символические ссылки, а не использовать $0напрямую?
10
1
@ l0b0: представьте, что ваш сценарий такой. /home/you/script.shВы можете cd /homeзапустить свой сценарий оттуда, так как ./you/script.shв этом случае dirname $0он вернется, ./youа другой сценарий завершится неудачей
dr.scre
1
Отличное предложение, однако, мне нужно было сделать следующее, чтобы он прочитал мои переменные в ` MY_DIR=$(dirname $(readlink -f $0)); source $MY_DIR/incl.sh
Фредерик Олингер,
21

Комбинация ответов на этот вопрос обеспечивает наиболее надежное решение.

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

#! / Bin / Баш

# Полный путь текущего скрипта
THIS = `readlink -f" $ {BASH_SOURCE [0]} "2> / dev / null || echo $ 0`

# Каталог, в котором находится текущий скрипт
DIR = `dirname" $ ​​{THIS} "`

# «Точка» означает «источник», то есть «включать»:
, "$ DIR / compile.sh"

Метод поддерживает все это:

  • Пространства в пути
  • Ссылки (через readlink)
  • ${BASH_SOURCE[0]} является более надежным, чем $0
Брайан Хаак
источник
1
ЭТО = readlink -f "${BASH_SOURCE[0]}" 2>/dev/null||echo $0 если ваша ссылка для чтения BusyBox v1.01
Alexx Roche
@AlexxRoche спасибо! Будет ли это работать на всех Linux?
Брайан Хаак
1
Я бы так и ожидал. Кажется, работает на Debian Sid 3.16 и QvAP armv5tel 3.4.6 Linux.
Алекс Роше
2
Я поместил это в одну строку:DIR=$(dirname $(readlink -f "${BASH_SOURCE[0]}" 2>/dev/null||echo $0)) # https://stackoverflow.com/a/34208365/
Acumenus
20
SRC=$(cd $(dirname "$0"); pwd)
source "${SRC}/incl.sh"
Максимум
источник
1
Я подозреваю, что вы проголосовали за "cd ...", когда dirname "$ 0" должно выполнить то же самое ...
Аарон Х.
7
Этот код будет возвращать абсолютный путь, даже если скрипт выполняется из текущего каталога. Только $ (dirname "$ 0") вернет просто "."
Макс
1
И ./incl.shразрешается по тому же пути, что и cd+ pwd. Так в чем же преимущество смены каталога?
10
Иногда вам нужен абсолютный путь скрипта, например, когда вам нужно менять каталоги туда и обратно.
Ларикс Децидуа
15

Это работает, даже если скрипт получен:

source "$( dirname "${BASH_SOURCE[0]}" )/incl.sh"
Алессандро Пеццато
источник
Не могли бы вы объяснить, какова цель "[0]"?
Рэй
1
@Ray BASH_SOURCE- это массив путей, соответствующих стеку вызовов. Первый элемент соответствует самому последнему сценарию в стеке, который является текущим выполняемым сценарием. На самом деле, $BASH_SOURCEвызываемая как переменная по умолчанию расширяется до первого элемента, поэтому [0]здесь нет необходимости. Смотрите эту ссылку для деталей.
Джонатан Х
10

1. Neatest

Я изучил почти каждое предложение, и вот самое подходящее, которое сработало для меня:

script_root=$(dirname $(readlink -f $0))

Это работает даже тогда, когда скрипт имеет символическую ссылку на $PATHкаталог.

Смотрите это в действии здесь: https://github.com/pendashteh/hcagent/blob/master/bin/hcagent

2. Самый крутой

# Copyright https://stackoverflow.com/a/13222994/257479
script_root=$(ls -l /proc/$$/fd | grep "255 ->" | sed -e 's/^.\+-> //')

Это на самом деле из другого ответа на этой самой странице, но я тоже добавляю его в свой ответ!

2. Самый надежный

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

# Copyright http://stackoverflow.com/a/7400673/257479
myreadlink() { [ ! -h "$1" ] && echo "$1" || (local link="$(expr "$(command ls -ld -- "$1")" : '.*-> \(.*\)$')"; cd $(dirname $1); myreadlink "$link" | sed "s|^\([^/].*\)\$|$(dirname $1)/\1|"); }
whereis() { echo $1 | sed "s|^\([^/].*/.*\)|$(pwd)/\1|;s|^\([^/]*\)$|$(which -- $1)|;s|^$|$1|"; } 
whereis_realpath() { local SCRIPT_PATH=$(whereis $1); myreadlink ${SCRIPT_PATH} | sed "s|^\([^/].*\)\$|$(dirname ${SCRIPT_PATH})/\1|"; } 

script_root=$(dirname $(whereis_realpath "$0"))

Вы можете увидеть это в действии в taskrunnerисточнике: https://github.com/pendashteh/taskrunner/blob/master/bin/taskrunner

Надеюсь, это поможет кому-то там :)

Кроме того, пожалуйста, оставьте его в качестве комментария, если он не работает для вас, и укажите вашу операционную систему и эмулятор. Спасибо!

Alexar
источник
7

Вам нужно указать расположение других скриптов, другого пути нет. Я бы порекомендовал настраиваемую переменную вверху вашего скрипта:

#!/bin/bash
installpath=/where/your/scripts/are

. $installpath/incl.sh

echo "The main script"

В качестве альтернативы вы можете настаивать на том, чтобы пользователь поддерживал переменную среды, указывающую, где находится ваша программа, например, PROG_HOME или что-то подобное. Это может быть предоставлено пользователю автоматически путем создания сценария с этой информацией в /etc/profile.d/, который будет запрашиваться при каждом входе пользователя в систему.

Стив Бейкер
источник
1
Я ценю стремление к конкретности, но не могу понять, почему требуется указывать полный путь, если сценарии включения не были частью другого пакета. Я не вижу загрузки разницы в безопасности по определенному относительному пути (то есть тому же каталогу, где выполняется скрипт) по сравнению с конкретным полным путем. Почему вы говорите, что нет пути?
Аарон Х.
4
Поскольку каталог, в котором выполняется ваш скрипт, не обязательно находится в том месте, где находятся скрипты, которые вы хотите включить в ваш скрипт. Вы хотите загрузить сценарии, в которых они установлены, и нет надежного способа определить, где они находятся во время выполнения. Не использовать фиксированное местоположение - это также хороший способ включить неверный (т.е. предоставленный хакером) скрипт и запустить его.
Стив Бейкер
6

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

Все остальные сценарии будут исходить из этого сценария, чтобы все расположения были общими для всех сценариев с использованием сценария setenv.

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

Мы использовали такую ​​технику на нашей сборочной обезьяне, которая использовалась для непрерывной интеграции в рамках проекта около 2000 kSLOC.

Роб Уэллс
источник
3

Ответ Стива, безусловно, является правильной техникой, но его следует реорганизовать так, чтобы ваша переменная installpath находилась в отдельном скрипте среды, в котором сделаны все такие объявления.

Затем все сценарии исходят из этого сценария и должны изменить путь установки, вам нужно изменить его только в одном месте. Делает вещи более, э, будущее. Боже, я ненавижу это слово! (-:

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

. ${installpath}/incl.sh

Если фигурные скобки пропущены, некоторые оболочки попытаются расширить переменную "installpath / incl.sh"!

Роб Уэллс
источник
3

Shell Script Loader является моим решением для этого.

Он предоставляет функцию с именем include (), которая может вызываться много раз во многих сценариях для ссылки на один сценарий, но загружает сценарий только один раз. Функция может принимать полные или частичные пути (сценарий ищется в пути поиска). Также предусмотрена аналогичная функция load (), которая загружает сценарии безоговорочно.

Это работает для bash , ksh , pd ksh и zsh с оптимизированными скриптами для каждого из них; и другие оболочки, которые в общем совместимы с исходным sh, такие как ash , dash , heirloom sh и т. д., с помощью универсального сценария, который автоматически оптимизирует его функции в зависимости от функций, которые может обеспечить оболочка.

[Пример наград]

start.sh

Это необязательный стартовый скрипт. Размещение методов запуска здесь просто для удобства и может быть помещено в основной скрипт. Этот сценарий также не нужен, если сценарии должны быть скомпилированы.

#!/bin/sh

# load loader.sh
. loader.sh

# include directories to search path
loader_addpath /usr/lib/sh deps source

# load main script
load main.sh

main.sh

include a.sh
include b.sh

echo '---- main.sh ----'

# remove loader from shellspace since
# we no longer need it
loader_finish

# main procedures go from here

# ...

a.sh

include main.sh
include a.sh
include b.sh

echo '---- a.sh ----'

b.sh

include main.sh
include a.sh
include b.sh

echo '---- b.sh ----'

вывод:

---- b.sh ----
---- a.sh ----
---- main.sh ----

Что лучше всего, сценарии на его основе также могут быть скомпилированы для формирования единого сценария с доступным компилятором.

Вот проект, который использует его: http://sourceforge.net/p/playshell/code/ci/master/tree/ . Он может работать переносимо с или без компиляции скриптов. Может также произойти компиляция для создания одного скрипта, что полезно при установке.

Я также создал более простой прототип для любой консервативной партии, которая может хотеть иметь краткое представление о том, как работает скрипт реализации: https://sourceforge.net/p/loader/code/ci/base/tree/loader-include-prototype .bash . Он небольшой, и любой может просто включить код в свой основной скрипт, если захочет, если его код предназначен для работы с Bash 4.0 или новее, и он также не использует eval.

konsolebox
источник
3
12 килобайт скрипта Bash, содержащего более 100 строк evalкода для загрузки зависимостей. Ой
10
1
Ровно один из трех evalблоков внизу всегда запускается. Так что, нужно ли это или нет, он обязательно использует eval.
10
3
Этот eval-вызов безопасен и не используется, если у вас Bash 4.0+. Я вижу, вы один из тех старых сценаристов, которые думают, что evalэто чистое зло, и не знают, как его использовать.
konsolebox
1
Я не знаю, что вы имеете в виду под "не используется", но он запущен. И после нескольких лет написания сценариев оболочки, как части моей работы, да, я больше убежден, чем когда-либо, что evalэто зло.
10
1
Мгновенно приходят на ум два простых и простых способа пометки файлов: либо ссылаясь на них по номеру их инода, либо помещая пути, разделенные NUL, в файл.
10
2

Лично поместите все библиотеки в libпапку и используйте importфункцию для их загрузки.

структура папок

введите описание изображения здесь

script.sh содержание

# Imports '.sh' files from 'lib' directory
function import()
{
  local file="./lib/$1.sh"
  local error="\e[31mError: \e[0mCannot find \e[1m$1\e[0m library at: \e[2m$file\e[0m"
  if [ -f "$file" ]; then
     source "$file"
    if [ -z $IMPORTED ]; then
      echo -e $error
      exit 1
    fi
  else
    echo -e $error
    exit 1
  fi
}

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

import "utils"
import "requirements"

Добавьте одну строку вверху каждой библиотеки (т.е. utils.sh):

IMPORTED="$BASH_SOURCE"

Теперь у вас есть доступ к функциям внутри utils.shи requirements.shизscript.sh

TODO: написать компоновщик для создания одного shфайла

Xaqron
источник
Решает ли это также проблему выполнения сценария вне каталога, в котором он находится?
Аарон Х.
@AaronH. Нет, это структурированный способ включения зависимостей в большие проекты.
Xaqron
1

Использование source или $ 0 не даст вам реальный путь к вашему сценарию. Вы можете использовать идентификатор процесса скрипта, чтобы получить его реальный путь

ls -l       /proc/$$/fd           | 
grep        "255 ->"            |
sed -e      's/^.\+-> //'

Я использую этот скрипт, и он всегда служил мне хорошо :)

francoisrv
источник
1

Я поместил все свои сценарии запуска в каталог .bashrc.d. Это обычная техника в таких местах, как /etc/profile.d и т. Д.

while read file; do source "${file}"; done <<HERE
$(find ${HOME}/.bashrc.d -type f)
HERE

Проблема с решением с использованием globbing ...

for file in ${HOME}/.bashrc.d/*.sh; do source ${file};done

... есть ли у вас список файлов, который "слишком длинный". Подход, как ...

find ${HOME}/.bashrc.d -type f | while read file; do source ${file}; done

... работает, но не меняет окружающую среду по желанию.

phreed
источник
1

Конечно, каждому свое, но я думаю, что блок ниже довольно солидный. Я считаю, что это включает «лучший» способ найти каталог и «лучший» способ вызвать другой скрипт bash:

scriptdir=`dirname "$BASH_SOURCE"`
source $scriptdir/incl.sh

echo "The main script"

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

modulitos
источник
1

Это должно работать надежно:

source_relative() {
 local dir="${BASH_SOURCE%/*}"
 [[ -z "$dir" ]] && dir="$PWD"
 source "$dir/$1"
}

source_relative incl.sh
PSkocik
источник
0

нам просто нужно найти папку, в которой хранятся наши incl.sh и main.sh; просто измените ваш main.sh с этим:

main.sh

#!/bin/bash

SCRIPT_NAME=$(basename $0)
SCRIPT_DIR="$(echo $0| sed "s/$SCRIPT_NAME//g")"
source $SCRIPT_DIR/incl.sh

echo "The main script"
fastrizwaan
источник
Неправильное цитирование, ненужное использование echoи неправильное использование sed«S gопции. -1.
10
Во всяком случае, чтобы получить этот сценарий работы: SCRIPT_DIR=$(echo "$0" | sed "s/${SCRIPT_NAME}//")а затемsource "${SCRIPT_DIR}incl.sh"
Krzysiek
0

Соответственно man hierподходящее место для сценария включает в себя/usr/local/lib/

/ USR / местные / Библиотека

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

Лично я предпочитаю /usr/local/lib/bash/includesвключать. Существует библиотека bash-helper для включения библиотек таким образом:

#!/bin/bash

. /usr/local/lib/bash/includes/bash-helpers.sh

include api-client || exit 1                   # include shared functions
include mysql-status/query-builder || exit 1   # include script functions

# include script functions with status message
include mysql-status/process-checker; status 'process-checker' $? || exit 1
include mysql-status/nonexists; status 'nonexists' $? || exit 1

bash-helpers включает вывод статуса

Александр Янчарук
источник
-5

Вы также можете использовать:

PWD=$(pwd)
source "$PWD/inc.sh"
Джанго
источник
9
Вы предполагаете, что находитесь в том же каталоге, где расположены скрипты. Это не сработает, если вы где-то еще.
Люк М