Qual a melhor forma de incluir outros scripts?

353

A maneira como você normalmente incluiria um script é com "source"

por exemplo:

main.sh:

#!/bin/bash

source incl.sh

echo "The main script"

incl.sh:

echo "The included script"

A saída da execução "./main.sh" é:

The included script
The main script

... Agora, se você tentar executar esse shell script de outro local, ele não poderá encontrar o include, a menos que esteja no seu caminho.

Qual é uma boa maneira de garantir que seu script encontre o script de inclusão, especialmente se, por exemplo, o script precisar ser portátil?

Aaron H.
fonte
11
Sua pergunta é tão boa e informativa que minha pergunta foi respondida antes mesmo de você ! Bom trabalho!
Gabriel Staples

Respostas:

229

Eu costumo fazer com que meus scripts sejam relativos um ao outro. Dessa forma, eu posso usar o dirname:

#!/bin/sh

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

"$my_dir/other_script.sh"
Chris Boran
fonte
5
Isso não funcionará se o script for executado através de $ PATH. em seguida, which $0irá ser útil
Hugo
41
Não há uma maneira confiável de determinar a localização de um script de shell, consulte mywiki.wooledge.org/BashFAQ/028
Philipp
12
@ Philipp, o autor dessa entrada está correto, é complexo e há truques. Mas faltam alguns pontos-chave: primeiro, o autor assume muitas coisas sobre o que você fará com o script bash. Eu não esperaria que um script python fosse executado sem suas dependências também. Bash é uma linguagem de cola que permite que você faça coisas rapidamente que, de outra forma, seriam difíceis. Quando você precisa que seu sistema de construção funcione, o pragmatismo (e um bom aviso sobre o script não conseguir encontrar dependências) vence.
Aaron H.
11
Acabamos de aprender sobre a BASH_SOURCEmatriz e como o primeiro elemento dessa matriz sempre aponta para a fonte atual.
haridsv
6
Isso não funcionará se os scripts forem locais diferentes. por exemplo, /home/me/main.sh chama /home/me/test/inc.sh como dirname retornará / home / me. resposta sacii usando BASH_SOURCE é uma solução melhor stackoverflow.com/a/12694189/1000011
opticyclic
187

Sei que estou atrasado para a festa, mas isso deve funcionar, independentemente de como você inicia o script e usa exclusivamente os componentes internos:

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

.O comando (ponto) é um alias para source, $PWDé o Caminho para o Diretório de Trabalho, BASH_SOURCEé uma variável de matriz cujos membros são os nomes de arquivos de origem, ${string%substring}retira a menor correspondência de $ substring na parte de trás de $ string

sacii
fonte
7
Esta é a única resposta no segmento que sempre trabalhou para mim
Justin
3
@sacii Posso saber quando é a linha if [[ ! -d "$DIR" ]]; then DIR="$PWD"; finecessária? Posso encontrar uma necessidade, se os comandos estiverem sendo colados em um prompt do bash para execução. No entanto, se estiver rodando dentro de um contexto de arquivo de script, eu não posso ver uma necessidade para ela ...
Johnny Wong
2
Também é importante notar que ele funciona conforme o esperado em vários sources (quero dizer, se você é sourceum script que sourceé outro em outro diretório e assim por diante, ele ainda funciona).
Ciro Costa
4
Não deveria ser ${BASH_SOURCE[0]}assim que você deseja apenas a última chamada? Além disso, usar DIR=$(dirname ${BASH_SOURCE[0]})permitirá que você se livrar do se-condição
kshenoy
11
Você deseja disponibilizar esse trecho sob uma licença sem atributos, como CC0 ou apenas liberá-lo para o domínio público? Eu gostaria de usar isso literalmente, mas é irritante colocar a atribuição no topo de cada script!
BeeOnRope 17/11/19
52

Uma alternativa para:

scriptPath=$(dirname $0)

é:

