Bagaimana cara terbaik untuk memasukkan skrip lain?

353

Cara Anda biasanya memasukkan skrip adalah dengan "sumber"

misalnya:

main.sh:

#!/bin/bash

source incl.sh

echo "The main script"

incl.sh:

echo "The included script"

Output dari mengeksekusi "./main.sh" adalah:

The included script
The main script

... Sekarang, jika Anda mencoba untuk mengeksekusi skrip shell dari lokasi lain, itu tidak dapat menemukan menyertakan kecuali itu di jalur Anda.

Apa cara yang baik untuk memastikan bahwa skrip Anda dapat menemukan skrip sertakan, terutama jika misalnya, skrip harus portabel?

Aaron H.
sumber
2
lihat yang ini: stackoverflow.com/questions/59895/…
Lluí
1
Pertanyaan Anda sangat bagus dan informatif sehingga pertanyaan saya dijawab bahkan sebelum Anda mengajukan pertanyaan Anda! Pekerjaan yang baik!
Gabriel Staples

Jawaban:

229

Saya cenderung membuat skrip saya menjadi relatif satu sama lain. Dengan begitu saya bisa menggunakan dirname:

#!/bin/sh

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

"$my_dir/other_script.sh"
Chris Boran
sumber
5
Ini tidak akan berfungsi jika skrip dijalankan melalui $ PATH. maka which $0akan berguna
Hugo
41
Tidak ada cara yang dapat diandalkan untuk menentukan lokasi skrip shell, lihat mywiki.wooledge.org/BashFAQ/028
Philipp
12
@ Pilipp, Penulis entri itu benar, kompleks, dan ada gotcha. Tapi ada beberapa poin kunci yang hilang, pertama, penulis mengasumsikan banyak hal tentang apa yang akan Anda lakukan dengan skrip bash Anda. Saya tidak akan mengharapkan skrip python berjalan tanpa ketergantungan itu baik. Bash adalah bahasa lem yang memungkinkan Anda melakukan hal-hal dengan cepat yang akan sulit sebaliknya. Ketika Anda membutuhkan sistem build Anda untuk bekerja, pragmatisme (Dan peringatan yang bagus tentang skrip tidak dapat menemukan dependensi) menang.
Aaron H.
11
Baru belajar tentang BASH_SOURCEarray, dan bagaimana elemen pertama dalam array ini selalu menunjuk ke sumber saat ini.
haridsv
6
Ini tidak akan berfungsi jika skrip berada di lokasi yang berbeda. misalnya / home / me / main.sh panggilan / home / me / test /inc.sh sebagai dirname akan kembali / home / me. sacii jawaban menggunakan BASH_SOURCE adalah solusi yang lebih baik stackoverflow.com/a/12694189/1000011
opticyclic
187

Saya tahu saya terlambat ke pesta, tetapi ini harus bekerja tidak peduli bagaimana Anda memulai skrip dan menggunakan builtin secara eksklusif:

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

.(dot) perintah adalah alias untuk source, $PWDadalah Path untuk Direktori Kerja, BASH_SOURCEadalah variabel array yang anggotanya adalah nama file sumber, ${string%substring}strip pencocokan terpendek $ substring dari belakang $ string

Sacii
sumber
7
Ini adalah satu-satunya jawaban di utas yang secara konsisten bekerja untuk saya
Justin
3
@sacii Bolehkah saya tahu kapan saluran if [[ ! -d "$DIR" ]]; then DIR="$PWD"; fidibutuhkan? Saya dapat menemukan kebutuhan untuk itu jika perintah sedang ditempelkan ke bash prompt untuk dijalankan. Namun, jika berjalan di dalam konteks file skrip, saya tidak dapat melihat kebutuhan untuk itu ...
Johnny Wong
2
Perlu juga dicatat bahwa ini berfungsi seperti yang diharapkan di beberapa sources (maksud saya, jika Anda sourceskrip yang sourcelain di direktori lain dan seterusnya, masih berfungsi).
Ciro Costa
4
Bukankah ini seharusnya ${BASH_SOURCE[0]}karena Anda hanya ingin yang terakhir dipanggil? Juga, menggunakan DIR=$(dirname ${BASH_SOURCE[0]})akan memungkinkan Anda untuk menyingkirkan if-condition
kshenoy
1
Apakah Anda bersedia membuat cuplikan ini tersedia di bawah lisensi tanpa atribut yang diperlukan, seperti CC0 atau hanya melepaskannya ke domain publik? Saya ingin menggunakan kata demi kata ini tetapi menjengkelkan untuk menempatkan atribusi di bagian atas setiap naskah!
BeeOnRope
52

