¿Cuál es la mejor manera de incluir otros scripts?

353

La forma en que normalmente incluirías un script es con "fuente"

p.ej:

main.sh:

#!/bin/bash

source incl.sh

echo "The main script"

incluido sh:

echo "The included script"

El resultado de ejecutar "./main.sh" es:

The included script
The main script

... Ahora, si intenta ejecutar ese script de shell desde otra ubicación, no puede encontrar la inclusión a menos que esté en su ruta.

¿Cuál es una buena manera de asegurarse de que su script pueda encontrar el script de inclusión, especialmente si, por ejemplo, el script necesita ser portátil?

Aaron H.
fuente
1
¡Su pregunta es tan buena e informativa que mi pregunta fue respondida incluso antes de que la hiciera ! ¡Buen trabajo!
Gabriel Staples

Respuestas:

229

Tiendo a hacer que todos mis guiones sean relativos entre sí. De esa manera puedo usar dirname:

#!/bin/sh

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

"$my_dir/other_script.sh"
Chris Boran
fuente
55
Esto no funcionará si el script se ejecuta a través de $ PATH. entonces which $0será útil
Hugo
41
No hay una manera confiable de determinar la ubicación de un script de shell, vea mywiki.wooledge.org/BashFAQ/028
Philipp
12
@Philipp, el autor de esa entrada es correcto, es complejo y hay problemas. Pero le faltan algunos puntos clave, primero, el autor asume muchas cosas sobre lo que va a hacer con su script bash. No esperaría que un script de Python se ejecute sin sus dependencias tampoco. Bash es un lenguaje adhesivo que te permite hacer cosas rápidamente que de otra forma sería difícil. Cuando necesita que su sistema de compilación funcione, gana el pragmatismo (y una buena advertencia acerca de que el script no puede encontrar dependencias).
Aaron H.
11
Acabo de aprender sobre la BASH_SOURCEmatriz y cómo el primer elemento de esta matriz siempre apunta a la fuente actual.
haridsv
66
Esto no funcionará si los scripts son ubicaciones diferentes. Por ejemplo, /home/me/main.sh llama a /home/me/test/inc.sh ya que dirname devolverá / home / me. sacii answer usando BASH_SOURCE es una mejor solución stackoverflow.com/a/12694189/1000011
opticyclic
187

Sé que llego tarde a la fiesta, pero esto debería funcionar sin importar cómo inicies el script y use exclusivamente los builtins:

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

.El comando (punto) es un alias para source, $PWDes la ruta para el directorio de trabajo, BASH_SOURCEes una variable de matriz cuyos miembros son los nombres de los archivos de origen, las ${string%substring}tiras coinciden más cortas de $ substring desde la parte posterior de $ string

sacii
fuente
77
Esta es la única respuesta en el hilo que siempre funcionó para mí
Justin
3
@sacii ¿Puedo saber cuándo se if [[ ! -d "$DIR" ]]; then DIR="$PWD"; finecesita la línea ? Puedo encontrar una necesidad si los comandos se pegan en un indicador de bash para que se ejecute. Sin embargo, si se ejecuta dentro de un contexto de archivo de script, no puedo ver la necesidad de hacerlo ...
Johnny Wong
2
También vale la pena señalar que funciona como se espera en varios correos electrónicos source(es decir, si sourceun script sourcees otro en otro directorio, etc., todavía funciona).
Ciro Costa
44
¿No debería ser esto ${BASH_SOURCE[0]}como solo quieres la última llamada? Además, el uso DIR=$(dirname ${BASH_SOURCE[0]})le permitirá deshacerse de la condición
if
1
¿Está dispuesto a hacer que este fragmento esté disponible bajo una licencia sin atributo requerido, como CC0 o simplemente liberarlo en el dominio público? ¡Me gustaría usar esta palabra, pero es molesto poner la atribución en la parte superior de cada script!
BeeOnRope
52

Una alternativa a:

scriptPath=$(dirname $0)

es:

scriptPath=${0%/*}

.. la ventaja es que no depende de dirname, que no es un comando incorporado (y no siempre está disponible en emuladores)

tarda
fuente
2
basePath=$(dirname $0)me dio un valor en blanco cuando se obtiene el archivo de script que contiene.
prayagupd
41

Si está en el mismo directorio, puede usar dirname $0:

#!/bin/bash

source $(dirname $0)/incl.sh

echo "The main script"
dsm
fuente
2
Dos trampas: 1) $0es ./t.shy retornos dirname .; 2) después de que cd binla devolución .no sea correcta. $BASH_SOURCENo es mejor.
18446744073709551615
otra trampa: intente esto con espacios en el nombre del directorio.
Hubert Grzeskowiak el
source "$(dirname $0)/incl.sh"funciona para esos casos
dsm
27

Creo que la mejor manera de hacer esto es usar la forma de Chris Boran, PERO debes calcular MY_DIR de esta manera:

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

Para citar las páginas del manual para 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

Nunca he encontrado un caso de uso donde MY_DIRno se calcule correctamente. Si accede a su script a través de un enlace simbólico en su $PATH, funciona.

Mat131
fuente
Solución agradable y simple, y funciona famoso para mí en tantas variaciones de invocación de script que se me ocurran. Gracias.
Brian Cline
Además de los problemas con las comillas faltantes, ¿hay algún caso de uso real en el que desee resolver los enlaces simbólicos en lugar de usarlos $0directamente?
l0b0
1
@ l0b0: Imagine que su secuencia de comandos es /home/you/script.shposible cd /homey ejecute su secuencia de comandos desde allí, ya que ./you/script.shen este caso dirname $0volverá ./youe incluirá otra secuencia de comandos que fallará
dr.scre
1
Gran sugerencia, sin embargo, necesitaba hacer lo siguiente para poder leer mis variables en ` MY_DIR=$(dirname $(readlink -f $0)); source $MY_DIR/incl.sh
Frederick Ollinger
21

Una combinación de las respuestas a esta pregunta proporciona la solución más sólida.

Funcionó para nosotros en scripts de grado de producción con gran soporte de dependencias y estructura de directorios:

#! / bin / bash

# Ruta completa del script actual
THIS = `readlink -f" $ {BASH_SOURCE [0]} "2> / dev / null || echo $ 0`

# El directorio donde reside el script actual
DIR = `dirname" $ ​​{THIS} "`

# 'Punto' significa 'fuente', es decir, 'incluir':
. "$ DIR / compile.sh"

El método admite todos estos:

  • Espacios en camino
  • Enlaces (vía readlink)
  • ${BASH_SOURCE[0]} es más robusto que $0
Brian Haak
fuente
1
THIS = readlink -f "${BASH_SOURCE[0]}" 2>/dev/null||echo $0 si su readlink es BusyBox v1.01
Alexx Roche
@AlexxRoche gracias! ¿Funcionará esto en todos los Linux?
Brian Haak
1
Yo esperaría que sí. Parece funcionar en Debian Sid 3.16 y el armv5tel 3.4.6 Linux de QNAP.
Alexx Roche
2
Lo puse en esta línea: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
fuente
1
Sospecho que te votaron por el "cd ..." cuando el nombre del directorio "$ 0" debería lograr lo mismo ...
Aaron H.
77
Este código devolverá la ruta absoluta incluso cuando el script se ejecute desde el directorio actual. $ (dirname "$ 0") solo devolverá solo "."
Max
1
Y se ./incl.shresuelve en la misma ruta que cd+ pwd. Entonces, ¿cuál es la ventaja de cambiar el directorio?
l0b0
A veces necesita la ruta absoluta del script, por ejemplo, cuando tiene que cambiar los directorios de un lado a otro.
Laryx Decidua
15

Esto funciona incluso si el script tiene su origen:

source "$( dirname "${BASH_SOURCE[0]}" )/incl.sh"
Alessandro Pezzato
fuente
¿Podría explicar cuál es el propósito de "[0]"?
Ray
1
@Ray BASH_SOURCEes una matriz de rutas, que corresponde a una pila de llamadas. El primer elemento corresponde al script más reciente en la pila, que es el script que se está ejecutando actualmente. En realidad, $BASH_SOURCEllamado como una variable se expande a su primer elemento por defecto, por [0]lo que no es necesario aquí. Vea este enlace para más detalles.
Jonathan H
10

1. Neatest

Exploré casi todas las sugerencias y aquí está la más bonita que me funcionó:

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

Funciona incluso cuando el script está vinculado a un $PATHdirectorio.

Véalo en acción aquí: https://github.com/pendashteh/hcagent/blob/master/bin/hcagent

2. El más genial

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

Esto es en realidad de otra respuesta en esta misma página, ¡pero también lo agregaré a mi respuesta!

2. El más confiable

Alternativamente, en el raro caso de que no funcionó, aquí está el enfoque a prueba de balas:

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

Puedes verlo en acción en la taskrunnerfuente: https://github.com/pendashteh/taskrunner/blob/master/bin/taskrunner

Espero que esto ayude a alguien por ahí :)

Además, déjelo como comentario si alguno no funcionó para usted y mencione su sistema operativo y emulador. ¡Gracias!

Alexar
fuente
7

Debe especificar la ubicación de los otros scripts, no hay otra forma de evitarlo. Recomiendo una variable configurable en la parte superior de su script:

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

. $installpath/incl.sh

echo "The main script"

Alternativamente, puede insistir en que el usuario mantenga una variable de entorno que indique dónde se encuentra el inicio de su programa, como PROG_HOME o somesuch. Esto se puede proporcionar al usuario automáticamente creando un script con esa información en /etc/profile.d/, que se obtendrá cada vez que un usuario inicie sesión.

Steve Baker
fuente
1
Aprecio el deseo de especificidad, pero no puedo ver por qué debería requerirse la ruta completa a menos que los scripts de inclusión fueran parte de otro paquete. No veo una carga de diferencia de seguridad desde una ruta relativa específica (es decir, el mismo directorio donde se ejecuta el script) frente a una ruta completa específica. ¿Por qué dices que no hay forma de evitarlo?
Aaron H.
44
Debido a que el directorio donde se ejecuta su secuencia de comandos no es necesariamente donde se encuentran las secuencias de comandos que desea incluir en su secuencia de comandos. Desea cargar los scripts donde están instalados y no hay una manera confiable de saber dónde está eso en tiempo de ejecución. No usar una ubicación fija también es una buena manera de incluir el script incorrecto (es decir, suministrado por el hacker) y ejecutarlo.
Steve Baker,
6

Le sugiero que cree un script setenv cuyo único propósito sea proporcionar ubicaciones para varios componentes en su sistema.

Todas las demás secuencias de comandos generarían esta secuencia de comandos para que todas las ubicaciones sean comunes en todas las secuencias de comandos que utilizan la secuencia de comandos setenv.

Esto es muy útil cuando se ejecutan cronjobs. Obtiene un entorno mínimo cuando ejecuta cron, pero si hace que todos los scripts cron incluyan primero el script setenv, podrá controlar y sincronizar el entorno en el que desea que se ejecuten los cronjobs.

Utilizamos nuestra técnica en nuestro mono de construcción que se utilizó para la integración continua en un proyecto de aproximadamente 2,000 kSLOC.

Rob Wells
fuente
3

La respuesta de Steve es definitivamente la técnica correcta, pero debe ser refactorizada para que su variable installpath esté en un script de entorno separado donde se realicen todas esas declaraciones.

Luego, todas las secuencias de comandos obtienen esa secuencia de comandos y deberían instalarse cambios de ruta, solo necesita cambiarla en una ubicación. Hace las cosas más, er, a prueba de futuro. Dios, odio esa palabra! (-:

Por cierto, debería referirse a la variable usando $ {installpath} cuando la use de la manera que se muestra en su ejemplo:

. ${installpath}/incl.sh

Si se dejan las llaves, algunos shells intentarán expandir la variable "installpath / incl.sh".

Rob Wells
fuente
3

Shell Script Loader es mi solución para esto.

Proporciona una función llamada include () que se puede invocar muchas veces en muchos scripts para referir un solo script, pero solo cargará el script una vez. La función puede aceptar rutas completas o rutas parciales (el script se busca en una ruta de búsqueda). También se proporciona una función similar llamada load () que cargará los scripts incondicionalmente.

Funciona para bash , ksh , pd ksh y zsh con scripts optimizados para cada uno de ellos; y otros shells que son genéricamente compatibles con el sh original como ash , dash , heirloom sh , etc., a través de un script universal que optimiza automáticamente sus funciones dependiendo de las características que el shell pueda proporcionar.

[Ejemplo de Fowarded]

start.sh

Este es un script de inicio opcional. Colocar los métodos de inicio aquí es solo una conveniencia y se puede colocar en el script principal. Este script tampoco es necesario si se van a compilar los scripts.

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

# ...

ceniza

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

salida:

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

Lo mejor es que los scripts basados ​​en él también pueden compilarse para formar un solo script con el compilador disponible.

Aquí hay un proyecto que lo usa: http://sourceforge.net/p/playshell/code/ci/master/tree/ . Puede ejecutarse de forma portátil con o sin compilar los scripts. Compilar para producir un solo script también puede suceder, y es útil durante la instalación.

También creé un prototipo más simple para cualquier partido conservador que quiera tener una breve idea de cómo funciona un script de implementación: https://sourceforge.net/p/loader/code/ci/base/tree/loader-include-prototype .bash . Es pequeño y cualquiera puede incluir el código en su script principal si lo desea, si su código está destinado a ejecutarse con Bash 4.0 o posterior, y tampoco lo usa eval.

konsolebox
fuente
3
12 kilobytes de script Bash que contiene más de 100 líneas de evalcódigo ed para cargar dependencias. Ouch
l0b0
1
Exactamente uno de los tres evalbloques cerca del fondo siempre se ejecuta. Entonces, ya sea que sea necesario o no, seguro lo está usando eval.
l0b0
3
Esa llamada de evaluación es segura y no se usa si tiene Bash 4.0+. Ya veo, usted es uno de esos scripters de los viejos tiempos que piensan que evales pura maldad, y no sabe cómo usarlo bien.
konsolebox
1
No sé qué quieres decir con "no usado", pero se ejecuta. Y después de algunos años de scripts de shell como parte de mi trabajo, sí, estoy más convencido que nunca de que eso evales malo.
l0b0
1
Dos formas simples y portátiles de marcar archivos vienen a la mente al instante: ya sea refiriéndose a ellos por su número de inodo o colocando rutas separadas por NUL en un archivo.
l0b0
2

Ponga personalmente todas las bibliotecas en una libcarpeta y use una importfunción para cargarlas.

estructura de carpetas

ingrese la descripción de la imagen aquí

script.sh contenido

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

Tenga en cuenta que esta función de importación debe estar al comienzo de su script y luego puede importar fácilmente sus bibliotecas de esta manera:

import "utils"
import "requirements"

Agregue una sola línea en la parte superior de cada biblioteca (es decir, utils.sh):

IMPORTED="$BASH_SOURCE"

Ahora tiene acceso a funciones dentro utils.shy requirements.shdesdescript.sh

TODO: escriba un vinculador para crear un solo sharchivo

Xaqron
fuente
¿Esto también resuelve el problema de ejecutar el script fuera del directorio en el que se encuentra?
Aaron H.
@AaronH. No. Es una forma estructurada para incluir dependencias en grandes proyectos.
Xaqron
1

Usar source o $ 0 no te dará la ruta real de tu script. Puede usar la identificación del proceso del script para recuperar su ruta real

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

Estoy usando este script y siempre me ha servido bien :)

francoisrv
fuente
1

Puse todos mis scripts de inicio en un directorio .bashrc.d. Esta es una técnica común en lugares como /etc/profile.d, etc.

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

El problema con la solución usando globbing ...

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

... es posible que tenga una lista de archivos que es "demasiado larga". Un enfoque como ...

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

... se ejecuta pero no cambia el entorno como se desea.

Phreed
fuente
1

Por supuesto, para cada uno lo suyo, pero creo que el siguiente bloque es bastante sólido. Creo que esto implica la "mejor" forma de encontrar un directorio, y la "mejor" forma de llamar a otro script bash:

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

echo "The main script"

Por lo tanto, esta puede ser la "mejor" forma de incluir otros scripts. Esto se basa en otra "mejor" respuesta que le dice a un script bash dónde está almacenado

modulitos
fuente
1

Esto debería funcionar de manera confiable:

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

source_relative incl.sh
PSkocik
fuente
0

solo necesitamos encontrar la carpeta donde se almacenan nuestros archivos incl.sh y main.sh; solo cambia tu main.sh con esto:

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
fuente
Cita incorrecta, uso innecesario echoy uso incorrecto de sedla gopción. -1.
l0b0
De todos modos, para hacer que este guión funcione: SCRIPT_DIR=$(echo "$0" | sed "s/${SCRIPT_NAME}//")y luegosource "${SCRIPT_DIR}incl.sh"
Krzysiek
0

Según el man hierlugar adecuado para la secuencia de comandos incluye es/usr/local/lib/

/ usr / local / lib

Archivos asociados con programas instalados localmente.

Personalmente prefiero /usr/local/lib/bash/includespara incluye. Hay bash-helper lib para incluir libs de esa manera:

#!/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 incluye salida de estado

Alexander Yancharuk
fuente
-5

También puedes usar:

PWD=$(pwd)
source "$PWD/inc.sh"
Django
fuente
99
Asume que está en el mismo directorio donde se encuentran los scripts. No funcionará si estás en otro lugar.
Luc M