如何最好地包括其他脚本?


353

您通常包含脚本的方式是使用“源代码”

例如:

main.sh:

#!/bin/bash

source incl.sh

echo "The main script"

包括sh:

echo "The included script"

执行“ ./main.sh”的输出为:

The included script
The main script

...现在,如果您尝试从另一个位置执行该Shell脚本,除非包含在您的路径中,否则它将找不到包含。

确保脚本可以找到包含脚本的好方法是什么,特别是例如在脚本需要可移植的情况下?



1
您的问题是如此的丰富和有益,以至于您甚至没有问过我的问题就已经回答了!不错的工作!
加布里埃尔·斯台普斯

Answers:


229

我倾向于使我的脚本彼此相对。这样我可以使用目录名:

#!/bin/sh

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

"$my_dir/other_script.sh"

5
如果脚本是通过$ PATH执行的,则将无法使用。那么which $0将会有用
雨果

41
没有确定外壳程序脚本位置的可靠方法,请参见mywiki.wooledge.org/BashFAQ/028
Philipp

12
@Philipp,该条目的作者是正确的,它很复杂,并且有陷阱。但是它缺少一些关键点,首先,作者假设您要对bash脚本执行的操作有很多事情。我也不希望没有它的依赖项就可以运行python脚本。Bash是一种胶合语言,它使您可以快速完成本来很难的事情。当您需要构建系统正常工作时,实用主义(以及关于脚本无法找到依赖项的一个很好的警告)将获胜。
亚伦·H。2010年

11
刚刚了解了BASH_SOURCE数组,以及数组中的第一个元素如何始终指向当前源。
haridsv 2014年

6
如果脚本位于不同的位置,则无法使用。例如/home/me/main.sh调用/home/me/test/inc.sh作为目录名将返回/ home / me。使用BASH_SOURCE的sacii答案是更好的解决方案stackoverflow.com/a/12694189/1000011
opticyclic

187

我知道我迟到了,但是无论您如何启动脚本并仅使用内置插件,这都应该起作用:

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

.(dot)命令是的别名source$PWD是工作目录的路径,BASH_SOURCE是一个数组变量,其成员是源文件名,${string%substring}从$ string的后面去除$ substring的最短匹配项


7
这是始终为我工作的线索中的唯一答案
贾斯汀

3
@sacii我可以知道什么时候if [[ ! -d "$DIR" ]]; then DIR="$PWD"; fi需要该行吗?如果将命令粘贴到bash提示符下运行,我会发现需要它。但是,如果在脚本文件上下文中运行,我看不到有此需要……
Johnny Wong

2
还值得注意的是,它可以跨多个sources正常工作(我的意思是,如果您source的脚本source在另一个目录中是另一个脚本,依此类推,它仍然可以工作)。
西罗科斯塔

4
这不应该是${BASH_SOURCE[0]}因为您只想要最新的通话吗?同样,使用它DIR=$(dirname ${BASH_SOURCE[0]})将使您摆脱if条件
kshenoy 16'July

1
您是否愿意在无属性要求的许可证(例如CC0)下提供此代码段,或只是将其发布到公共领域?我想使用此逐字记录,但是将归因放在每个脚本的顶部很烦人!
BeeOnRope19年

52

替代方法:

scriptPath=$(dirname $0)

是:

scriptPath=${0%/*}

..优点是不依赖于dirname,它不是内置命令(并且在仿真器中并不总是可用)


2
basePath=$(dirname $0)包含脚本文件的源文件给了我空白值。
祈祷

41

如果在同一目录中,则可以使用dirname $0

#!/bin/bash

source $(dirname $0)/incl.sh

echo "The main script"

2
有两个陷阱:1)$0is ./t.sh和dirname返回.;2)cd bin退回后.不正确。$BASH_SOURCE没有更好
18446744073709551615 2015年

另一个陷阱:请在目录名称中使用空格进行尝试。
Hubert Grzeskowiak

source "$(dirname $0)/incl.sh"适用于这些情况
dsm

27

我认为最好的方法是使用Chris Boran的方法,但是您应该这样计算MY_DIR:

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

引用手册页的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

我从未遇到用例MY_DIR计算不正确的情况。如果您通过符号链接访问脚本,$PATH那么它将起作用。


不错且简单的解决方案,它在我能想到的多种脚本调用变体中为我着名。谢谢。
Brian Cline 2013年

除了缺少引号的问题外,是否还有实际用例需要解析符号链接而不是$0直接使用?
l0b0

1
@ l0b0:假设您的脚本是/home/you/script.sh“可以” cd /home并从那里运行,因为./you/script.sh在这种情况下dirname $0将返回./you并且包括其他脚本将失败
dr.scre dr.16.24.16

1
很好的建议,但是,我需要执行以下操作才能读取`MY_DIR=$(dirname $(readlink -f $0)); source $MY_DIR/incl.sh
Frederick Ollinger

21

这个问题的答案的组合提供了最可靠的解决方案。

它在生产级脚本中为我们工作,对依赖项和目录结构提供了强大的支持:

#!/ bin / bash

#当前脚本的完整路径
THIS =`readlink -f“ $ {BASH_SOURCE [0]}” 2> / dev / null || echo $ 0`

#当前脚本所在的目录
DIR =`dirname“ $ {THIS}”``

#'Dot'表示'source',即'include':
。“ $ DIR / compile.sh”

该方法支持所有这些:

  • 路径中的空格
  • 链接(通过readlink
  • ${BASH_SOURCE[0]}$0

1
THIS = readlink -f "${BASH_SOURCE[0]}" 2>/dev/null||echo $0 如果您的readlink是BusyBox的V1.01
阿里克斯罗氏

@AlexxRoche谢谢!这可以在所有Linux上使用吗?
Brian Haak

1
我希望如此。似乎可以在Debian Sid 3.16和QNAP的armv5tel 3.4.6 Linux上使用。
Alexx Roche

2
我将其放在这一行中: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
我怀疑当目录名“ $ 0”应该完成相同的事情时,您还是投下了“ cd ...”的票……
Aaron

7
即使从当前目录执行脚本,此代码也将返回绝对路径。仅$(dirname“ $ 0”)将仅返回“。”
马克斯

1
./incl.sh解析为与cd+ 相同的路径pwd。那么更改目录的好处是什么?
l0b0

有时,您需要脚本的绝对路径,例如,当您必须来回更改目录时。
Laryx Decidua

15

即使脚本来源也可以使用:

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

您能解释一下“ [0]”的目的是什么?

1
@Ray BASH_SOURCE是路径的数组,对应于调用堆栈。第一个元素对应于堆栈中的最新脚本,即当前正在执行的脚本。实际上,$BASH_SOURCE默认情况下,称为变量的变量会扩展到其第一个元素,因此[0]在这里没有必要。有关详细信息,请参见此链接
乔纳森·H

10

1.内斯特

我研究了几乎所有建议,这是最适合我的建议:

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

即使脚本符号链接到$PATH目录,它也可以工作。

在此处查看其运行情况:https : //github.com/pendashteh/hcagent/blob/master/bin/hcagent

2.最酷

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

这实际上是来自此页面上的另一个答案,但我也将其添加到我的答案中!

2.最可靠

另外,在极少数情况下这些方法无效的情况下,以下是防弹方法:

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

您可以在taskrunner源代码中看到它的运行情况:https : //github.com/pendashteh/taskrunner/blob/master/bin/taskrunner

希望这可以帮助某人:)

另外,如果对您不起作用,请留下评论,并提及您的操作系统和仿真器。谢谢!


7

您需要指定其他脚本的位置,没有其他办法。我建议在脚本顶部使用一个可配置的变量:

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

. $installpath/incl.sh

echo "The main script"

或者,您可以坚持要求用户维护一个环境变量,该变量指示程序主目录的位置,例如PROG_HOME或诸如此类。通过在/etc/profile.d/中创建包含该信息的脚本,可以自动为用户提供此信息,该脚本将在用户每次登录时提供。


1
我很欣赏对特定性的渴望,但是除非包含脚本是另一个软件包的一部分,否则我不明白为什么需要完整的路径。我看不到从特定的相对路径(即执行脚本的目录)和特定的全路径加载的安全差异。你为什么说没有办法解决?
亚伦H.10年

4
因为脚本执行的目录不一定是您要包括在脚本中的脚本所在的位置。您想加载脚本的安装位置,而没有可靠的方法来告诉它们在运行时的位置。不使用固定位置也是一种包含错误(即由黑客提供)脚本并运行该脚本的好方法。
史蒂夫·贝克

6

我建议您创建一个setenv脚本,其唯一目的是为系统中各个组件提供位置。

然后,所有其他脚本都将源此脚本,以便使用setenv脚本在所有脚本中所有位置都相同。

这在运行cronjobs时非常有用。运行cron时,您得到的环境最少,但是如果使所有cron脚本都首先包含setenv脚本,则可以控制和同步要在其中执行cronjobs的环境。

我们在构建猴子上使用了这种技术,该技术用于在大约2,000 kSLOC的项目中进行持续集成。


3

Steve的回复绝对是正确的技术,但是应该对其进行重构,以便将installpath变量放在单独的环境脚本中,在该脚本中进行所有此类声明。

然后所有脚本源该脚本并应更改安装路径,您只需要在一个位置进行更改。使事情变得更加错误,永不过时。上帝,我讨厌这个词!(-:

顺便说一句,当您以示例中所示的方式使用变量时,应该真正使用$ {installpath}来引用该变量:

. ${installpath}/incl.sh

如果不使用花括号,则某些外壳程序将尝试扩展变量“ installpath / incl.sh”!


3

Shell Script Loader是我的解决方案。

它提供了一个名为include()的函数,该函数可以在许多脚本中多次调用以引用单个脚本,但仅加载一次脚本。该函数可以接受完整路径或部分路径(在搜索路径中搜索脚本)。还提供了一个名为load()的类似函数,该函数将无条件加载脚本。

它适用于bashkshpd kshzsh,并且每个脚本都有优化的脚本。以及其他与原始sh(例如ashdashheirloom sh等)一般兼容的shell,它们通过通用脚本根据其可提供的功能自动优化其功能。

[有重点的例子]

start.sh

这是一个可选的启动脚本。将启动方法放在这里只是一种方便,可以将其放在主脚本中。如果要编译脚本,则也不需要此脚本。

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

# ...

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

输出:

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

最好的是,基于该脚本的脚本也可以与可用的编译器一起编译为单个脚本。

这是一个使用它的项目:http : //sourceforge.net/p/playshell/code/ci/master/tree/。无论是否编译脚本,它都可以方便地运行。生成单个脚本的编译也可能发生,这在安装过程中很有帮助。

我还为可能希望对实现脚本的工作原理进行简要介绍的任何保守派人士创建了一个更简单的原型:https : //sourceforge.net/p/loader/code/ci/base/tree/loader-include-prototype .bash。它很小,任何人都可以在自己的主脚本中包含该代码,如果他们希望其代码旨在与Bash 4.0或更高版本一起运行,并且也不会使用eval


3
12 KB的Bash脚本,其中包含超过100行的evaled代码以加载依赖项。哎哟
l0b0

1
eval底部附近的三个街区之一恰好一直在运行。因此,无论是否需要,都可以肯定使用eval
l0b0

3
该eval调用是安全的,如果您具有Bash 4.0+,则不会使用。我知道,您是那些认为eval纯粹的邪恶,却不知道如何善加利用的古老脚本编写者之一。
konsolebox

1
我不知道您所说的“未使用”是什么意思,但是它运行了。是的,经过几年的shell脚本编写工作之后,是的,我比以往任何时候都坚信这eval是邪恶的。
l0b0

1
立即想到两种可移植的,简单的标记文件的方法:通过它们的inode编号引用它们,或者通过将NUL分隔的路径放入文件中。
l0b0 2014年

2

亲自将所有库放在lib文件夹中,并使用import函数来加载它们。

文件夹结构

在此处输入图片说明

script.sh 内容

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

请注意,此导入功能应在脚本的开头,然后您可以像这样轻松导入库:

import "utils"
import "requirements"

在每个库的顶部添加一行(即utils.sh):

IMPORTED="$BASH_SOURCE"

现在你有内部的功能的访问utils.sh,并requirements.shscript.sh

待办事项:编写链接器以构建一个sh文件


这是否也解决了在目录之外执行脚本的问题?
亚伦H.18年

@AaronH。否。这是在大型项目中包含依赖项的结构化方式。
Xaqron

1

使用source或$ 0不会给您脚本的真实路径。您可以使用脚本的进程ID来检索其真实路径。

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

我正在使用此脚本,它一直对我有用:)


1

我将所有启动脚本放在.bashrc.d目录中。这是/etc/profile.d等位置的常用技术。

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

使用全局解决方案的问题...

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

...您可能有一个“太长”的文件列表。像...这样的方法

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

...可以运行,但不会根据需要更改环境。


1

当然,每个人都有自己的想法,但是我认为下面的内容非常可靠。我相信这涉及找到目录的“最佳”方式,以及调用另一个bash脚本的“最佳”方式:

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

echo "The main script"

因此,这可能是包括其他脚本的“最佳”方法。这是基于另一个“最佳”答案的,该答案告诉bash脚本存储的位置


1

这应该可靠地工作:

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

source_relative incl.sh

0

我们只需要找出incl.sh和main.sh的存储文件夹;只需使用以下命令更改main.sh:

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"

不正确引用,不必要地使用echo,和不正确使用sedg选项。-1。
2014年

无论如何,要获得此脚本,请执行以下操作:SCRIPT_DIR=$(echo "$0" | sed "s/${SCRIPT_NAME}//")然后source "${SCRIPT_DIR}incl.sh"
Krzysiek 2014年

0

根据man hier脚本的合适位置包括/usr/local/lib/

/ usr /本地/ lib

与本地安装的程序关联的文件。

我个人更喜欢/usr/local/lib/bash/includes包含。有bash-helper lib以这种方式包括lib:

#!/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包含状态输出


-5

您还可以使用:

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

9
您假定您位于脚本所在的目录中。如果您在其他地方,它将无法正常工作。
吕克M
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.