Il modo migliore per includere altri script?


353

Il modo in cui normalmente includeresti uno script è con "source"

per esempio:

main.sh:

#!/bin/bash

source incl.sh

echo "The main script"

incl.sh:

echo "The included script"

L'output dell'esecuzione "./main.sh" è:

The included script
The main script

... Ora, se si tenta di eseguire quello script di shell da un'altra posizione, non è possibile trovare l'inclusione a meno che non si trovi nel proprio percorso.

Qual è un buon modo per assicurarsi che il tuo script sia in grado di trovare lo script include, specialmente se, ad esempio, lo script deve essere portatile?



1
La tua domanda è così buona e istruttiva che la mia domanda ha avuto risposta prima ancora che tu ponessi la tua domanda! Bel lavoro!
Gabriel Staples

Risposte:


229

Tendo a rendere i miei script tutti relativi l'uno rispetto all'altro. In questo modo posso usare dirname:

#!/bin/sh

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

"$my_dir/other_script.sh"

5
Questo non funzionerà se lo script viene eseguito tramite $ PATH. allora which $0sarà utile
Hugo,

41
Non esiste un modo affidabile per determinare la posizione di uno script della shell, vedere mywiki.wooledge.org/BashFAQ/028
Philipp

12
@Philipp, l'autore di quella voce è corretta, è complessa e ci sono dei gotchas. Ma mancano alcuni punti chiave, in primo luogo, l'autore assume molte cose su ciò che farai con il tuo script bash. Non mi aspetterei che uno script Python venga eseguito senza le sue dipendenze. Bash è un linguaggio colla che ti consente di fare rapidamente cose che altrimenti sarebbero difficili. Quando hai bisogno che il tuo sistema di compilazione funzioni, il pragmatismo (E un bel avvertimento sullo script che non è in grado di trovare dipendenze) vince.
Aaron H.

11
Ho appena appreso l' BASH_SOURCEarray e come il primo elemento in questo array punta sempre alla fonte corrente.
Haridsv,

6
Questo non funzionerà se gli script sono in posizioni diverse. es. /home/me/main.sh chiama /home/me/test/inc.sh come dirname restituirà / home / me. SacII risposta utilizzando BASH_SOURCE è una soluzione migliore stackoverflow.com/a/12694189/1000011
opticyclic

187

So di essere in ritardo alla festa, ma questo dovrebbe funzionare indipendentemente da come avvii lo script e usi esclusivamente i builtin:

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

. Il comando (punto) è un alias di source , $PWDè il percorso per la directory di lavoro, BASH_SOURCEè una variabile di matrice i cui membri sono i nomi dei file di origine, ${string%substring}elimina la corrispondenza più breve di $ sottostringa dal retro di $ string


7
Questa è l'unica risposta nel thread che ha funzionato costantemente per me
Justin

3
@sacii Posso sapere quando è if [[ ! -d "$DIR" ]]; then DIR="$PWD"; finecessaria la linea ? Posso trovarne la necessità se i comandi vengono incollati su un prompt di bash per essere eseguiti. Tuttavia, se si esegue in un contesto di file di script, non riesco a vederne la necessità ...
Johnny Wong

