Comment inclure au mieux d'autres scripts?

353

La façon dont vous incluez normalement un script est avec "source"

par exemple:

main.sh:

#!/bin/bash

source incl.sh

echo "The main script"

y compris sh:

echo "The included script"

Le résultat de l'exécution de "./main.sh" est:

The included script
The main script

... Maintenant, si vous essayez d'exécuter ce script shell à partir d'un autre emplacement, il ne peut pas trouver l'inclusion à moins qu'il ne se trouve sur votre chemin.

Quelle est la bonne façon de s'assurer que votre script peut trouver le script include, surtout si, par exemple, le script doit être portable?

Aaron H.
la source
2
voir celui-ci: stackoverflow.com/questions/59895/…
Lluís
1
Votre question est si bonne et si informative que ma question a été répondue avant même que vous ne la posiez! Bon travail!
Gabriel Staples le

Réponses:

229

J'ai tendance à faire en sorte que mes scripts soient tous relatifs les uns aux autres. De cette façon, je peux utiliser dirname:

#!/bin/sh

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

"$my_dir/other_script.sh"
Chris Boran
la source
5
Cela ne fonctionnera pas si le script est exécuté via $ PATH. puis which $0sera utile
Hugo
41
Il n'y a aucun moyen fiable de déterminer l'emplacement d'un script shell, voir mywiki.wooledge.org/BashFAQ/028
Philipp
12
@Philipp, L'auteur de cette entrée est correct, c'est complexe et il y a des pièges. Mais il manque certains points clés, premièrement, l'auteur suppose beaucoup de choses sur ce que vous allez faire avec votre script bash. Je ne m'attendrais pas à ce qu'un script python s'exécute sans ses dépendances non plus. Bash est un langage de collage qui vous permet de faire des choses rapidement qui seraient difficiles autrement. Lorsque vous avez besoin que votre système de build fonctionne, le pragmatisme (et un bel avertissement sur le script ne pouvant pas trouver de dépendances) gagne.
Aaron H.
11
Je viens d'apprendre le BASH_SOURCEtableau et comment le premier élément de ce tableau pointe toujours vers la source actuelle.
haridsv
6
Cela ne fonctionnera pas si les scripts sont à des emplacements différents. par exemple /home/me/main.sh appelle /home/me/test/inc.sh comme dirname retournera / home / me. sacii answer using BASH_SOURCE is a better solution stackoverflow.com/a/12694189/1000011
opticyclic
187

Je sais que je suis en retard à la fête, mais cela devrait fonctionner, peu importe la façon dont vous démarrez le script et utilisez exclusivement les commandes intégrées:

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

.La commande (dot) est un alias de source, $PWDest le chemin d'accès au répertoire de travail, BASH_SOURCEest une variable de tableau dont les membres sont les noms de fichiers source, ${string%substring}supprime la correspondance la plus courte de $ substring à l'arrière de $ string

sacii
la source
7
C'est la seule réponse dans le fil qui a toujours fonctionné pour moi
Justin
3
@sacii Puis-je savoir quand la ligne est if [[ ! -d "$DIR" ]]; then DIR="$PWD"; finécessaire? Je peux en trouver le besoin si les commandes sont collées dans une invite bash à exécuter. Cependant, si je cours dans un contexte de fichier de script, je ne vois pas la nécessité de le faire ...
Johnny Wong
2
Il convient également de noter que cela fonctionne comme prévu sur plusieurs sources (je veux dire, si vous sourceun script qui en sourceest un autre dans un autre répertoire et ainsi de suite, cela fonctionne toujours).
Ciro Costa
4
Cela ne devrait-il pas être ${BASH_SOURCE[0]}comme vous ne voulez que le dernier appelé? De plus, l'utilisation DIR=$(dirname ${BASH_SOURCE[0]})vous permettra de vous débarrasser de la condition
if
1
Êtes-vous prêt à rendre cet extrait disponible sous une licence sans attribut requis, comme CC0 ou à le publier dans le domaine public? Je voudrais utiliser ce verbatim mais c'est ennuyeux de mettre l'attribution en haut de chaque script!
BeeOnRope
52

Une alternative à:

scriptPath=$(dirname $0)

est:

scriptPath=${0%/*}

.. l'avantage étant de ne pas avoir la dépendance de dirname, qui n'est pas une commande intégrée (et pas toujours disponible dans les émulateurs)

tardive
la source
2
basePath=$(dirname $0)m'a donné une valeur vide lorsque le fichier de script contenant est d'origine.
Prayagupd
41

S'il se trouve dans le même répertoire, vous pouvez utiliser dirname $0:

#!/bin/bash

source $(dirname $0)/incl.sh

echo "The main script"
dsm
la source
2
Deux pièges: 1) $0is ./t.shet dirname retournent .; 2) après cd binle retour .n'est pas correct. $BASH_SOURCEn'est pas mieux.
18446744073709551615
un autre écueil: essayez ceci avec des espaces dans le nom du répertoire.
Hubert Grzeskowiak
source "$(dirname $0)/incl.sh"fonctionne pour ces cas
dsm
27

Je pense que la meilleure façon de le faire est d'utiliser la méthode de Chris Boran, MAIS vous devriez calculer MY_DIR de cette façon:

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

Pour citer les pages de manuel de 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

Je n'ai jamais rencontré de cas d'utilisation où il MY_DIRn'est pas correctement calculé. Si vous accédez à votre script via un lien symbolique dans votre, $PATHcela fonctionne.

Mat131
la source
Solution agréable et simple, et qui fonctionne pour moi dans autant de variantes d'invocation de script que je pouvais penser. Merci.
Brian Cline
Mis à part les problèmes avec les guillemets manquants, existe-t-il un cas d'utilisation réel où vous voudriez résoudre les liens symboliques plutôt que d'utiliser $0directement?
l0b0
1
@ l0b0: Imaginez que votre script est /home/you/script.shVous pouvez cd /homeet exécutez votre script à partir de là car ./you/script.shdans ce cas, dirname $0il reviendra ./youet l'inclusion d'un autre script échouera
dr.scre
1
Grande suggestion, cependant, j'avais besoin de faire ce qui suit pour qu'il puisse lire mes variables dans ` MY_DIR=$(dirname $(readlink -f $0)); source $MY_DIR/incl.sh
Frederick Ollinger
21

Une combinaison des réponses à cette question fournit la solution la plus robuste.

Cela a fonctionné pour nous dans les scripts de production avec un grand support des dépendances et de la structure des répertoires:

#! / bin / bash

# Chemin complet du script actuel
CECI = `readlink -f" $ {BASH_SOURCE [0]} "2> / dev / null || echo $ 0`

# Le répertoire où réside le script actuel
DIR = `dirname" $ ​​{THIS} "`

# 'Dot' signifie 'source', c'est-à-dire 'include':
. "$ DIR / compile.sh"

La méthode prend en charge tous ces éléments:

  • Espaces dans le chemin
  • Liens (via readlink)
  • ${BASH_SOURCE[0]} est plus robuste que $0
Brian Haak
la source
1
CECI = readlink -f "${BASH_SOURCE[0]}" 2>/dev/null||echo $0 si votre lien de lecture est BusyBox v1.01
Alexx Roche
@AlexxRoche merci! Est-ce que cela fonctionnera sur tous les Linux?
Brian Haak
1
Je m'y attendrais. Semble fonctionner sur Debian Sid 3.16 et armv5tel 3.4.6 Linux de QNAP.
Alexx Roche
2
Je l'ai mis dans cette seule ligne: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"
Max
la source
1
Je soupçonne que vous avez voté pour le "cd ..." alors que dirname "$ 0" devrait accomplir la même chose ...
Aaron H.
7
Ce code renverra le chemin absolu même lorsque le script est exécuté à partir du répertoire courant. $ (dirname "$ 0") seul renverra juste "."
Max
1
Et ./incl.shrésout le même chemin que cd+ pwd. Alors, quel est l'avantage de changer de répertoire?
l0b0
Parfois, vous avez besoin du chemin absolu du script, par exemple lorsque vous devez changer de répertoire dans les deux sens.
Laryx Decidua
15

Cela fonctionne même si le script provient:

source "$( dirname "${BASH_SOURCE[0]}" )/incl.sh"
Alessandro Pezzato
la source
Pourriez-vous expliquer à quoi sert le "[0]"?
Ray
1
@Ray BASH_SOURCEest un tableau de chemins, correspondant à une pile d'appels. Le premier élément correspond au script le plus récent de la pile, qui est le script en cours d'exécution. En fait, $BASH_SOURCEappelé comme variable se développe par défaut vers son premier élément, il [0]n'est donc pas nécessaire ici. Voir ce lien pour plus de détails.
Jonathan H
10

1. Le plus net

J'ai exploré presque toutes les suggestions et voici la plus intéressante qui a fonctionné pour moi:

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

Cela fonctionne même lorsque le script est lié à un $PATHrépertoire.

Voyez-le en action ici: https://github.com/pendashteh/hcagent/blob/master/bin/hcagent

2. Le plus cool

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

C'est en fait d'une autre réponse sur cette même page, mais je l'ajoute aussi à ma réponse!

2. Le plus fiable

Alternativement, dans les rares cas où ceux-ci n'ont pas fonctionné, voici l'approche à l'épreuve des balles:

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

Vous pouvez le voir en action dans la taskrunnersource: https://github.com/pendashteh/taskrunner/blob/master/bin/taskrunner

J'espère que cela aidera quelqu'un là-bas :)

Veuillez également le laisser en commentaire si l'un ne vous convient pas et mentionner votre système d'exploitation et votre émulateur. Merci!

Alexar
la source
7

Vous devez spécifier l'emplacement des autres scripts, il n'y a pas d'autre moyen de le contourner. Je recommanderais une variable configurable en haut de votre script:

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

. $installpath/incl.sh

echo "The main script"

Alternativement, vous pouvez insister pour que l'utilisateur maintienne une variable d'environnement indiquant où se trouve la page d'accueil de votre programme, comme PROG_HOME ou somesuch. Cela peut être fourni automatiquement à l'utilisateur en créant un script avec ces informations dans /etc/profile.d/, qui seront obtenues à chaque fois qu'un utilisateur se connecte.

Steve Baker
la source
1
J'apprécie le désir de spécificité, mais je ne vois pas pourquoi le chemin complet devrait être requis à moins que les scripts d'inclusion fassent partie d'un autre package. Je ne vois pas de différence de sécurité se charger à partir d'un chemin relatif spécifique (c'est-à-dire le même répertoire où le script s'exécute.) Vs un chemin complet spécifique. Pourquoi dites-vous qu'il n'y a aucun moyen de contourner cela?
Aaron H.
4
Parce que le répertoire dans lequel votre script s'exécute n'est pas nécessairement où se trouvent les scripts que vous souhaitez inclure dans votre script. Vous souhaitez charger les scripts là où ils sont installés et il n'existe aucun moyen fiable de dire où cela se trouve au moment de l'exécution. Ne pas utiliser un emplacement fixe est également un bon moyen d'inclure le mauvais script (c'est-à-dire fourni par un pirate) et de l'exécuter.
Steve Baker du
6

Je vous suggère de créer un script setenv dont le seul but est de fournir des emplacements pour divers composants sur votre système.

Tous les autres scripts fourniraient alors ce script afin que tous les emplacements soient communs à tous les scripts utilisant le script setenv.

Ceci est très utile lors de l'exécution de cronjobs. Vous obtenez un environnement minimal lors de l'exécution de cron, mais si vous faites d'abord en sorte que tous les scripts cron incluent le script setenv, vous pouvez contrôler et synchroniser l'environnement dans lequel vous voulez que les cronjobs s'exécutent.

Nous avons utilisé une telle technique sur notre singe de construction qui a été utilisée pour une intégration continue à travers un projet d'environ 2 000 kSLOC.

Rob Wells
la source
3

La réponse de Steve est certainement la bonne technique mais elle devrait être refactorisée afin que votre variable installpath soit dans un script d'environnement séparé où toutes ces déclarations sont faites.

Ensuite, tous les scripts source ce script et doivent changer de chemin d'installation, il vous suffit de le changer dans un seul emplacement. Rend les choses plus, euh, à l'épreuve du temps. Dieu que je déteste ce mot! (-:

BTW Vous devriez vraiment vous référer à la variable en utilisant $ {installpath} lorsque vous l'utilisez de la manière indiquée dans votre exemple:

. ${installpath}/incl.sh

Si les accolades sont omises, certains shells essaieront et développeront la variable "installpath / incl.sh"!

Rob Wells
la source
3

Shell Script Loader est ma solution pour cela.

Il fournit une fonction nommée include () qui peut être appelée plusieurs fois dans de nombreux scripts pour faire référence à un seul script mais ne chargera le script qu'une seule fois. La fonction peut accepter des chemins complets ou des chemins partiels (le script est recherché dans un chemin de recherche). Une fonction similaire nommée load () est également fournie qui chargera les scripts sans condition.

Il fonctionne pour bash , ksh , pd ksh et zsh avec des scripts optimisés pour chacun d'eux; et d'autres shells qui sont génériquement compatibles avec le sh d'origine comme ash , dash , heirloom sh , etc., grâce à un script universel qui optimise automatiquement ses fonctions en fonction des fonctionnalités que le shell peut fournir.

[Exemple présenté]

start.sh

Il s'agit d'un script de démarrage facultatif. Placer les méthodes de démarrage ici n'est qu'une commodité et peut être placé à la place dans le script principal. Ce script n'est également pas nécessaire si les scripts doivent être compilés.

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

# ...

cendre

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

production:

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

Ce qui est le mieux, c'est que les scripts basés dessus peuvent également être compilés pour former un seul script avec le compilateur disponible.

Voici un projet qui l'utilise: http://sourceforge.net/p/playshell/code/ci/master/tree/ . Il peut s'exécuter de manière portable avec ou sans compilation des scripts. La compilation pour produire un seul script peut également se produire et est utile lors de l'installation.

J'ai également créé un prototype plus simple pour toute partie conservatrice qui voudrait avoir une brève idée du fonctionnement d'un script d'implémentation: https://sourceforge.net/p/loader/code/ci/base/tree/loader-include-prototype .bash . Il est petit et n'importe qui peut simplement inclure le code dans son script principal s'il le souhaite si son code est destiné à fonctionner avec Bash 4.0 ou plus récent, et il ne l'utilise pas non plus eval.

konsolebox
la source
3
12 kilo-octets de script Bash contenant plus de 100 lignes de evalcode ed pour charger les dépendances. Ouch
l0b0
1
Exactement l'un des trois evalblocs près du bas est toujours exécuté. Donc, que ce soit nécessaire ou non, il utilise certainement eval.
l0b0
3
Cet appel eval est sûr et il n'est pas utilisé si vous avez Bash 4.0+. Je vois, vous êtes l'un de ces scénaristes d'autrefois qui pensent que evalc'est du mal pur, et ne sait pas comment en faire bon usage à la place.
konsolebox
1
Je ne sais pas ce que vous entendez par "non utilisé", mais il est exécuté. Et après quelques années de scripts shell dans le cadre de mon travail, oui, je suis plus convaincu que jamais que evalc'est mal.
l0b0
1
Deux façons simples et portables de signaler les fichiers vous viennent instantanément à l'esprit: soit en vous y référant par leur numéro d'inode, soit en plaçant des chemins séparés par NUL dans un fichier.
l0b0
2

Mettez personnellement toutes les bibliothèques dans un libdossier et utilisez une importfonction pour les charger.

structure des dossiers

entrez la description de l'image ici

script.sh Contenu

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

Notez que cette fonction d'importation devrait être au début de votre script et que vous pouvez ensuite facilement importer vos bibliothèques comme ceci:

import "utils"
import "requirements"

Ajoutez une seule ligne en haut de chaque bibliothèque (ie utils.sh):

IMPORTED="$BASH_SOURCE"

Vous avez maintenant accès aux fonctions à l'intérieur utils.shet à requirements.shpartir descript.sh

TODO: Écrivez un éditeur de liens pour créer un seul shfichier

Xaqron
la source
Est-ce que cela résout également le problème de l'exécution du script en dehors du répertoire dans lequel il se trouve?
Aaron H.
@AaronH. Non. C'est une manière structurée d'inclure des dépendances dans de grands projets.
Xaqron
1

Utiliser source ou $ 0 ne vous donnera pas le vrai chemin de votre script. Vous pouvez utiliser l'ID de processus du script pour récupérer son vrai chemin

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

J'utilise ce script et il m'a toujours bien servi :)

francoisrv
la source
1

J'ai mis tous mes scripts de démarrage dans un répertoire .bashrc.d. Il s'agit d'une technique courante dans des endroits tels que /etc/profile.d, etc.

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

Le problème avec la solution utilisant le globbing ...

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

... vous avez peut-être une liste de fichiers "trop ​​longue". Une approche comme ...

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

... s'exécute mais ne change pas l'environnement comme vous le souhaitez.

Phreed
la source
1

Bien sûr, à chacun, mais je pense que le bloc ci-dessous est assez solide. Je crois que cela implique la "meilleure" façon de trouver un répertoire et la "meilleure" façon d'appeler un autre script bash:

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

echo "The main script"

C'est donc peut-être la "meilleure" façon d'inclure d'autres scripts. Ceci est basé sur une autre "meilleure" réponse qui indique à un script bash où il est stocké

modulitos
la source
1

Cela devrait fonctionner de manière fiable:

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

source_relative incl.sh
PSkocik
la source
0

nous avons juste besoin de trouver le dossier où nos incl.sh et main.sh sont stockés; changez simplement votre main.sh avec ceci:

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
la source
Devis incorrect, utilisation inutile echoet utilisation incorrecte de sedl' goption de. -1.
l0b0
Quoi qu'il en soit, pour faire fonctionner ce script, faites: SCRIPT_DIR=$(echo "$0" | sed "s/${SCRIPT_NAME}//")puissource "${SCRIPT_DIR}incl.sh"
Krzysiek
0

L' man hierendroit approprié pour le script comprend/usr/local/lib/

/ usr / local / lib

Fichiers associés aux programmes installés localement.

Personnellement, je préfère les /usr/local/lib/bash/includesinclus. Il existe une bibliothèque bash-helper pour inclure des bibliothèques de cette manière:

#!/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 inclut une sortie d'état

Alexander Yancharuk
la source
-5

Vous pouvez aussi utiliser:

PWD=$(pwd)
source "$PWD/inc.sh"
Django
la source
9
Vous supposez que vous êtes dans le même répertoire où se trouvent les scripts. Cela ne fonctionnera pas si vous êtes ailleurs.
Luc M