¿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?



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"

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


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)


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"

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.


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

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"

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"

¿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!


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.


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.


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".


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.


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


¿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 :)


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.


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


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

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"

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


-5

También puedes usar:

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

99
Asume que está en el mismo directorio donde se encuentran los scripts. No funcionará si estás en otro lugar.
Luc M
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.