2
Vale anche la pena notare che funziona come previsto su più sources (voglio dire, se sourceuno script sourceè un altro in un'altra directory e così via, funziona ancora).
Ciro Costa,

4
Non dovrebbe essere ${BASH_SOURCE[0]}come vuoi solo l'ultimo chiamato? Inoltre, l'utilizzo DIR=$(dirname ${BASH_SOURCE[0]})ti consentirà di sbarazzarti di if-condition
kshenoy

1
Sei disposto a rendere disponibile questo frammento con una licenza senza attributo richiesto, come CC0 o semplicemente a renderlo di dominio pubblico? Mi piacerebbe usare questo testualmente, ma è fastidioso mettere l'attribuzione in cima a ogni singolo script!
BeeOnRope,

52

Un'alternativa a:

scriptPath=$(dirname $0)

è:

scriptPath=${0%/*}

.. il vantaggio è di non avere la dipendenza da dirname, che non è un comando integrato (e non sempre disponibile negli emulatori)


2
basePath=$(dirname $0)mi ha dato un valore vuoto quando il file di script contenente è di provenienza.
prayagupd,

41

Se si trova nella stessa directory è possibile utilizzare dirname $0:

#!/bin/bash

source $(dirname $0)/incl.sh

echo "The main script"

2
Due insidie: 1) $0is ./t.she dirname ritorna .; 2) dopo che cd binil reso .non è corretto. $BASH_SOURCEnon è migliore.
18446744073709551615,

un altro inconveniente: provare questo con spazi nel nome della directory.
Hubert Grzeskowiak il

source "$(dirname $0)/incl.sh"funziona per questi casi
dsm,

27

Penso che il modo migliore per farlo sia usare la maniera di Chris Boran, MA dovresti calcolare MY_DIR in questo modo:

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

Per citare le pagine man per 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

Non ho mai riscontrato un caso d'uso in cui MY_DIRnon è stato calcolato correttamente. Se accedi al tuo script tramite un collegamento simbolico nel tuo $PATHfunziona.


Soluzione piacevole e semplice, e funziona in modo famoso per me in tutte le varianti di invocazione dello script che mi sono venute in mente. Grazie.
Brian Cline,

A parte i problemi con le virgolette mancanti, esiste un caso d'uso reale in cui si desidera risolvere i collegamenti simbolici anziché utilizzarli $0direttamente?
l0b0

1
@ l0b0: immagina che il tuo script sia /home/you/script.shpossibile cd /homeed esegui il tuo script da lì poiché ./you/script.shin questo caso dirname $0tornerà ./youe l'inclusione di altri script fallirà
dr.scre

1
Grande suggerimento, tuttavia, ho dovuto fare quanto segue per poter leggere le mie variabili in ` MY_DIR=$(dirname $(readlink -f $0)); source $MY_DIR/incl.sh
Frederick Ollinger

21

Una combinazione delle risposte a questa domanda fornisce la soluzione più solida.

Ha funzionato per noi negli script di livello di produzione con un grande supporto di dipendenze e struttura delle directory:

#! / Bin / bash

# Percorso completo dello script corrente
THIS = `readlink -f" $ {BASH_SOURCE [0]} "2> / dev / null || echo $ 0`

# La directory in cui risiede lo script corrente
DIR = `dirname" $ ​​{THIS} "`

# 'Punto' significa 'fonte', cioè 'include':
. "$ DIR / compile.sh"

Il metodo supporta tutti questi:

  • Spazi nel percorso
  • Link (via readlink)
  • ${BASH_SOURCE[0]} è più robusto di $0

1
QUESTO = readlink -f "${BASH_SOURCE[0]}" 2>/dev/null||echo $0 se il tuo readlink è BusyBox v1.01
Alexx Roche

@AlexxRoche grazie! Funzionerà su tutti i Linux?
Brian Haak,

1
Me lo aspetterei. Sembra funzionare su Debian Sid 3.16 e QNAP's armv5tel 3.4.6 Linux.
Alexx Roche,

2
L'ho inserito in questa riga: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
Ho il sospetto che tu abbia votato per il "cd ..." quando dirname "$ 0" dovrebbe realizzare la stessa cosa ...
Aaron H.

7
Questo codice restituirà il percorso assoluto anche quando lo script viene eseguito dalla directory corrente. $ (dirname "$ 0") da solo restituirà solo "."
Max

1
E si ./incl.shrisolve nello stesso percorso di cd+ pwd. Quindi qual è il vantaggio di cambiare la directory?
l0b0

A volte è necessario il percorso assoluto dello script, ad esempio quando si devono cambiare directory avanti e indietro.
Laryx Decidua,

15

Funziona anche se lo script è di provenienza:

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

Potresti spiegare qual è lo scopo di "[0]"?
Ray

1
@Ray BASH_SOURCEè una matrice di percorsi, corrispondente a uno stack di chiamate. Il primo elemento corrisponde allo script più recente nello stack, che è lo script attualmente in esecuzione. In realtà, $BASH_SOURCEchiamato come variabile si espande al suo primo elemento per impostazione predefinita, quindi [0]non è necessario qui. Vedi questo link per i dettagli.
Jonathan H,

10

1. Più pulito

Ho esplorato quasi tutti i suggerimenti ed ecco il più ordinato che ha funzionato per me:

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

Funziona anche quando lo script è collegato a una $PATHdirectory.

Guardalo in azione qui: https://github.com/pendashteh/hcagent/blob/master/bin/hcagent

2. Il più cool

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

Questo è in realtà da un'altra risposta in questa stessa pagina, ma lo sto aggiungendo anche alla mia risposta!

2. Il più affidabile

In alternativa, nel raro caso in cui quelli non funzionassero, ecco l'approccio a prova di proiettile:

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

Puoi vederlo in azione nella taskrunnerfonte: https://github.com/pendashteh/taskrunner/blob/master/bin/taskrunner

Spero che questo aiuti qualcuno là fuori :)

Inoltre, ti preghiamo di lasciarlo come commento se uno non ha funzionato per te e menzionare il tuo sistema operativo ed emulatore. Grazie!


7

Devi specificare la posizione degli altri script, non c'è altro modo per aggirarlo. Consiglierei una variabile configurabile nella parte superiore del tuo script:

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

. $installpath/incl.sh

echo "The main script"

In alternativa, puoi insistere sul fatto che l'utente mantenga una variabile di ambiente che indica dove si trova la home del tuo programma, come PROG_HOME o somesuch. Questo può essere fornito automaticamente all'utente creando uno script con tali informazioni in /etc/profile.d/, che verrà fornito ogni volta che un utente accede.


1
Apprezzo il desiderio di specificità, ma non riesco a capire perché dovrebbe essere richiesto il percorso completo a meno che gli script include non facessero parte di un altro pacchetto. Non vedo una differenza di sicurezza caricarsi da un percorso relativo specifico (ovvero la stessa directory in cui è in esecuzione lo script) rispetto a un percorso completo specifico. Perché dici che non c'è modo di aggirarlo?
Aaron H.

4
Perché la directory in cui viene eseguito lo script non è necessariamente dove si trovano gli script che si desidera includere nello script. Si desidera caricare gli script in cui sono installati e non esiste un modo affidabile per sapere dove si trova in fase di esecuzione. Non usare una posizione fissa è anche un buon modo per includere lo script sbagliato (cioè fornito dagli hacker) ed eseguirlo.
Steve Baker,

6

Suggerirei di creare uno script setenv il cui unico scopo è fornire posizioni per vari componenti nel sistema.

Tutti gli altri script avrebbero quindi origine questo script in modo che tutte le posizioni siano comuni a tutti gli script utilizzando lo script setenv.

Questo è molto utile quando si eseguono cronjobs. Si ottiene un ambiente minimo quando si esegue cron, ma se si rendono tutti gli script cron che includono prima lo script setenv, si è in grado di controllare e sincronizzare l'ambiente in cui si desidera eseguire cronjobs.

Abbiamo usato una tale tecnica sulla nostra scimmia di costruzione che è stata utilizzata per l'integrazione continua in un progetto di circa 2.000 kSLOC.


3

La risposta di Steve è sicuramente la tecnica corretta, ma dovrebbe essere rifattorizzata in modo che la variabile installpath sia in uno script di ambiente separato in cui vengono fatte tutte queste dichiarazioni.

Quindi tutti gli script generano tale script e dovrebbero cambiare il percorso di installazione, è necessario solo modificarlo in una posizione. Rende le cose più, ehm, a prova di futuro. Dio odio quella parola! (-:

A proposito, dovresti davvero fare riferimento alla variabile usando $ {installpath} quando la usi nel modo mostrato nel tuo esempio:

. ${installpath}/incl.sh

Se le parentesi graffe vengono lasciate fuori, alcune shell tenteranno di espandere la variabile "installpath / incl.sh"!


3

Shell Script Loader è la mia soluzione per questo.

Fornisce una funzione chiamata include () che può essere richiamata più volte in molti script per fare riferimento a un singolo script ma caricherà lo script una sola volta. La funzione può accettare percorsi completi o percorsi parziali (lo script viene cercato in un percorso di ricerca). Viene inoltre fornita una funzione simile denominata load () che carica incondizionatamente gli script.

Funziona con bash , ksh , pd ksh e zsh con script ottimizzati per ognuno di essi; e altre shell genericamente compatibili con lo sh originale come ash , dash , sh heirloom , ecc., attraverso uno script universale che ottimizza automaticamente le sue funzioni a seconda delle funzionalità che la shell può fornire.

[Esempio imminente]

start.sh

Questo è uno script iniziale opzionale. Inserire qui i metodi di avvio è solo una comodità e può essere inserito nello script principale. Questo script non è inoltre necessario se gli script devono essere compilati.

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

# ...

cenere

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

produzione:

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

La cosa migliore è che gli script basati su di esso possono anche essere compilati per formare un singolo script con il compilatore disponibile.

Ecco un progetto che lo utilizza: http://sourceforge.net/p/playshell/code/ci/master/tree/ . Può essere eseguito in modo portabile con o senza compilare gli script. La compilazione per produrre un singolo script può anche accadere ed è utile durante l'installazione.

Ho anche creato un prototipo più semplice per qualsiasi partito conservatore che potrebbe voler avere una breve idea di come funziona uno script di implementazione: https://sourceforge.net/p/loader/code/ci/base/tree/loader-include-prototype .bash . È piccolo e chiunque può semplicemente includere il codice nel proprio script principale se lo desidera se il suo codice è destinato a funzionare con Bash 4.0 o versioni successive e inoltre non lo utilizza eval.


3
12 kilobyte di script Bash contenenti oltre 100 righe di evalcodice ed per caricare dipendenze. Ahi
l0b0

1
Esattamente uno dei tre evalblocchi vicino al fondo viene sempre eseguito. Quindi, sia che sia necessario o meno, è sicuramente in uso eval.
l0b0

3
Quella chiamata di valutazione è sicura e non viene utilizzata se si dispone di Bash 4.0+. Vedo, sei uno di quegli scripter di altri tempi che pensano che evalsia puro male e che invece non sappia farne buon uso.
konsolebox

1
Non so cosa intendi per "non utilizzato", ma è eseguito. E dopo alcuni anni di scripting di shell come parte del mio lavoro, sì, sono più convinto che mai che evalsia malvagio.
l0b0

1
Vengono immediatamente in mente due semplici modi portatili per contrassegnare i file: riferirsi ad essi tramite il loro numero di inode o inserendo percorsi separati da NUL in un file.
l0b0

2

Metti personalmente tutte le librerie in una libcartella e usa una importfunzione per caricarle.

struttura delle cartelle

inserisci qui la descrizione dell'immagine

script.sh Contenuti

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

Nota che questa funzione di importazione dovrebbe essere all'inizio del tuo script e quindi puoi facilmente importare le tue librerie in questo modo:

import "utils"
import "requirements"

Aggiungi una sola riga nella parte superiore di ogni libreria (ad esempio utils.sh):

IMPORTED="$BASH_SOURCE"

Ora hai accesso alle funzioni all'interno utils.she requirements.shdascript.sh

TODO: scrivere un linker per creare un singolo shfile


Questo risolve anche il problema dell'esecuzione dello script al di fuori della directory in cui si trova?
Aaron H.

@AaronH. No. È un modo strutturato per includere le dipendenze in grandi progetti.
Xaqron,

1

L'uso di source o $ 0 non ti darà il vero percorso del tuo script. È possibile utilizzare l'id di processo dello script per recuperare il suo percorso reale

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

Sto usando questo script e mi è sempre servito bene :)


1

Ho messo tutti i miei script di avvio in una directory .bashrc.d. Questa è una tecnica comune in luoghi come /etc/profile.d, ecc.

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

Il problema con la soluzione utilizzando globbing ...

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

... è che potresti avere un elenco di file "troppo lungo". Un approccio come ...

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

... funziona ma non cambia l'ambiente come desiderato.


1

Certo, a ciascuno il suo, ma penso che il blocco sottostante sia piuttosto solido. Credo che ciò implichi il modo "migliore" per trovare una directory e il modo "migliore" per chiamare un altro script bash:

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

echo "The main script"

Quindi questo potrebbe essere il modo "migliore" per includere altri script. Questo si basa su un'altra "migliore" risposta che dice a uno script bash dove è memorizzato


1

Questo dovrebbe funzionare in modo affidabile:

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

source_relative incl.sh

0

dobbiamo solo scoprire la cartella in cui sono memorizzati i nostri incl.sh e main.sh; basta cambiare main.sh con questo:

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"

Errato citando, uso non necessario di echo, e un uso non corretto di sed's gopzione. -1.
l0b0

Ad ogni modo, per far funzionare questa sceneggiatura fai: SCRIPT_DIR=$(echo "$0" | sed "s/${SCRIPT_NAME}//")e poisource "${SCRIPT_DIR}incl.sh"
Krzysiek il

0

Secondo il man hierluogo adatto per lo script include è/usr/local/lib/

/ Usr / local / lib

File associati a programmi installati localmente.

Personalmente preferisco /usr/local/lib/bash/includesper include. Esiste una libreria bash-helper per includere le librerie in quel modo:

#!/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 include l'output dello stato


-5

Puoi anche usare:

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

9
Supponi di trovarti nella stessa directory in cui si trovano gli script. Non funzionerà se ti trovi altrove.
Luc M,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.