这个问题,如果重要的话,最好是在 Bash 的上下文中,但我希望有一个跨语言的解决方案。
我遇到的问题是在用另一个脚本“获取”一个脚本的情况下,但以递归方式执行此操作。问题是它似乎无限地执行此操作。
具体案例如下。我创建了一个简单的 bash 脚本来加载同一目录中的其他 bash 脚本。由于在我的项目中所有脚本都位于同一目录中,因此现在可以。
所以“脚本加载器”如下(_source_script.sh
定义以下函数的文件):
_source_script()
{
# Define a few colors for the possible error messages below.
RED=$(tput setaf 1)
NORMAL=$(tput sgr0)
EXPECTED_DIR="scripts"
if [ "$#" -ge "1" ]
then
EXPECTED_DIR=$1
fi
# Based on: http://stackoverflow.com/a/1371283/3924118
CURRENT_DIR=${PWD##*/}
if [ "$CURRENT_DIR" = "$EXPECTED_DIR" ]
then
for (( arg = 2; arg <= $#; arg++ ))
do
# Based on:
# - http://askubuntu.com/questions/306851/how-to-import-a-variable-from-a-script
# - http://unix.stackexchange.com/questions/114300/whats-the-meaning-of-a-dot-before-a-command-in-shell
# - http://stackoverflow.com/questions/20094271/bash-using-dot-or-source-calling-another-script-what-is-difference
printf ". ./${!arg}.sh\n"
# Try to load script ${!arg}.sh
. ./${!arg}.sh
# If it was not loaded successfully, exit with status 1.
if [ $? -ne 0 ]
then
printf "${RED}Script '${!arg}.sh' not loaded successfully. Exiting...${NORMAL}\n"
exit 1
fi
done
else
printf "No script loaded: $CURRENT_DIR != $EXPECTED_DIR.\n"
exit 1
fi
}
要使用这个“脚本加载器”,其他脚本必须首先“获取”它,如下所示:
. ./_source_script.sh
问题是当我尝试通过 function 获取其他脚本来包含它们时_source_script
。例如,当我尝试这样做时(在 script 中some_script.sh
):
_source_script scripts colors asserts clean_environment
它永远持续运行。
里面colors.sh
有:
#!/usr/bin/env bash
# Colors used when printing.
export GREEN=$(tput setaf 2)
export RED=$(tput setaf 1)
export NORMAL=$(tput sgr0)
export YELLOW=$(tput setaf 3)
里面asserts.sh
有:
...
. ./_source_script.sh
_source_script scripts colors
里面clean_environment.sh
我还有:
. ./_source_script.sh
_source_script scripts colors
根据我的理解,这应该递归运行,直到找到一个不加载任何内容的脚本或找到一个循环,但情况并非如此。
所以,我的解决方案是some_script.sh
:
_source_script scripts colors
_source_script scripts asserts
_source_script scripts clean_environment
也就是说,单独运行它们。
那么,为什么我不能在循环中“获取”多个脚本呢?
答案1
不要太仔细地查看代码,一般方法将包括类似于 C 的“标头防护”的内容:
#ifndef HEADER_H
#define HEADER_H
/* The contents of the header file, with typedefs etc. */
#endif
其中HEADER_H
是特定于此标头的 C 预处理器宏,仅用于避免再次包含该标头(如果已包含该标头)。我通常创建一个宏,其名称源自头文件本身,例如MCMC_H
名为mcmc.h
.
在 shell 脚本中,这可能很简单
if [ -z "$script_source_guard" ]; then
script_source_guard=1
# ... do things (define functions etc.)
fi
变量的名称script_source_guard
需要特定于此文件。同样,该名称可以任意源自源文件名;名称为 的文件可能有一个名为or或或其他名称myfuncts.shlib
的保护变量。MYFUNCTS
_MYFUNCTS
guard_myfuncts
答案2
这是一个范围问题。按照设计,该_source_script
函数实际上是递归调用自身,因为它正在执行. ./${!arg}.sh
并获取调用_source_script
.但 shell 脚本中的变量默认是全局的。因此,_source_script
同时处于活动状态(在堆栈上)的多个函数化身共享同一个 arg
.
详细地:
当asserts.sh
orclean_environment.sh
调用时
_source_script scripts colors
函数_source_script
正在变得$#
= 2。因此for (( arg = 2; arg <= $#; arg++ ))
循环运行 while $arg
is <= 2,所以它最终是 3。当. asserts.sh
被调用时,$arg
已经是 3,因为asserts
is$3
在
_source_script scripts colors asserts clean_environment
命令在some_script.sh
.所以,如果$arg
是放 到 3 in asserts.sh
,这最终成为一个空操作——恢复$arg
到原来的状态。
但是,当. clean_environment.sh
被调用时,$arg
是 4,因为clean_environment
是$4
在
_source_script scripts colors asserts clean_environment
命令。因此,如果将 in (当调用)$arg
设置为 3 时, (in )的顶级化身将失去其在循环中的位置,并返回并再次执行。一次又一次,一次又一次……clean_environment.sh
_source_script
_source_script
some_script.sh
for (( arg = 2; arg <= $#; arg++ ))
. clean_environment.sh
解决方案
当然,解决方案就是简单地 local arg
在函数中放置一个声明_source_script
。
关于脚本的其他注释:
你有这样的情况:
- 您有一些定义各种内容(常量、函数、别名等)的文件。
- 您还有其他文件使用上面定义的那些东西,所以它们
.
(源)包含定义的文件,以及 有些文件同时属于上述两个组;例如,
asserts.sh
并且clean_environment.sh
可能向其调用者提供一些资源,但他们也读取colors.sh
以获取其中定义的值。大概有一个层次结构:some_script.sh
来电colors.sh
some_script.sh
来电asserts.sh
asserts.sh
来电colors.sh
some_script.sh
来电clean_environment.sh
clean_environment.sh
来电colors.sh
ETC。
一些用户建议使用“标头防护”来防止 shell 命令文件被多次处理。虽然这个建议很好并且与您相关情况 (如上所述),我真的不明白它与你有什么关系问题。
- 除了你应该使用它
_source_script.sh
本身。在上面的层次结构中,我忽略了这样一个事实:每个文件来电
_source_script.sh
。这意味着该_source_script
函数 是在运行时被重新定义。 这听起来可能非常危险。你说
if [ "$#" -ge "1" ] then EXPECTED_DIR=$1 fi ︙
你也可以直接说
if [ "$#" -le 1 ] then exit 1 # or maybe return 1 fi EXPECTED_DIR=$1 ︙
因为如果参数少于两个则无事可做。
printf
除非您确定需要,否则不要在格式字符串(即第一个参数)中使用变量会更安全 。所以printf ". ./${!arg}.sh\n" printf "${RED}Script '${!arg}.sh' not loaded successfully. Exiting...${NORMAL}\n" printf "No script loaded: $CURRENT_DIR != $EXPECTED_DIR.\n"
应该
printf ". ./%s.sh\n" "${!arg}" printf "%sScript '%s.sh' not loaded successfully. Exiting...%s\n" \ "$RED" "${!arg}" "$NORMAL" printf "No script loaded: %s != %s.\n" "$CURRENT_DIR" "$EXPECTED_DIR"
您应该更多地使用引号。
. ./${!arg}.sh
应该
. "./${!arg}.sh"
而且,严格来说,要真正做到
偏执安全,$?
应该是"$?"
。"$?"
但是,为什么不直接说,而不是对 进行明确的测试if ! . "./${!arg}.sh" then printf "%sScript '%s.sh' not loaded successfully. Exiting...%s\n" \ "$RED" "${!arg}" "$NORMAL" exit 1 fi
?
请注意,一个
.
成功的命令将设置$?
为文件中最后一个命令的退出状态。因此,请注意您的头文件(.sh
定义其他文件要使用的内容的文件)返回准确的退出状态。
答案3
如果您想通过一个父文件来链接采购,没问题。我不会将源语句放在函数中。只需获取父文件,它也应该获取所有子文件。
从内部命令的 bash 手册页source
:
[It]从文件名中读取并执行命令当前的 shell 环境并返回从 filename 执行的最后一个命令的退出状态。
如果您不能保证当前 shell 环境将持续执行某个函数,您可能需要更仔细地考虑您的配置。