Jak najlepiej dołączyć inne skrypty?


353

Sposób, w jaki zwykle dołączasz skrypt, to „source”

na przykład:

main.sh:

#!/bin/bash

source incl.sh

echo "The main script"

w tym sh:

echo "The included script"

Wynikiem wykonania polecenia „./main.sh” jest:

The included script
The main script

... Teraz, jeśli spróbujesz wykonać ten skrypt powłoki z innej lokalizacji, nie będzie w stanie znaleźć dołączenia, chyba że jest na twojej ścieżce.

Jaki jest dobry sposób, aby twój skrypt mógł znaleźć skrypt dołączania, zwłaszcza jeśli na przykład skrypt musi być przenośny?



1
Twoje pytanie jest tak dobre i pouczające, że na moje pytanie udzielono odpowiedzi, zanim jeszcze zadałeś swoje pytanie! Dobra robota!
Gabriel Staples

Odpowiedzi:


229

Staram się, aby wszystkie moje skrypty były względem siebie względne. W ten sposób mogę użyć nazwy katalogu:

#!/bin/sh

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

"$my_dir/other_script.sh"

5
To nie zadziała, jeśli skrypt zostanie wykonany za pomocą $ PATH. wtedy which $0przyda się
Hugo,

41
Nie ma niezawodnego sposobu ustalenia lokalizacji skryptu powłoki, patrz mywiki.wooledge.org/BashFAQ/028
Philipp

12
@Philipp, autor tego wpisu jest poprawny, jest złożony i są gotchas. Ale brakuje kilku kluczowych punktów, po pierwsze, autor zakłada wiele rzeczy o tym, co zamierzasz zrobić ze skryptem bash. Nie spodziewałbym się też, że skrypt Pythona będzie działał bez jego zależności. Bash to język kleju, który pozwala szybko robić rzeczy, które w innym przypadku byłyby trudne. Kiedy potrzebujesz systemu kompilacji do pracy, pragmatyzm (i miłe ostrzeżenie o tym, że skrypt nie może znaleźć zależności) wygrywa.
Aaron H.

11
Właśnie poznałem BASH_SOURCEtablicę i jak pierwszy element w tej tablicy zawsze wskazuje na bieżące źródło.
haridsv

6
To nie zadziała, jeśli skrypty są w różnych lokalizacjach. np. /home/me/main.sh wywołuje /home/me/test/inc.sh, ponieważ dirname zwróci / home / me. Odpowiedź sacii przy użyciu BASH_SOURCE jest lepszym rozwiązaniem stackoverflow.com/a/12694189/1000011
opticyclic

187

Wiem, że jestem spóźniony na imprezę, ale powinno to działać bez względu na to, jak uruchomisz skrypt i używasz wyłącznie wbudowanych funkcji:

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

.Komenda (kropka) jest aliasem do source, $PWDjest ścieżką do katalogu roboczego, BASH_SOURCEjest zmienną tablicową, której członkowie są źródłowymi nazwami plików, ${string%substring}usuwa najkrótsze dopasowanie $ substring z tyłu $ string


7
To jedyna odpowiedź w wątku, który konsekwentnie dla mnie działał
Justin

3
@sacii Czy mogę wiedzieć, kiedy linia jest if [[ ! -d "$DIR" ]]; then DIR="$PWD"; fipotrzebna? Mogę znaleźć taką potrzebę, jeśli polecenia są wklejane do monitu bash, aby uruchomić. Jednak jeśli działam w kontekście pliku skryptu, nie widzę potrzeby, aby to zrobić ...
Johnny Wong

2
Warto również zauważyć, że działa zgodnie z oczekiwaniami na wielu sources (to znaczy, jeśli sourceskrypt sourcejest inny w innym katalogu i tak dalej, nadal działa).
Ciro Costa,

4
Czy nie powinno to być ${BASH_SOURCE[0]}tak, jak chcesz tylko najnowszą? Ponadto użycie DIR=$(dirname ${BASH_SOURCE[0]})pozwoli ci pozbyć się warunku
if

