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

452

通常包含脚本的方法是使用"source"关键字。

例如:

main.sh:

#!/bin/bash

source incl.sh

echo "The main script"

incl.sh:

echo "The included script"
执行"./main.sh"的输出为:
The included script
The main script

现在,如果您尝试从另一个位置执行该shell脚本,则它无法找到包含文件,除非它在您的路径中。

有什么好的方法可以确保您的脚本能够找到包含的脚本,特别是如果例如该脚本需要是可移植的吗?


3
请看这个链接:https://dev59.com/yXVD5IYBdhLWcg3wL4cA - Lluís
1
顺便提一下,与此问题相关的是:这是我关于如何获取正在运行的脚本的SCRIPT_DIRECTORY路径的答案,因此您可以使用它来相对于当前脚本导入其他脚本(通过source.):如何从脚本内部获取Bash脚本的源目录?。(我的答案在那里排名很低,有了这个链接,您也可以节省一些滚动时间 :))。我的答案可以被认为是这里接受的答案的一个变体。 - Gabriel Staples
23个回答

267

我倾向于让我的脚本相互之间都是相对路径的。这样我就可以使用dirname:

#!/bin/sh

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

"$my_dir/other_script.sh"

7
如果脚本是通过 $PATH 执行的,那么这将不起作用。然后 which $0 就会很有用。 - Hugo
46
确定 shell 脚本的位置没有可靠的方法,参见 http://mywiki.wooledge.org/BashFAQ/028。 - Philipp
17
@Philipp,那篇文章的作者是正确的,Bash脚本确实很复杂,而且也有很多需要注意的地方。但它缺少了一些关键点,首先,作者假设您会对Bash脚本进行的操作有很多预设。我也不会指望Python脚本在没有依赖项的情况下运行。Bash是一种胶水语言,可以让您快速完成其他方式难以完成的任务。当您需要构建系统正常工作时,务实主义(和一个关于脚本无法找到依赖项的好警告)才是最重要的。 - Aaron H.
13
刚刚了解到BASH_SOURCE数组,其中的第一个元素始终指向当前源。 - haridsv
6
如果脚本位于不同的位置,这种方法将不起作用。例如,/home/me/main.sh 调用 /home/me/test/inc.sh,因为 dirname 将返回 /home/me。使用 BASH_SOURCE 是一个更好的解决方案,参见 https://dev59.com/-nVC5IYBdhLWcg3ww0Dr#12694189。 - opticyclic
显示剩余5条评论

238
我知道我来晚了,但这个应该可以在任何情况下使用,并且仅使用内置功能:
DIR="${BASH_SOURCE%/*}"
if [[ ! -d "$DIR" ]]; then DIR="$PWD"; fi
. "$DIR/incl.sh"
. "$DIR/main.sh"

. 命令是 source 命令的别名,$PWD 是工作目录的路径,BASH_SOURCE 是一个数组变量,其成员是源文件的文件名,${string%substring} 从 $string 的末尾删除最短匹配的 $substring。


15
这是该讨论串中唯一对我一直有效的答案。 - Justin
3
@sacii,我想问一下,什么情况下需要使用 if [[ ! -d "$DIR" ]]; then DIR="$PWD"; fi 这行代码?如果是将命令粘贴到 bash 提示符中运行,我可以理解为什么需要它。但是,如果在脚本文件上下文中运行,我就看不出有必要了... - Johnny Wong
2
值得注意的是,它可以在多个“源”上正常工作(也就是说,如果您在另一个目录中“源”了一个脚本,该脚本又“源”了另一个脚本,等等,它仍然可以正常工作)。 - Ciro Costa
5
这句话的意思是:${BASH_SOURCE[0]} 是否应该使用,因为你只想获取最新的调用?使用 DIR=$(dirname ${BASH_SOURCE[0]}) 可以让你摆脱条件语句。 - kshenoy
2
如果您使用 $ bash script.sh 从脚本目录调用脚本,则会失败,因为 $BASH_SOURCE 中不包括前导点,而 DIR 解析为 script.sh,而 $(dirname $BASH_SOURCE) 正确解析为 . - user1823021
显示剩余6条评论

58

另一种选择:

scriptPath=$(dirname $0)

是:

scriptPath=${0%/*}

这样的好处是不依赖于dirname命令,因为它不是一个内置命令(而且在仿真器中也不总是可用)。


2
当包含脚本文件被引用时,basePath=$(dirname $0)会给我返回空值。 - prayagupa

52

如果文件在同一目录下,可以使用dirname $0

#!/bin/bash

source $(dirname $0)/incl.sh

echo "The main script"

2
两个陷阱:1)$0./t.sh,dirname返回.;2)在cd bin之后,返回的.不正确。 $BASH_SOURCE也不好用。 - 18446744073709551615
另一个陷阱: 尝试在目录名称中添加空格。 - Hubert Grzeskowiak
4
source "$(dirname $0)/incl.sh" 适用于这些情况。 (翻译说明:对原文进行了直接翻译,并尽可能保留了原文的结构和含义,同时使语言更加通俗易懂。) - dsm

32

我认为最好的方法是使用Chris Boran的方式,但你应该按照以下方式计算MY_DIR:

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

引用readlink的man页:

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
除了缺少引号的问题之外,是否存在任何实际用例需要解析符号链接而不是直接使用 $0 - l0b0
1
假设你的脚本是/home/you/script.sh 你可以cd /home并从那里运行你的脚本,如./you/script.sh 在这种情况下,dirname $0将返回./you,包括其他脚本将失败。 - dr.scre
1
很好的建议,但是我需要按照以下步骤才能使它读取我的变量: MY_DIR=$(dirname $(readlink -f $0)); source $MY_DIR/incl.sh` - Frederick Ollinger

26

通过结合这个问题的答案可以提供最稳健的解决方案。

在生产级别脚本中,这种方法与依赖项和目录结构的良好支持一起为我们工作:

#!/bin/bash
# 当前脚本的完整路径 THIS=`readlink -f "${BASH_SOURCE[0]}" 2>/dev/null||echo $0`
# 当前脚本所在目录 DIR=`dirname "${THIS}"`
# '点'意味着'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 - Alexx Roche
@AlexxRoche 谢谢!这个在所有的Linux系统上都能用吗? - Brian Cannard
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://dev59.com/-nVC5IYBdhLWcg3ww0Dr#34208365/ - Asclepius

23
SRC=$(cd $(dirname "$0"); pwd)
source "${SRC}/incl.sh"

1
我认为你之所以被 down vote 是因为 "cd ...",当 dirname "$0" 可以实现相同的功能... - Aaron H.
9
这段代码将返回绝对路径,即使脚本从当前目录执行。仅使用 $(dirname "$0") 将只返回“。”。 - Max Arnold
1
./incl.sh 解析为与 cd + pwd 相同的路径。那么更改目录的优点是什么? - l0b0
有时候你需要脚本的绝对路径,例如当你需要来回切换目录时。 - András Aszódi
谢谢您添加这个。仅使用dirname让我感到不适:) 我几年前从导师那里学到了这个,但是形式稍有不同,为DIR =“ $(cd”$(dirname $0)“&& pwd)”。 - Rondo

19

1. 最简洁的

我尝试了几乎所有的建议,这是最简洁的一个,并且对我有效:

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

它甚至可以在将脚本链接到 $PATH 目录的情况下运行。

在这里查看它的表现:https://github.com/pendashteh/hcagent/blob/master/bin/hcagent

2. 最酷的

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

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

3. 最可靠的方法

或者,在极少数情况下,如果那些方法都不起作用,这里有一个防弹方法:

# Copyright https://dev59.com/5nVD5IYBdhLWcg3wWaNh#7400673
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

希望这对某些人有所帮助 :)

如果其中一种方法不适用于您,请将其作为评论留下,并提及您的操作系统和模拟器。谢谢!


16

即使脚本被调用,这也能起作用:

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

你能解释一下"[0]"的作用是什么吗? - Ray
2
@Ray BASH_SOURCE 是一个路径数组,对应于调用堆栈。第一个元素对应于堆栈中最近的脚本,也就是当前正在执行的脚本。实际上,默认情况下 $BASH_SOURCE 被称为变量并扩展到其第一个元素,因此这里不需要 [0]。有关详细信息,请参见此链接 - Jonathan H

8

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

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

. $installpath/incl.sh

echo "The main script"

或者,您可以要求用户维护一个环境变量,指示您的程序所在的位置,例如PROG_HOME之类的变量。这可以通过创建一个包含该信息的脚本放置在/etc/profile.d/目录下来自动为用户提供。每次用户登录时都会加载该脚本。


1
我理解你想要具体性,但我不明白为什么除非包含脚本是另一个软件包的一部分,否则需要完整路径。我认为从特定相对路径(即执行脚本的同一目录)加载与从特定完整路径加载没有安全差异。你为什么说这是无法避免的? - Aaron H.
4
因为你的脚本执行时所在的目录不一定是你想要包含在脚本中的脚本所在的位置。你希望在安装脚本的位置加载这些脚本,但是在运行时没有可靠的方法告诉你这个位置。不使用固定位置也是引用错误(例如黑客提供的)脚本并运行它的好方法。 - Steve Baker
在运行时,使用相对路径从脚本的位置是可靠的。而且可以通过$(cd "$(dirname $0)" && pwd)" 可以可靠地找到$0的绝对规范路径,这也可以用于规范化库目录:"$(cd "$(cd "$(dirname $0)" && pwd)/../lib" && pwd)"。 - Rondo

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接