source语法的引入,使得shell的脚本也可以像其它语言一样,一份代码能够分成多个模块,基本的模块可以像库文件一样被多个脚本使用。例如/etc/init.d/functions,它被多个服务脚本使用。

source除了导入库代码的作用之外,它还可以用于导入配置文件,这个在Linux系统中使用的非常广泛,因为大多数配置文件都是文本格式的,而shell本身又没有解析过于复杂的配置文件的能力,例如xml、json等。典型的例子有,/etc/default/rcS或者/etc/sysconfig/network等等。

也正因为如此,一旦Shell脚本的代码量到达一定的规模,模块化的趋势是必然的,很多地方都会用到source,所以理解source是很有必要的。source的过程,其实就是将脚本导入到当前的执行环境下,并且依次执行其中的代码。因此对于被source的脚本来说,它的一切环境变量都是与当前环境保持一致的,被source脚本中对环境做的任何改动都会影响到当前的执行环境。

下面的例子很好的说明了这点,假设有两个脚本a.sh和b.sh,它们的内容如下所示:

[kodango@devops workspace]$ cat a.sh 
echo "Before source: $(pwd)"
. ./b.sh
echo "After source: $(pwd)"
[kodango@devops workspace]$ cat b.sh
cd /tmp

现在执行下a.sh,会获得以下的结果:

[kodango@devops workspace]$ sh a.sh 
Before source: /home/kodango/workspace
After source: /tmp

可见,b.sh中对当前路径的变化影响到了a.sh脚本。

那如果多次source同一个脚本会怎么样?答案是,被source的脚本的代码会被执行多次。如果是函数的定义还好,多次执行不会有什么影响,但是如果是变量的定义就会有问题了,同样用一例子来说明一下这个场景。

假设a.sh和b.sh两个脚本的内容如下所示:

[kodango@devops workspace]$ cat a.sh 
. ./b.sh
echo "after source: b=$b"

b=1
echo "assign value: b=$b"

. ./b.sh
echo "after second source: b=$b"
[kodango@devops workspace]$ cat b.sh 
b=0

b.sh的代码只有一行,仅仅是将变量b赋值成0。而在a.sh中,两次导入b.sh,同时在两次导入的中间会主动将b赋值为1。执行a.sh后,会得到以下的结果:

[kodango@devops workspace]$ sh a.sh 
after source: b=0
assign value: b=1
after second source: b=0

这个结果并不太让人意外,因为正如我们之前说的,被source的脚本的代码会在导入的过程中被执行。在这里,每次导入b.sh的时候,变量b都会重新赋值成0。所以会设置默认值的脚本,在被source的时候,要非常注意这一点,当脚本被重复导入的时候,该变量的值会被重新赋值成默认值。

这个问题在我使用logdotsh的时候遇到了,logdotsh为了能够方便使用,对日志打印的行为设置了一些默认值,例如日志级别、日志格式等等,同时也提供了设置这些参数的函数。比如可以通过set_loglevel来调整默认的日志级别。这样就带来了一个问题,当你导入logdotsh脚本之后,通过set_loglevel设置了日志级别,但是如果重新再导入一次logdotsh之后,日志级别会被默认值覆盖。

所以必须找到一种方法,可以避免一个脚本被多次重复导入。我现在的做法是增加一个标志变量:当第一次导入logdotsh时会设置该变量,同时设置默认参数;第二次导入的时候检查该变量的值,如果不为空则不再设置默认值,具体的代码是这样的:

# Determines whether the default value have be set
if [ -z "$_log_set_default" ]; then
    # Set the flag to 1
    _log_set_default=1

    # Show log whose level less than this
    _log_level=3
fi

这种做法类似C/C++中避免头文件被多次引用的解决方法:

#ifndef _HEADERNAME_H
#define _HEADERNAME_H

// 头文件内容

#endif

这种做法比较简单,唯一需要关注的是标志变量_log_set_default必须要特别点,尽量保证不会在脚本的其它地方使用或者设置。下面的代码中,使用__sourced_$$__作为标志变量的名称,其中$$会被展开成当前脚本的pid,${!_sourced_}是间接引用,关于这点我们在简洁的Bash编程技巧续篇中曾经介绍过。

[kodango@devops workspace]$ cat sourced.sh 
_sourced_="__sourced_$$__"

echo "Flag variable $_sourced_=${!_sourced_}"

if [ -z "${!_sourced_}" ]; then
    eval "$_sourced_=1"
    echo "It is the first time to source script"
else
    echo "The script have been sourced"
fi

接下来,我们在当前环境下多次导入上面的脚本:

[kodango@devops workspace]$ source sourced.sh 
Flag variable __sourced_381__=
It iss the first time to source script
[kodango@devops workspace]$ source sourced.sh 
Flag variable __sourced_381__=1
The script have been sourced

通过标志变量,现在我们就能避免一个脚本被多次导入了。不知道大家还有没有更好的方法来处理这种情况?