scriptPath=${0%/*}

.. a vantagem de não depender da dirname, que não é um comando interno (e nem sempre está disponível nos emuladores)

atrasado
fonte
2
basePath=$(dirname $0)me deu um valor em branco quando o arquivo de script que contém é originado.
prayagupd
41

Se estiver no mesmo diretório, você pode usar dirname $0:

#!/bin/bash

source $(dirname $0)/incl.sh

echo "The main script"
dsm
fonte
2
Duas armadilhas: 1) $0é ./t.she dirname retorna .; 2) após cd bino retorno .não está correto. $BASH_SOURCEnão é melhor.
18446744073709551615
outra armadilha: tente isso com espaços no nome do diretório.
Hubert Grzeskowiak 7/11
source "$(dirname $0)/incl.sh"funciona para esses casos
dsm
27

Eu acho que a melhor maneira de fazer isso é usar o caminho de Chris Boran, mas você deve calcular MY_DIR desta maneira:

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

Para citar as páginas de manual do 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

Eu nunca encontrei um caso de uso em que MY_DIRnão seja calculado corretamente. Se você acessar seu script através de um link simbólico no seu, $PATHele funcionará.

Mat131
fonte
Solução agradável e simples, e funciona famosamente para mim em tantas variações de invocação de script que eu conseguia pensar. Obrigado.
Brian Cline
Além de problemas com aspas ausentes, existe algum caso de uso real em que você deseja resolver os links simbólicos em vez de usar $0diretamente?
L0b0 31/05
11
@ l0b0: Imagine seu script é /home/you/script.shVocê pode cd /homee executar o script de lá como ./you/script.shNeste caso dirname $0voltará ./youe inclusive outro script falhará
dr.scre
11
Ótima sugestão, no entanto, eu precisava fazer o seguinte para ler minhas variáveis ​​em `# MY_DIR=$(dirname $(readlink -f $0)); source $MY_DIR/incl.sh
Frederick Ollinger 25/01/18
21

Uma combinação das respostas a esta pergunta fornece a solução mais robusta.

Ele funcionou para nós em scripts de nível de produção com grande suporte a dependências e estrutura de diretórios:

#! / bin / bash

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

# O diretório em que o script atual reside
DIR = `dirname" $ ​​{THIS} "`

# 'Ponto' significa 'fonte', ou seja, 'incluir':
. "$ DIR / compile.sh"

O método suporta todos estes:

  • Espaços no caminho
  • Links (via readlink)
  • ${BASH_SOURCE[0]} é mais robusto que $0
Brian Haak
fonte
11
THIS = readlink -f "${BASH_SOURCE[0]}" 2>/dev/null||echo $0 se o seu link de leitura for BusyBox v1.01
Alexx Roche
@AlexxRoche thank you! Isso funcionará em todos os Linux?
Brian Haak 25/02
11
Eu esperaria que sim. Parece funcionar no Debian Sid 3.16 e no armv5tel 3.4.6 Linux da QNAP.
Alexx Roche
2
Coloquei-o nesta linha:DIR=$(dirname $(readlink -f "${BASH_SOURCE[0]}" 2>/dev/null||echo $0)) # https://stackoverflow.com/a/34208365/
Acumenus 21/01
20
SRC=$(cd $(dirname "$0"); pwd)
source "${SRC}/incl.sh"
Máx.
fonte
11
Eu suspeito que você foi votado para o "cd ..." quando o dirname "$ 0" deve realizar a mesma coisa ...
Aaron H.
7
Esse código retornará o caminho absoluto, mesmo quando o script for executado no diretório atual. Somente $ (dirname "$ 0") retornará apenas "."
Max
11
E ./incl.shresolve para o mesmo caminho que cd+ pwd. Então, qual é a vantagem de alterar o diretório?
L0b0 31/05
Às vezes, você precisa do caminho absoluto do script, por exemplo, quando você precisa mudar os diretórios para frente e para trás.
Laryx Decidua 24/03
15

Isso funciona mesmo se o script for originado:

source "$( dirname "${BASH_SOURCE[0]}" )/incl.sh"
Alessandro Pezzato
fonte
Você poderia explicar qual é o objetivo do "[0]"?
Raio
11
@Ray BASH_SOURCEé uma matriz de caminhos, correspondente a uma pilha de chamadas. O primeiro elemento corresponde ao script mais recente na pilha, que é o script atualmente em execução. Na verdade, $BASH_SOURCEchamado como variável se expande para seu primeiro elemento por padrão, portanto, [0]não é necessário aqui. Veja este link para detalhes.
Jonathan H
10

1. Mais arrumado

Eu explorei quase todas as sugestões e aqui está a mais legal que funcionou para mim:

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

Funciona mesmo quando o script está vinculado a um $PATHdiretório.

Veja em ação aqui: https://github.com/pendashteh/hcagent/blob/master/bin/hcagent

2. O mais legal

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

Na verdade, isso é de outra resposta nesta página, mas também estou adicionando à minha resposta!

2. O mais confiável

Como alternativa, nos raros casos em que eles não funcionaram, aqui está a abordagem à prova 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"))

Você pode vê-lo em ação na taskrunnerfonte: https://github.com/pendashteh/taskrunner/blob/master/bin/taskrunner

Espero que isso ajude alguém lá fora :)

Além disso, deixe como comentário se um não funcionou para você e mencione seu sistema operacional e emulador. Obrigado!

Alexar
fonte
7

Você precisa especificar a localização dos outros scripts, não há outra maneira de contornar isso. Eu recomendo uma variável configurável na parte superior do seu script:

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

. $installpath/incl.sh

echo "The main script"

Como alternativa, você pode insistir para que o usuário mantenha uma variável de ambiente indicando onde está sua página inicial do programa, como PROG_HOME ou algo assim. Isso pode ser fornecido ao usuário automaticamente, criando um script com essas informações em /etc/profile.d/, que será obtido toda vez que um usuário efetuar login.

Steve Baker
fonte
11
Aprecio o desejo de especificidade, mas não vejo por que o caminho completo deve ser necessário, a menos que os scripts de inclusão façam parte de outro pacote. Não vejo uma diferença de segurança sendo carregada de um caminho relativo específico (ou seja, o mesmo diretório em que o script está sendo executado.) Vs um caminho completo específico. Por que você diz que não há maneira de contornar isso?
Aaron H.
4
Como o diretório em que seu script está executando não é necessariamente onde estão localizados os scripts que você deseja incluir no script. Você deseja carregar os scripts em que eles estão instalados e não há uma maneira confiável de saber onde está o tempo de execução. Não usar um local fixo também é uma boa maneira de incluir e executar o script errado (por exemplo, fornecido pelo hacker).
22610 Steve
6

Eu sugiro que você crie um script setenv cujo único objetivo seja fornecer locais para vários componentes em seu sistema.

Todos os outros scripts originariam esse script para que todos os locais sejam comuns em todos os scripts usando o script setenv.

Isso é muito útil ao executar cronjobs. Você obtém um ambiente mínimo ao executar o cron, mas se você fizer todos os scripts cron primeiro incluirem o script setenv, poderá controlar e sincronizar o ambiente em que deseja que os cronjobs executem.

Usamos essa técnica em nosso macaco de construção que foi usado para integração contínua em um projeto de cerca de 2.000 kSLOC.

Rob Wells
fonte
3

A resposta de Steve é ​​definitivamente a técnica correta, mas deve ser refatorada para que sua variável installpath esteja em um script de ambiente separado onde todas essas declarações sejam feitas.

Então, todos os scripts o originam e devem mudar de novo, só é necessário alterá-lo em um local. Torna as coisas mais, er, à prova de futuro. Deus, eu odeio essa palavra! (-:

BTW Você realmente deve se referir à variável usando $ {installpath} ao usá-la da maneira mostrada no seu exemplo:

. ${installpath}/incl.sh

Se as chaves forem deixadas de fora, algumas conchas tentarão expandir a variável "installpath / incl.sh"!

Rob Wells
fonte
3

O Shell Script Loader é a minha solução para isso.

Ele fornece uma função chamada include () que pode ser chamada várias vezes em vários scripts para referenciar um único script, mas carregará o script apenas uma vez. A função pode aceitar caminhos completos ou parciais (o script é pesquisado em um caminho de pesquisa). Também é fornecida uma função semelhante chamada load () que carregará os scripts incondicionalmente.

Ele funciona para bash , ksh , pd ksh e zsh com scripts otimizados para cada um deles; e outros shells que são genericamente compatíveis com o sh original, como ash , dash , relíquia sh , etc., por meio de um script universal que otimiza automaticamente suas funções, dependendo dos recursos que o shell pode oferecer.

[Exemplo conhecido]

start.sh

Este é um script inicial opcional. A colocação dos métodos de inicialização aqui é apenas uma conveniência e pode ser colocada no script principal. Este script também não é necessário se os scripts forem compilados.

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

# ...

cinza

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

resultado:

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

O melhor é que os scripts baseados nele também possam ser compilados para formar um único script com o compilador disponível.

Aqui está um projeto que o utiliza: http://sourceforge.net/p/playshell/code/ci/master/tree/ . Pode ser executado de forma portável, com ou sem a compilação dos scripts. A compilação para produzir um único script também pode acontecer e é útil durante a instalação.

Também criei um protótipo mais simples para qualquer parte conservadora que queira ter uma breve idéia de como um script de implementação funciona: https://sourceforge.net/p/loader/code/ci/base/tree/loader-include-prototype .bash . É pequeno e qualquer um pode incluir o código no script principal, se quiser, se o código for executado com o Bash 4.0 ou mais recente, e também não o usar eval.

konsolebox
fonte
3
12 kilobytes de script Bash contendo mais de 100 linhas de evalcódigo ed para carregar dependências. Ouch
l0b0
11
Exatamente um dos três evalblocos próximos ao fundo é sempre executado. Então, seja necessário ou não, com certeza está usando eval.
l0b0 31/05
3
Essa chamada de avaliação é segura e não é usada se você tiver o Bash 4.0 ou superior. Entendo, você é um daqueles roteiristas dos velhos tempos que pensam que evalé puro mal e, em vez disso, não sabe como fazer bom uso dele.
Konsolebox
11
Não sei o que você quer dizer com "não usado", mas é executado. E depois de alguns anos de script de shell como parte do meu trabalho, sim, estou mais convencido do que nunca que isso evalé mau.
l0b0 31/05
11
Duas maneiras simples e portáteis de sinalizar arquivos vêm à mente instantaneamente: referindo-se a eles pelo número de inode ou colocando caminhos separados por NUL em um arquivo.
l0b0 31/05
2

Coloque pessoalmente todas as bibliotecas em uma libpasta e use uma importfunção para carregá-las.

estrutura de pastas

insira a descrição da imagem aqui

script.sh conteúdo

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

Observe que essa função de importação deve estar no início do seu script e, em seguida, você pode importar facilmente suas bibliotecas como esta:

import "utils"
import "requirements"

Adicione uma única linha na parte superior de cada biblioteca (ou seja, utils.sh):

IMPORTED="$BASH_SOURCE"

Agora você tem acesso a funções internas utils.she requirements.shexternasscript.sh

TODO: escreva um vinculador para criar um único sharquivo

Xaqron
fonte
Isso também resolve o problema de executar o script fora do diretório em que está?
Aaron H.
@AaronH. Não. É uma maneira estruturada de incluir dependências em grandes projetos.
Xaqron
1

Usar source ou $ 0 não fornecerá o caminho real do seu script. Você pode usar a identificação do processo do script para recuperar seu caminho real

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

Estou usando esse script e ele sempre me serviu bem :)

francoisrv
fonte
1

Coloquei todos os meus scripts de inicialização em um diretório .bashrc.d. Essa é uma técnica comum em lugares como /etc/profile.d, etc.

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

O problema com a solução usando globbing ...

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

... é possível que você tenha uma lista de arquivos "muito longa". Uma abordagem como ...

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

... é executado, mas não altera o ambiente, conforme desejado.

phreed
fonte
1

Claro, cada um na sua, mas acho que o bloco abaixo é bastante sólido. Acredito que isso envolva a "melhor" maneira de encontrar um diretório e a "melhor" maneira de chamar outro script bash:

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

echo "The main script"

Portanto, essa pode ser a melhor maneira de incluir outros scripts. Isso se baseia em outra resposta "melhor" que informa ao script bash onde ele está armazenado

modulitos
fonte
1

Isso deve funcionar de forma confiável:

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

source_relative incl.sh
PSkocik
fonte
0

nós apenas precisamos descobrir a pasta onde nosso incl.sh e main.sh estão armazenados; apenas mude seu main.sh com isto:

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
fonte
Citando, uso incorreto desnecessária de echoe uso incorreto de sed's gopção. -1.
L0b0 31/05
De qualquer forma, para obter esse script de trabalho do: SCRIPT_DIR=$(echo "$0" | sed "s/${SCRIPT_NAME}//")e depoissource "${SCRIPT_DIR}incl.sh"
Krzysiek
0

O man hierlocal adequado para o script inclui é/usr/local/lib/

/ usr / local / lib

Arquivos associados a programas instalados localmente.

Pessoalmente, prefiro /usr/local/lib/bash/includesincluir. Existe uma biblioteca bash-helper para incluir bibliotecas dessa maneira:

#!/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 inclui saída de status

Alexander Yancharuk
fonte
-5

Você também pode usar:

PWD=$(pwd)
source "$PWD/inc.sh"
Django
fonte
9
Você supõe que esteja no mesmo diretório em que os scripts estão localizados. Não funcionará se você estiver em outro lugar.
Luc M