Alternatif untuk:

scriptPath=$(dirname $0)

adalah:

scriptPath=${0%/*}

.. keuntungannya adalah tidak memiliki ketergantungan pada dirname, yang bukan perintah bawaan (dan tidak selalu tersedia di emulator)

tardate
sumber
2
basePath=$(dirname $0)memberi saya nilai kosong saat file skrip yang berisi bersumber.
prayagupd
41

Jika berada di direktori yang sama Anda dapat menggunakan dirname $0:

#!/bin/bash

source $(dirname $0)/incl.sh

echo "The main script"
dsm
sumber
2
Dua jebakan: 1) $0adalah ./t.shdan dirname mengembalikan .; 2) setelah cd bindikembalikan .tidak benar. $BASH_SOURCEtidak lebih baik.
18446744073709551615
perangkap lain: coba ini dengan spasi dalam nama direktori.
Hubert Grzeskowiak
source "$(dirname $0)/incl.sh"bekerja untuk kasus-kasus itu
dsm
27

Saya pikir cara terbaik untuk melakukan ini adalah dengan menggunakan cara Chris Boran, TETAPI Anda harus menghitung MY_DIR dengan cara ini:

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

Mengutip halaman manual untuk tautan baca:

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

Saya belum pernah menemukan kasus penggunaan di mana MY_DIRtidak dihitung dengan benar. Jika Anda mengakses skrip Anda melalui symlink, maka skrip Anda $PATHakan berfungsi.

Mat131
sumber
Solusi yang bagus dan sederhana, dan berfungsi dengan baik untuk saya dalam banyak variasi permintaan naskah yang dapat saya pikirkan. Terima kasih.
Brian Cline
Selain masalah dengan tanda kutip yang hilang, apakah ada kasus penggunaan aktual di mana Anda ingin menyelesaikan tautan simbolik daripada menggunakan $0secara langsung?
l0b0
1
@ l0b0: Bayangkan skrip /home/you/script.shAnda adalah Anda dapat cd /homedan menjalankan skrip Anda dari sana karena ./you/script.shDalam hal ini dirname $0akan kembali ./youdan termasuk skrip lain akan gagal
dr.scre
1
Namun saran yang bagus, saya perlu melakukan yang berikut agar dapat membaca variabel saya di ` MY_DIR=$(dirname $(readlink -f $0)); source $MY_DIR/incl.sh
Frederick Ollinger
21

Kombinasi jawaban untuk pertanyaan ini memberikan solusi yang paling kuat.

Ini bekerja untuk kami dalam skrip tingkat produksi dengan dukungan dependensi dan struktur direktori:

#! / bin / bash

# Path lengkap dari skrip saat ini
INI = `readlink -f" $ {BASH_SOURCE [0]} "2> / dev / null || echo $ 0`

# Direktori tempat skrip saat ini berada
DIR = `dirname" $ ​​{THIS} "`

# 'Dot' berarti 'sumber', yaitu 'termasuk':
. "$ DIR / compile.sh"

Metode ini mendukung semua ini:

  • Spasi di jalur
  • Tautan (via readlink)
  • ${BASH_SOURCE[0]} lebih kuat dari $0
Brian Haak
sumber
1
INI = readlink -f "${BASH_SOURCE[0]}" 2>/dev/null||echo $0 jika tautan balik Anda adalah BusyBox v1.01
Alexx Roche
@AlexxRoche terima kasih! Apakah ini akan bekerja di semua Linux?
Brian Haak
1
Saya harapkan begitu. Tampaknya bekerja pada Debian Sid 3.16 dan QNAP's armv5tel 3.4.6 Linux.
Alexx Roche
2
Saya letakkan di baris ini: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"
Maks
sumber
1
Saya menduga Anda turun memilih untuk "cd ..." ketika dirname "$ 0" harus mencapai hal yang sama ...
Aaron H.
7
Kode ini akan mengembalikan path absolut bahkan ketika skrip dieksekusi dari direktori saat ini. $ (nama samaran "$ 0") saja akan kembali "."
Maks
1
Dan ./incl.shmemutuskan ke jalur yang sama dengan cd+ pwd. Jadi apa keuntungan dari mengubah direktori?
l0b0
Terkadang Anda membutuhkan jalur absolut skrip, misalnya saat Anda harus mengubah direktori bolak-balik.
Laryx Decidua
15

Ini berfungsi bahkan jika skrip bersumber:

source "$( dirname "${BASH_SOURCE[0]}" )/incl.sh"
Alessandro Pezzato
sumber
Bisakah Anda menjelaskan apa tujuan dari "[0]"?
Ray
1
@ Ray BASH_SOURCEadalah array jalur, sesuai dengan panggilan-tumpukan. Elemen pertama berhubungan dengan skrip terbaru di stack, yang merupakan skrip yang sedang dieksekusi. Sebenarnya, yang $BASH_SOURCEdisebut sebagai variabel meluas ke elemen pertama secara default, jadi [0]tidak perlu di sini. Lihat tautan ini untuk detailnya.
Jonathan H
10

1. Rapi

Saya menjelajahi hampir setiap saran dan inilah saran yang paling bagus untuk saya:

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

Ia bekerja bahkan ketika skrip disinkronkan ke $PATHdirektori.

Lihat beraksi di sini: https://github.com/pendashteh/hcagent/blob/master/bin/hcagent

2. Yang paling keren

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

Ini sebenarnya dari jawaban lain di halaman ini, tapi saya juga menambahkannya ke jawaban saya!

2. Yang paling bisa diandalkan

Atau, dalam kasus yang jarang terjadi yang tidak berhasil, berikut adalah pendekatan bukti-peluru:

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

Anda dapat melihatnya beraksi di taskrunnersumber: https://github.com/pendashteh/taskrunner/blob/master/bin/taskrunner

Semoga ini bisa membantu seseorang di luar sana :)

Juga, silakan tinggalkan ini sebagai komentar jika ada yang tidak bekerja untuk Anda dan sebutkan sistem operasi dan emulator Anda. Terima kasih!

Alexar
sumber
7

Anda perlu menentukan lokasi skrip lain, tidak ada jalan lain untuk mengatasinya. Saya akan merekomendasikan variabel yang dapat dikonfigurasi di bagian atas skrip Anda:

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

. $installpath/incl.sh

echo "The main script"

Sebagai alternatif, Anda dapat mendesak agar pengguna mempertahankan variabel lingkungan yang menunjukkan di mana rumah program Anda berada, seperti PROG_HOME atau semacamnya. Ini dapat disediakan untuk pengguna secara otomatis dengan membuat skrip dengan informasi itu di /etc/profile.d/, yang akan bersumber setiap kali pengguna login.

Steve Baker
sumber
1
Saya menghargai keinginan untuk spesifisitas, tetapi saya tidak bisa melihat mengapa path lengkap harus diminta kecuali skrip yang disertakan adalah bagian dari paket lain. Saya tidak melihat perbedaan keamanan yang memuat dari jalur relatif tertentu (yaitu dir yang sama di mana skrip dijalankan.) Vs jalur penuh khusus. Mengapa Anda mengatakan tidak ada jalan lain?
Aaron H.
4
Karena direktori tempat skrip Anda dijalankan belum tentu skrip yang ingin Anda sertakan dalam skrip Anda berada. Anda ingin memuat skrip tempat skrip diinstal dan tidak ada cara yang dapat diandalkan untuk mengetahui di mana itu pada saat run-time. Tidak menggunakan lokasi tetap juga merupakan cara yang baik untuk memasukkan skrip yang salah (mis. Disediakan hacker) dan jalankan.
Steve Baker
6

Saya sarankan Anda membuat skrip setenv yang tujuan utamanya adalah untuk menyediakan lokasi untuk berbagai komponen di seluruh sistem Anda.

Semua skrip lain kemudian akan sumber skrip ini sehingga semua lokasi umum di semua skrip menggunakan skrip setenv.

Ini sangat berguna saat menjalankan cronjobs. Anda mendapatkan lingkungan minimal saat menjalankan cron, tetapi jika Anda membuat semua skrip cron terlebih dahulu memasukkan skrip setenv maka Anda dapat mengontrol dan menyinkronkan lingkungan yang Anda ingin jalankan cronjobs.

Kami menggunakan teknik seperti itu pada monyet bangun kami yang digunakan untuk integrasi berkesinambungan di proyek sekitar 2.000 kSLOC.

Rob Wells
sumber
3

Balasan Steve jelas merupakan teknik yang benar tetapi harus dire-refored agar variabel installpath Anda berada dalam skrip lingkungan terpisah tempat semua deklarasi tersebut dibuat.

Maka semua skrip sumber skrip itu dan harus menginstal perubahan, Anda hanya perlu mengubahnya di satu lokasi. Membuat lebih banyak hal, eh, futureproof. Ya Tuhan, aku benci kata itu! (-:

BTW Anda harus benar-benar merujuk ke variabel menggunakan $ {installpath} saat menggunakannya dengan cara yang ditunjukkan dalam contoh Anda:

. ${installpath}/incl.sh

Jika kawat gigi ditinggalkan, beberapa cangkang akan mencoba dan memperluas variabel "installpath / incl.sh"!

Rob Wells
sumber
3

Shell Script Loader adalah solusi saya untuk ini.

Ini menyediakan fungsi bernama include () yang dapat dipanggil berkali-kali dalam banyak skrip untuk merujuk skrip tunggal tetapi hanya akan memuat skrip sekali. Fungsi dapat menerima jalur lengkap atau jalur sebagian (skrip dicari dalam jalur pencarian). Fungsi serupa bernama load () juga disediakan yang akan memuat skrip tanpa syarat.

Ini berfungsi untuk bash , ksh , pd ksh dan zsh dengan skrip yang dioptimalkan untuk masing-masingnya; dan cangkang lain yang secara umum kompatibel dengan sh asli seperti abu , tanda hubung , pusaka sh , dll., melalui skrip universal yang secara otomatis mengoptimalkan fungsinya tergantung pada fitur yang disediakan cangkang.

[Fowarded example]

start.sh

Ini adalah skrip starter opsional. Menempatkan metode startup di sini hanya kenyamanan dan dapat ditempatkan di skrip utama sebagai gantinya. Skrip ini juga tidak diperlukan jika skrip ingin dikompilasi.

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

# ...

Abu

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

keluaran:

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

Yang terbaik adalah skrip berdasarkan itu juga dapat dikompilasi untuk membentuk satu skrip dengan kompiler yang tersedia.

Berikut adalah proyek yang menggunakannya: http://sourceforge.net/p/playshell/code/ci/master/tree/ . Itu dapat berjalan dengan mudah dengan atau tanpa mengkompilasi skrip. Kompilasi untuk menghasilkan satu skrip juga dapat terjadi, dan sangat membantu selama instalasi.

Saya juga membuat prototipe sederhana untuk pihak konservatif yang mungkin ingin memiliki gagasan singkat tentang cara kerja script implementasi: https://sourceforge.net/p/loader/code/ci/base/tree/loader-include-prototype .bash . Ini kecil dan siapa saja bisa memasukkan kode dalam skrip utama mereka jika mereka mau jika kode mereka dimaksudkan untuk dijalankan dengan Bash 4.0 atau lebih baru, dan itu juga tidak digunakan eval.

konsolebox
sumber
3
12 kilobyte skrip Bash yang berisi lebih dari 100 baris evalkode ed untuk memuat dependensi. Aduh
l0b0
1
Persis satu dari tiga evalblok di dekat bagian bawah selalu dijalankan. Jadi apakah itu dibutuhkan atau tidak, itu pasti menggunakan eval.
l0b0
3
Panggilan eval itu aman dan tidak digunakan jika Anda memiliki Bash 4.0+. Saya mengerti, Anda adalah salah satu dari penulis lama yang menganggapnya evalmurni jahat, dan tidak tahu bagaimana memanfaatkannya dengan baik.
konsolebox
1
Saya tidak tahu apa yang Anda maksud dengan "tidak digunakan", tetapi dijalankan. Dan setelah beberapa tahun menulis shell sebagai bagian dari pekerjaan saya, ya, saya lebih yakin daripada sebelumnya bahwa evalitu jahat.
l0b0
1
Dua cara portabel dan sederhana untuk menandai file langsung muncul di pikiran: Entah merujuknya dengan nomor inode mereka, atau dengan meletakkan jalur yang dipisahkan NUL ke dalam file.
l0b0
2

Secara pribadi letakkan semua perpustakaan di libfolder dan gunakan importfungsi untuk memuatnya.

struktur folder

masukkan deskripsi gambar di sini

script.sh isi

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

Perhatikan bahwa fungsi impor ini harus di awal skrip Anda dan kemudian Anda dapat dengan mudah mengimpor perpustakaan Anda seperti ini:

import "utils"
import "requirements"

Tambahkan satu baris di bagian atas setiap perpustakaan (mis. Utils.sh):

IMPORTED="$BASH_SOURCE"

Sekarang Anda memiliki akses ke fungsi di dalam utils.shdan requirements.shdariscript.sh

TODO: Tulis tautan untuk membuat satu shfile

Xaqron
sumber
Apakah ini juga memecahkan masalah mengeksekusi skrip di luar direktori itu?
Aaron H.
@ Harun. Tidak. Ini adalah cara terstruktur untuk memasukkan dependensi dalam proyek-proyek besar.
Xaqron
1

Menggunakan sumber atau $ 0 tidak akan memberi Anda jalan asli skrip Anda. Anda dapat menggunakan id proses skrip untuk mengambil jalur aslinya

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

Saya menggunakan skrip ini dan selalu membantu saya dengan baik :)

francoisrv
sumber
1

Saya meletakkan semua skrip startup saya di direktori .bashrc.d. Ini adalah teknik umum di tempat-tempat seperti /etc/profile.d, dll.

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

Masalah dengan solusi menggunakan globbing ...

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

... adalah Anda mungkin memiliki daftar file yang "terlalu panjang". Suatu pendekatan seperti ...

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

... berjalan tetapi tidak mengubah lingkungan seperti yang diinginkan.

phreed
sumber
1

Tentu saja, untuk masing-masing mereka sendiri, tetapi saya pikir blok di bawah ini cukup solid. Saya percaya ini melibatkan cara "terbaik" untuk menemukan direktori, dan cara "terbaik" untuk memanggil skrip bash lain:

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

echo "The main script"

Jadi ini mungkin cara "terbaik" untuk memasukkan skrip lain. Ini didasarkan pada jawaban "terbaik" lain yang memberi tahu skrip bash tempat disimpannya

modulitos
sumber
1

Ini harus bekerja dengan andal:

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

source_relative incl.sh
PSkocik
sumber
0

kita hanya perlu mencari tahu folder tempat incl.sh dan main.sh disimpan; ubah main.sh Anda dengan ini:

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
sumber
Salah mengutip, penggunaan yang tidak perlu echo, dan penggunaan yang salah dari sed's gpilihan. -1.
l0b0
Bagaimanapun, untuk mendapatkan skrip ini, lakukan: SCRIPT_DIR=$(echo "$0" | sed "s/${SCRIPT_NAME}//")dan kemudiansource "${SCRIPT_DIR}incl.sh"
Krzysiek
0

Menurut man hiertempat yang cocok untuk skrip termasuk adalah/usr/local/lib/

/ usr / local / lib

File yang terkait dengan program yang diinstal secara lokal.

Secara pribadi saya lebih suka /usr/local/lib/bash/includesuntuk menyertakan. Ada lib -helper lib untuk menyertakan lib dengan cara itu:

#!/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-help termasuk keluaran status

Alexander Yancharuk
sumber
-5

Anda juga bisa menggunakan:

PWD=$(pwd)
source "$PWD/inc.sh"
Django
sumber
9
Anda menganggap bahwa Anda berada di direktori yang sama di mana skrip berada. Ini tidak akan berhasil jika Anda berada di tempat lain.
Luc M