Як найкраще включати інші сценарії?


353

Те, як ви зазвичай включаєте сценарій, є "джерелом"

наприклад:

main.sh:

#!/bin/bash

source incl.sh

echo "The main script"

в т.ч.

echo "The included script"

Результатом виконання "./main.sh" є:

The included script
The main script

... Тепер, якщо ви намагаєтеся виконати цей скрипт оболонки з іншого місця, він не може знайти включити, якщо це не на вашому шляху.

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


2

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
@Philipp, Автор цього запису правильний, він складний, і є gotchas. Але в ньому відсутні деякі ключові моменти, по-перше, автор припускає дуже багато речей про те, що ви збираєтеся робити зі своїм баш-сценарієм. Я не очікував би запуску сценарію python і без його залежностей. Bash - це клейова мова, що дозволяє швидко робити речі, які інакше були б важкими. Коли вам потрібна система побудови для роботи, прагматизм (І приємне попередження про те, що сценарій не може знайти залежності) виграє.
Аарон Х.

11
Щойно дізнався про BASH_SOURCEмасив та про те, як перший елемент у цьому масиві завжди вказує на поточне джерело.
haridsv

6
Це не працюватиме, якщо сценарії мають різні місця. напр. /home/me/main.sh викликає /home/me/test/inc.sh як dirname повернеться / home / me. SacII відповідь з використанням 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"

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


7
Це єдина відповідь у темі, яка послідовно працювала для мене
Джастін

3
@sacii Чи можу я знати, коли if [[ ! -d "$DIR" ]]; then DIR="$PWD"; fiпотрібна лінія ? Я можу знайти потребу в тому, якщо команди будуть вставлені в баш-підказку для запуску. Однак, якщо ви працюєте в контексті файлу сценарію, я не бачу потреби в ньому ...
Johnny Wong

2
Варто також зазначити, що він працює так, як очікувалося, для декількох sources (я маю на увазі, якщо ви sourceсценарій, який sourceінший в іншому каталозі тощо), він все ще працює)
Ciro Costa

4
Чи не повинно бути так, ${BASH_SOURCE[0]}як ви хочете лише останнього дзвонив? Також використання DIR=$(dirname ${BASH_SOURCE[0]})дозволить позбутися від умовного стану
kshenoy

1
Чи готові ви зробити цей фрагмент доступним під ліцензією, яка не потребує атрибутів, як CC0, або просто випустити його у загальнодоступне надбання? Я хотів би використати це дослівно, але прикро ставити атрибуцію у верхній частині кожного окремого сценарію!
BeeOnRope

52

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

scriptPath=$(dirname $0)

є:

scriptPath=${0%/*}

.. перевагою є відсутність залежності від dirname, яка не є вбудованою командою (і не завжди доступна в емуляторах)


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"працює для тих випадків
dsm

27

Я думаю, що найкращий спосіб зробити це - використовувати спосіб Кріса Борана, Але ви повинні обчислити MY_DIR таким чином:

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

Щоб цитувати довідкові сторінки для 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він працює.


Приємне і просте рішення, і чудово працює для мене у багатьох варіантах виклику сценарію, про які я міг придумати. Дякую.
Брайан Клайн

Окрім проблем із відсутніми цитатами, чи є фактичний випадок використання, де ви хочете вирішити символічні посилання, а не використовувати $0безпосередньо?
l0b0

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

Поєднання відповідей на це питання дає найбільш надійне рішення.

Це працювало для нас у виробничих сценаріях з великою підтримкою залежностей та структури директорій:

#! / бін / баш

# Повний шлях поточного сценарію
ЦЕ = `readlink -f" $ {BASH_SOURCE [0]} "2> / dev / null || ехо $ 0"

# Каталог, де знаходиться поточний сценарій
DIR = `dirname" $ ​​{ЦЕ}} "`

# "Крапка" означає "джерело", тобто "включати":
. "$ DIR / compile.sh"

Метод підтримує все це:

  • Проміжки в шляху
  • Посилання (через readlink)
  • ${BASH_SOURCE[0]} є більш міцним, ніж $0

1
ЦЕ = readlink -f "${BASH_SOURCE[0]}" 2>/dev/null||echo $0 якщо вашим readlink є BusyBox v1.01
Алекс Рош

@AlexxRoche дякую! Чи буде це працювати на всіх Linux?
Брайан Хаак

1
Я би очікував так. Здається, працює над Debian Sid 3.16 та QNAP 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. Тож яка перевага зміни каталогу?
l0b0

Іноді потрібен абсолютний шлях сценарію, наприклад, коли вам доведеться змінювати каталоги вперед і назад.
Laryx Decidua

15

Це працює, навіть якщо сценарій розміщений:

source "$( dirname "${BASH_SOURCE[0]}" )/incl.sh"

Чи можете ви пояснити, яка мета "[0]"?
Рей

1
@Ray BASH_SOURCE- це масив шляхів, відповідний стеку викликів. Перший елемент відповідає останньому сценарію в стеку, який є сценарієм, що виконується в даний час. Насправді $BASH_SOURCEвиклик як змінна розширюється до свого першого елемента за замовчуванням, тому [0]тут не потрібно. Детальну інформацію див. За цим посиланням .
Джонатан Н

10

1. Неатест

Я вивчив майже кожну пропозицію, і ось найновіша, яка працювала для мене:

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

Сподіваюся, це допоможе комусь там :)

Крім того, залиште це як коментар, якщо хтось не працював для вас, і згадайте про вашу операційну систему та емулятор. Дякую!


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.

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

Ми використовували таку техніку на нашій мавпі, яка будувалась для постійної інтеграції в проект близько 2000 кСЛОК.


3

Відповідь Стіва, безумовно, є правильною технікою, але вона повинна бути відновлена, щоб змінна installpath знаходилась в окремому сценарії середовища, де зроблено всі такі декларації.

Тоді всі сценарії джерела цього сценарію і повинні встановити шлях змінити, вам потрібно змінити його лише в одному місці. Робить речі більшіми, е, перспективними. Боже, я ненавиджу це слово! (-:

BTW Ви дійсно повинні посилатися на змінну, використовуючи $ {installpath}, використовуючи її так, як показано у вашому прикладі:

. ${installpath}/incl.sh

Якщо дужки не залишиться, деякі оболонки спробують розширити змінну "installpath / incl.sh"!


3

Shell Script Loader - це моє рішення для цього.

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

Він працює для bash , ksh , pd ksh та zsh з оптимізованими сценаріями для кожного з них; та інші оболонки, які загалом сумісні з оригінальним ш, як попел , тире , реліквія 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

# ...

зола

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

echo '---- a.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 4.0 або новішою версією, і він також не використовується eval.


3
12 кілобайт сценарію Bash, що містить понад 100 рядків evaled-коду для завантаження залежностей. Ой
l0b0

1
Точно один із трьох evalблоків біля нижнього завжди виконується. Тож, потрібен він чи ні, він, звичайно, використовує eval.
l0b0

3
Цей виклик eval є безпечним і не використовується, якщо у вас є Bash 4.0+. Я бачу, ви один із тих давніх скриптерів, які вважають evalчистим злом, і не знаєте, як добре використати це.
konsolebox

1
Я не знаю, що ви маєте на увазі під "не використовується", але це працює. І через кілька років сценаріїв снарядів як частини моєї роботи, так, я переконаний, як ніколи, в тому, що evalце зло.
l0b0

1
Два переносних, прості способи позначення файлів приходять на розум миттєво: або посилаючись на них за їх номером inode, або шляхом введення в файл файлів, розділених NUL.
l0b0

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файлу


Чи це також вирішує проблему виконання сценарію за межами режиму, в якому він знаходиться?
Аарон Х.

@AaronH. Ні. Це структурований спосіб включення залежностей у великі проекти.
Xaqron

1

Використання джерела або $ 0 не дасть вам реального шляху вашого сценарію. Ви можете використовувати ідентифікатор процесу скрипту, щоб отримати його реальний шлях

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

Я використовую цей сценарій, і він мені завжди добре служив :)


1

Я розміщую всі мої сценарії запуску в каталог .bashrc.d. Це поширена техніка в таких місцях, як /etc/profile.d тощо.

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

Проблема з рішенням за допомогою глобулінгу ...

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

... чи може у вас є список файлів, який "занадто довгий". Підхід, як ...

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

... працює, але не змінює середовище, як бажано.


1

Звичайно, у кожного свої, але я думаю, що блок нижче досить міцний. Я вважаю, що це включає "найкращий" спосіб пошуку каталогу та "найкращий" спосіб викликати інший скрипт bash:

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

echo "The main script"

Тож це може бути "найкращий" спосіб включення інших сценаріїв. Це ґрунтується на іншій "найкращій" відповіді, яка повідомляє сценарій bash, де він зберігається


1

Це має надійно працювати:

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

source_relative incl.sh

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"

Неправильне цитування, непотрібне використання echoта неправильне використання опції sed's g. -1.
l0b0

У будь-якому випадку, щоб зробити цю роботу над сценарієм: SCRIPT_DIR=$(echo "$0" | sed "s/${SCRIPT_NAME}//")а потімsource "${SCRIPT_DIR}incl.sh"
Krzysiek

0

Відповідне man hierмісце для сценарію включає в себе/usr/local/lib/

/ usr / local / lib

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

Особисто я віддаю перевагу /usr/local/lib/bash/includesдля включає. Існує " bash-helper lib" для включення libs таким чином:

#!/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
Ви припускаєте, що перебуваєте в тому самому каталозі, де розміщені сценарії. Це не спрацює, якщо ви десь ще.
Люк М
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.