1
Czy chcesz udostępnić ten fragment kodu na podstawie licencji bez atrybutów, takich jak CC0, czy po prostu udostępnić go publicznie? Chciałbym użyć tego dosłownie, ale denerwujące jest umieszczanie atrybucji na szczycie każdego skryptu!
BeeOnRope

52

Alternatywa dla:

scriptPath=$(dirname $0)

jest:

scriptPath=${0%/*}

.. zaletą jest brak zależności od dirname, co nie jest wbudowanym poleceniem (i nie zawsze jest dostępne w emulatorach)


2
basePath=$(dirname $0)dał mi pustą wartość, gdy zawiera plik skryptowy zawierający źródło.
prayagupd

41

Jeśli znajduje się w tym samym katalogu, możesz użyć dirname $0:

#!/bin/bash

source $(dirname $0)/incl.sh

echo "The main script"

2
Dwie pułapki: 1) zwraca i $0nazwa ./t.shkatalogu wraca .; 2) po cd binzwróceniu .jest nieprawidłowy. $BASH_SOURCEnie jest lepsze.
18446744073709551615

kolejna pułapka: spróbuj tego ze spacjami w nazwie katalogu.
Hubert Grzeskowiak

source "$(dirname $0)/incl.sh" działa w tych przypadkach
dsm

27

Myślę, że najlepszym sposobem na to jest użycie metody Chrisa Borana, ALE powinieneś obliczyć MY_DIR w ten sposób:

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

Aby zacytować strony podręcznika 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

Nigdy nie spotkałem przypadku użycia, w którym MY_DIRnie jest poprawnie obliczony. Jeśli uzyskasz dostęp do skryptu za pomocą dowiązania symbolicznego $PATH, działa.


Ładne i proste rozwiązanie, które działa dla mnie w wielu odmianach wywoływania skryptów, o których mogłem myśleć. Dzięki.
Brian Cline,

Oprócz problemów z brakującymi cytatami, czy jest jakiś rzeczywisty przypadek użycia, w którym chciałbyś rozwiązać dowiązania symboliczne, a nie użyć $0bezpośrednio?
l0b0

1
@ l0b0: Wyobraź sobie, że masz skrypt, z którego /home/you/script.shmożesz cd /homeuruchomić skrypt, ponieważ w ./you/script.shtym przypadku dirname $0powróci ./youi
dołączenie

1
Świetna sugestia, ale musiałem wykonać następujące czynności, aby odczytać moje zmienne w ` MY_DIR=$(dirname $(readlink -f $0)); source $MY_DIR/incl.sh
Frederick Ollinger

21

Kombinacja odpowiedzi na to pytanie zapewnia najbardziej niezawodne rozwiązanie.

Pracował dla nas w skryptach klasy produkcyjnej z doskonałym wsparciem dla zależności i struktury katalogów:

#! / bin / bash

# Pełna ścieżka bieżącego skryptu
TO = `readlink -f" $ {BASH_SOURCE [0]} "2> / dev / null || echo $ 0`

# Katalog, w którym znajduje się bieżący skrypt
DIR = `dirname" $ ​​{THIS} "`

# „Kropka” oznacza „źródło”, tzn. „Dołącz”:
. „$ DIR / compile.sh”

Metoda obsługuje wszystkie te:

  • Przestrzenie na ścieżce
  • Linki (przez readlink)
  • ${BASH_SOURCE[0]} jest bardziej solidny niż $0

1
TO = readlink -f "${BASH_SOURCE[0]}" 2>/dev/null||echo $0 jeśli twój link do odczytu to BusyBox v1.01
Alexx Roche

@AlexxRoche dziękuję! Czy to zadziała na wszystkich Linuxach?
Brian Haak,

1
Oczekiwałbym tego. Wygląda na to, że działa na Debianie Sid 3.16 i armv5tel 3.4.6 Linux QNAP.
Alexx Roche

2
Umieszczam to w jednym wierszu: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
Podejrzewam, że zagłosowałeś na „cd ...”, kiedy nazwa reżysera „0 $” powinna osiągnąć to samo ...
Aaron H.,

7
Ten kod zwróci ścieżkę bezwzględną, nawet jeśli skrypt jest wykonywany z bieżącego katalogu. Sam $ (nazwa katalogu „0 $”) zwróci tylko „.”
Max

1
I ./incl.shrozwiązuje tę samą ścieżkę co cd+ pwd. Jaka jest więc zaleta zmiany katalogu?
l0b0

Czasami potrzebujesz bezwzględnej ścieżki skryptu, np. Gdy musisz zmieniać katalogi tam iz powrotem.
Laryx Decidua

15

Działa to nawet wtedy, gdy skrypt pochodzi:

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

Czy możesz wyjaśnić, jaki jest cel „[0]”?
Ray

1
@Ray BASH_SOURCEto tablica ścieżek odpowiadająca stosowi wywołań. Pierwszy element odpowiada najnowszemu skryptowi na stosie, który jest obecnie wykonywanym skryptem. W rzeczywistości $BASH_SOURCEwywoływana jako zmienna domyślnie rozwija się do pierwszego elementu, więc [0]tutaj nie jest to konieczne. Zobacz ten link, aby uzyskać szczegółowe informacje.
Jonathan H

10

1. Najładniejszy

Zbadałem prawie każdą sugestię i oto najładniejsza, która dla mnie działała:

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

Działa nawet wtedy, gdy skrypt jest dowiązaniem symbolicznym do $PATH katalogu.

Zobacz to w akcji tutaj: https://github.com/pendashteh/hcagent/blob/master/bin/hcagent

2. Najfajniejsza

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

To właściwie inna odpowiedź na tej samej stronie, ale dodaję ją również do mojej odpowiedzi!

2. Najbardziej niezawodny

Alternatywnie, w rzadkim przypadku, gdy te nie działały, oto podejście kuloodporne:

# 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"))

Możesz zobaczyć to w akcji w taskrunnerźródle: https://github.com/pendashteh/taskrunner/blob/master/bin/taskrunner

Mam nadzieję, że pomoże to komuś tam :)

Proszę również zostawić to jako komentarz, jeśli jeden nie działał dla Ciebie, i podaj swój system operacyjny i emulator. Dzięki!


7

Musisz określić lokalizację innych skryptów, nie ma innego wyjścia. Poleciłbym konfigurowalną zmienną u góry skryptu:

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

. $installpath/incl.sh

echo "The main script"

Alternatywnie możesz nalegać, aby użytkownik utrzymywał zmienną środowiskową wskazującą, gdzie znajduje się strona główna programu, na przykład PROG_HOME lub jakoś. Można to podać użytkownikowi automatycznie, tworząc skrypt z tymi informacjami w /etc/profile.d/, który będzie pozyskiwany przy każdym logowaniu użytkownika.


1
Doceniam potrzebę specyficzności, ale nie rozumiem, dlaczego pełna ścieżka powinna być wymagana, chyba że skrypty dołączające były częścią innego pakietu. Nie widzę różnicy zabezpieczeń ładującej się z określonej ścieżki względnej (tj. Z tego samego katalogu, w którym wykonuje się skrypt) w porównaniu z określoną pełną ścieżką. Dlaczego mówisz, że nie da się tego obejść?
Aaron H.

4
Ponieważ katalog, w którym wykonuje się skrypt, niekoniecznie musi zawierać lokalizację skryptów, które chcesz dołączyć do skryptu. Chcesz załadować skrypty, w których są zainstalowane, i nie ma niezawodnego sposobu, aby powiedzieć, gdzie to jest w czasie wykonywania. Niestosowanie stałej lokalizacji jest również dobrym sposobem na włączenie i uruchomienie niewłaściwego skryptu (tj. Dostarczonego przez hakera).
Steve Baker

6

Sugeruję utworzenie skryptu setenv, którego jedynym celem jest zapewnienie lokalizacji dla różnych komponentów w całym systemie.

Wszystkie inne skrypty następnie źródły tego skryptu, dzięki czemu wszystkie lokalizacje są wspólne dla wszystkich skryptów za pomocą skryptu setenv.

Jest to bardzo przydatne podczas uruchamiania cronjobs. Podczas uruchamiania crona otrzymujesz minimalne środowisko, ale jeśli wszystkie skrypty crona najpierw dołączą skrypt setenv, będziesz w stanie kontrolować i synchronizować środowisko, w którym chcesz uruchamiać cronjobs.

Zastosowaliśmy taką technikę na naszej małpce, która była używana do ciągłej integracji w ramach projektu o wielkości około 2000 kSLOC.


3

Odpowiedź Steve'a jest zdecydowanie poprawną techniką, ale należy ją ponownie przeredagować, aby zmienna installpath znajdowała się w osobnym skrypcie środowiska, w którym składane są wszystkie takie deklaracje.

Następnie wszystkie skrypty źródłowe tego skryptu i powinny zmienić ścieżkę instalacji, wystarczy ją zmienić tylko w jednym miejscu. Sprawia, że ​​wszystko jest bardziej bezpieczne. Boże, nienawidzę tego słowa! (-:

BTW Powinieneś naprawdę odwoływać się do zmiennej za pomocą $ {installpath}, gdy używasz jej w sposób pokazany w twoim przykładzie:

. ${installpath}/incl.sh

Jeśli nawiasy klamrowe zostaną pominięte, niektóre powłoki spróbują rozwinąć zmienną „installpath / incl.sh”!


3

Shell Script Loader to moje rozwiązanie.

Zapewnia funkcję o nazwie include (), która może być wywoływana wiele razy w wielu skryptach w celu odniesienia jednego skryptu, ale załaduje skrypt tylko raz. Funkcja może akceptować pełne lub częściowe ścieżki (skrypt jest przeszukiwany w ścieżce wyszukiwania). Zapewniona jest również podobna funkcja o nazwie load (), która bezwarunkowo ładuje skrypty.

Działa dla bash , ksh , pd ksh i zsh ze zoptymalizowanymi skryptami dla każdego z nich; oraz inne powłoki, które są ogólnie kompatybilne z oryginalnym sh, takie jak ash , dash , heirloom sh itp., poprzez uniwersalny skrypt, który automatycznie optymalizuje swoje funkcje w zależności od funkcji, które może zapewnić powłoka.

[Przyjęty przykład]

start.sh

To jest opcjonalny skrypt startowy. Umieszczenie tutaj metod uruchamiania jest tylko wygodą i można je zamiast tego umieścić w skrypcie głównym. Ten skrypt nie jest również potrzebny, jeśli skrypty mają zostać skompilowane.

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

# ...

popiół

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 ----'

wynik:

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

Najlepsze są oparte na nim skrypty, które można również skompilować w celu utworzenia pojedynczego skryptu z dostępnym kompilatorem.

Oto projekt, który go wykorzystuje: http://sourceforge.net/p/playshell/code/ci/master/tree/ . Może działać przenośnie z kompilacją skryptów lub bez. Kompilacja w celu utworzenia pojedynczego skryptu może się również zdarzyć i jest pomocna podczas instalacji.

Stworzyłem również prostszy prototyp dla każdej partii konserwatywnej, która może chcieć mieć krótkie wyobrażenie o tym, jak działa skrypt implementacyjny: https://sourceforge.net/p/loader/code/ci/base/tree/loader-include-prototype .bash . Jest mały i każdy może po prostu dołączyć kod do głównego skryptu, jeśli chce, jeśli jego kod ma działać z Bash 4.0 lub nowszym, a także nie używa eval.


3
12 kilobajtów skryptu Bash zawierającego ponad 100 wierszy evalkodu ed do ładowania zależności. Ojej
l0b0

1
Dokładnie jeden z trzech evalbloków u dołu jest zawsze uruchamiany. Tak więc, czy jest to potrzebne, czy nie, z pewnością używa eval.
l0b0

3
To eval połączenie jest bezpieczne i nie jest używane, jeśli masz Bash 4.0+. Rozumiem, że jesteś jednym z tych starych skrypterów, którzy myślą, że evaljest czystym złem i nie wiedzą, jak je wykorzystać.
konsolebox

1
Nie wiem, co rozumiesz przez „nieużywany”, ale jest uruchamiany. I po kilku latach pisania skryptów w ramach mojej pracy, tak, jestem bardziej niż kiedykolwiek przekonany, że evalto zło.
l0b0

1
Natychmiast przychodzą mi na myśl dwa przenośne, proste sposoby oznaczania plików: albo odwołanie się do nich po numerze i-węzła, albo poprzez umieszczenie ścieżek oddzielonych od NUL w pliku.
l0b0

2

Osobiście umieść wszystkie biblioteki w libfolderze i użyj importfunkcji, aby je załadować.

struktura folderów

wprowadź opis zdjęcia tutaj

script.sh zawartość

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

Pamiętaj, że ta funkcja importowania powinna znajdować się na początku skryptu, a następnie możesz łatwo importować swoje biblioteki w następujący sposób:

import "utils"
import "requirements"

Dodaj pojedynczy wiersz u góry każdej biblioteki (tj. Utils.sh):

IMPORTED="$BASH_SOURCE"

Teraz masz dostęp do funkcji wewnątrz utils.shi requirements.shzscript.sh

DO ZROBIENIA: Napisz linker, aby zbudować pojedynczy shplik


Czy to również rozwiązuje problem wykonywania skryptu poza katalogiem, w którym się znajduje?
Aaron H.,

@AaronH. Nie. Jest to ustrukturyzowany sposób uwzględniania zależności w dużych projektach.
Xaqron,

1

Użycie źródła lub 0 $ nie da prawdziwej ścieżki do twojego skryptu. Możesz użyć identyfikatora procesu skryptu, aby pobrać jego prawdziwą ścieżkę

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

Używam tego skryptu i zawsze mi dobrze służył :)


1

Wszystkie moje skrypty startowe umieszczam w katalogu .bashrc.d. Jest to powszechna technika w takich miejscach jak /etc/profile.d itp.

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

Problem z rozwiązaniem wykorzystującym globbing ...

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

... czy możesz mieć listę plików, która jest „za długa”. Podejście takie jak ...

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

... działa, ale nie zmienia środowiska zgodnie z oczekiwaniami.


1

Oczywiście dla każdego z nich, ale myślę, że poniższy blok jest dość solidny. Wierzę, że obejmuje to „najlepszy” sposób na znalezienie katalogu i „najlepszy” sposób na wywołanie innego skryptu bash:

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

echo "The main script"

Może to być „najlepszy” sposób na włączenie innych skryptów. Jest to oparte na innej „najlepszej” odpowiedzi, która informuje skrypt bash, gdzie jest przechowywany


1

Powinno to działać niezawodnie:

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

source_relative incl.sh

0

musimy tylko znaleźć folder, w którym przechowywane są nasze pliki inc. sh i main.sh; po prostu zmień plik main.sh za pomocą tego:

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"

Nieprawidłowy cytowanie, niepotrzebne użycie echoi niewłaściwe użytkowanie sed„s gopcja. -1.
l0b0

W każdym razie, aby ten skrypt działał, wykonaj: SCRIPT_DIR=$(echo "$0" | sed "s/${SCRIPT_NAME}//")a następniesource "${SCRIPT_DIR}incl.sh"
Krzysiek

0

Według man hierodpowiednie miejsce na skrypt obejmuje/usr/local/lib/

/ usr / local / lib

Pliki powiązane z lokalnie zainstalowanymi programami.

Osobiście preferuję /usr/local/lib/bash/includesdla obejmuje. Istnieje biblioteka bash-helper do dołączania bibliotek w ten sposób:

#!/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 zawiera wyjście statusu


-5

Możesz także użyć:

PWD=$(pwd)
source "$PWD/inc.sh"

9
Zakładasz, że znajdujesz się w tym samym katalogu, w którym znajdują się skrypty. To nie zadziała, jeśli jesteś gdzieś indziej.
Luc M
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.