shell如何模块化和复用——shell深入学习
2011-09-18 星期天 阴天
基本上所有的编程语言都支持模块化,以达到功能复用的效果。比如java和python的import xxx。C/C++的include。那么shell支持模块化吗?
shell本质上并不支持模块化,但是有些机制可以使它达到类似的效果。
首先要了解有两种方式可以执行一个shell脚本:
1. 一种是新产生一个shell,然后执行相应的shell scripts:
方法是在scripts文件开头加入以下语句
#!/bin/bash
一般的script文件(.sh)即是这种用法。这种方法先启用新的sub-shell(新的子进程),然后在其下执行命令。
也可以指定shell类型,如:
$sh script文件
2. 一种是在当前shell下执行,不再启用其他shell:
方法是使用source命令,不再产生新的shell,而在当前shell下执行一切命令。
也有两种语法:
$source script文件
或者直接用点号:. script文件(sh 只支持点号,不支持source命令,所以建议使用点号)
一个非常形象的类比是:shell的source就是C中的include。
注意:shell不会判断一个shell脚本是不是被导入多次,每次source(或者点号)scriptFile,都会在当前shell中执行scriptFile。这点和C的include是一样的。正如C可以使用条件包含避免重复导入头文件,shell也有类似的机制。这个我们在下面会讲到。
了解了背后的原理和机制,现在让我们回过头来看看client shell中如何导入module shell。
一个例子胜过千言万语,我们将采用循序渐进的方式逐渐修复到最完美的方案。
先解决最简单的导入情况——module shell和client shell在一个文件夹下:
子模块定义:
forrest@forrest-laptop:~/study/shell$ cat amodule.sh
#!/bin/bash
global_var="This is a global variable define in amodule."
say_hello(){
global_var_define_in_function="This is a global variable define in a function in amodule."
local local_var="This is a local variable define in a function in amodule."
echo "Hello, $1. This is a function define in amodule."
}
使用脚本定义:
forrest@forrest-laptop:~/study/shell$ cat main.sh
#!/bin/bash
. ./amodule.sh
say_hello "Forrest Gump"
echo "global_var=$global_var"
echo "global_var_define_in_function=$global_var_define_in_function"
echo "local_var=$local_var"
执行结果:
forrest@forrest-laptop:~/study/shell$ ./main.sh
Hello, Forrest Gump. This is a function define in amodule.
global_var=This is a global variable define in amodule.
global_var_define_in_function=This is a global variable define in a function in amodule.
local_var=
可以看到,在main.sh中通过source amodule.sh,main.sh可以直接使用amodule.sh中定义的全局变量和函数。而局部变量是拿不到的。
但是在比较大型的shell项目中,module shell和client shell往往不在一个目录下,而且module shell之间还会相互依赖(导入),你会发现使用相对路径会出现找不到module shell的情况。看下面这个比较复杂而真实的例子:
forrest@forrest-laptop:~/study/shell/svn_tools$ tree
.
├── modules
│ ├── log.sh
│ ├── svn_core.sh
│ └── utils.sh
└── svncobranches.sh
1 directory, 4 files
依赖(导入)关系是:
svncobranches.sh导入modules/svn_core.sh,svn_core.sh导入utils.sh和log.sh,log.sh导入utils.sh。当然,svncobranches.sh也可能依赖modules/log.sh和modules/utils.sh,但是通过svn_core.sh已经间接导入了。
一般来说是这样的导入方式:
forrest@forrest-laptop:~/study/shell/svn_tools$ cat svncobranches.sh
#!/bin/bash
. modules/svn_core.sh
forrest@forrest-laptop:~/study/shell/svn_tools$ cat modules/svn-core.sh
. utils.sh
. log.sh
forrest@forrest-laptop:~/study/shell/svn_tools$ cat modules/log.sh
. utils.sh
但是因为我们是运行svncobranches.sh,当svncobranches.sh在source svn_core.sh时,svn_core.sh的相对路径不是在modules目录,而不是在svn_tools目录。这样svn_core.sh导入utils.sh和log.sh就会出错:
forrest@forrest-laptop:~/study/shell/svn_tools$ sh svncobranches.sh
.: 1: utils.sh: not found
而如果我们不是在svn_tools目录下运行svncobranches.sh,那么svncobranches.sh导入. modules/svn_core.sh就已经报错了:
forrest@forrest-laptop:~$ sh ~/study/shell/svn_tools/svncobranches.sh
.: 5: Can't open modules/svn_core.sh
这个问题更严重了,因为我们不能强迫用户一定在client shell所在目录下运行我们的client shell脚本。如何处理呢?
注意到shell的source是这么一个查找机制:被source的module shell script,shell会在$PATH环境变量中搜索。根据这个信息,将我们的modules目录放在环境变量$PATH中,就可以不用指定相对或者绝对路径了。
但是关键在于第一个source,就是你的client shell导入modules shell,必须找到你当前的运行目录。这可以通过dirname得到:
forrest@forrest-laptop:~/study/shell/svn_tools$ cat svncobranches.sh
#!/bin/bash
# determine base directory; preserve where you're running from
basedir=$(dirname $0)
#echo $basedir
export PATH=$PATH:$basedir/modules
. svn_core.sh
forrest@forrest-laptop:~/study/shell/svn_tools$ cat modules/svn-core.sh
. utils.sh
. log.sh
### svn上将指定的SVN URL co到本地指定的目录,这里只是演示,打个log而已。
# svncobranch svnurl path
svncobranch()
{
local svnurl=$1
local path=$2
log "svncobranch($svnurl, $path)"
}
forrest@forrest-laptop:~/study/shell/svn_tool/modules$ cat log.sh
# log(meg, loglevel)
log()
{
datetime=`date +"%y-%m-%d %H:%M:%S"`
message=$1
if [ -z "$2" ]
then
loglevel="INFO"
else
loglevel=$2
fi
if [ ! -d log/ ]; then
mkdir log
fi
logfname=`removepostfix $0`
echo "$datetime [$0] $loglevel :: $message" | tee -a "log/$logfname.log"
}
log_error()
{
log "$1" "ERROR"
}
log_info()
{
log "$1" "INFO"
}
log_debug()
{
log "$1" "DEGUG"
}
log_warn()
{
log "$1" "WARN"
}
forrest@forrest-laptop:~/study/shell/svn_tools/modules$ cat utils.sh
# trim(str)
# remove blank space in both side
trim()
{
echo $*
}
# remove the postfix
# svncobranches.sh ==> svncobranches
removepostfix()
{
filename=$(basename "$1")
echo "$filename" | sed 's/.sh$//'
}
forrest@forrest-laptop:~$ sh ~/study/shell/svn_tool/svncobranches.sh
可以看到,我们的模块化策略其实是变成这样了:由client shell将整个modules目录放入到PATH环境变量中,那么在该shell进程中所有的导入都不需要指定相对或者绝对路径。好像它们都在同一个目录下一样。
上面的程序在一般情况下运行良好,不过有个小问题,就是如果用户对你的shell建立了一个软链接,那么需要follow symbol link:
forrest@forrest-laptop:~$ ln -s /home/forrest/study/shell/svn_tools/svncobranches.sh svncobranches.sh
forrest@forrest-laptop:~$ sh svncobranches.sh
.
.: 10: svn_core.sh: not found
使用readlink -f $0可以得到真实的文件路径:
forrest@forrest-laptop:~/study/shell/svn_tools$ cat svncobranches.sh
forrest@forrest-laptop:~$ sh svncobranches.sh
Path to test.sh is /home/forrest/study/shell/svncobranches.sh
basedir=/home/forrest/study/shell
最后一个问题,就是我们在前面一开始提到的重复导入问题:如何避免一个module shell被导入多次呢?
首先看一下如果发生这种事情,会出现什么状况。
我们知道log.sh依赖于utils.sh,假如我们让utils.sh也依赖于log.sh(这个很正常,log本来就是很基础的服务。)
forrest@forrest-laptop:~/study/shell/svn_tools/modules$ cat utils.sh
. log.sh
# trim(str)
# remove blank space in both side
trim()
{
echo $*
}
# remove the postfix
# svncobranches.sh ==> svncobranches
removepostfix()
{
filename=$(basename "$1")
echo "$filename" | sed 's/.sh$//'
}
执行结果是根本执行不了,直接包错了:
forrest@forrest-laptop:~/study/shell/svn_tools$ sh svncobranches.sh
.: 1: 3: Too many open files
这是因为shell在source的时候陷入了死循环了,因为log.sh和utils.sh互相依赖导致的。
#ifndef MYHEADER_H
#define MYHEADER_H
. . . // This will be seen by the compiler only once
#endif /* MYHEADER_H */
我们可以采用类似的方式——export就相当于#define宏。
if [ "$log" ]; then
return
fi
export log="log.sh"
. utils.sh
...
最终所有代码如下:
client shell定义如下:
forrest@forrest-laptop:~/study/shell/svn_tools$ cat svncobranches.sh
#!/bin/bash
# determine base directory; preserve where you're running from
realpath=$(readlink -f "$0")
export basedir=$(dirname "$realpath") #export basedir, so that module shell can use it. log.sh. e.g.
export filename=$(basename "$realpath") #export filename, so that module shell can use it. log.sh. e.g.
export PATH=$PATH:$basedir/modules
. svn_core.sh
forrest@forrest-laptop:~/study/shell/svn_tools$ cd modules/
forrest@forrest-laptop:~/study/shell/svn_tools/modules$ cat svn_core.sh
if [ "$svn_core" ]; then
return
fi
export svn_core="svn_core.sh"
. utils.sh
. log.sh
### svn上将指定的SVN URL co到本地指定的目录
# svncobranch svnurl path
svncobranch()
{
local svnurl=$1
local path=$2
log "svncobranch($svnurl, $path)"
}
forrest@forrest-laptop:~/study/shell/svn_tools/modules$ cat log.sh
if [ "$log" ]; then
return
fi
export log="log.sh"
. utils.sh
# log(meg, loglevel)
log()
{
datetime=`date +"%y-%m-%d %H:%M:%S"`
message=$1
if [ -z "$2" ]
then
loglevel="INFO"
else
loglevel=$2
fi
outdir="$basedir/log"
if [ ! -d "$outdir" ]; then
mkdir "$outdir"
fi
logname=`removepostfix $filename`
echo "$datetime [$0] $loglevel :: $message" | tee -a "$outdir/$logname.log"
}
log_error()
{
log "$1" "ERROR"
}
log_info()
{
log "$1" "INFO"
}
log_debug()
{
log "$1" "DEGUG"
}
log_warn()
{
log "$1" "WARN"
}
forrest@forrest-laptop:~/study/shell/svn_tools/modules$ cat utils.sh
if [ "$utils" ]; then
return
fi
export utils="utils.sh"
. log.sh
# trim(str)
# remove blank space in both side
trim()
{
echo $*
}
# remove the postfix
# svncobranches.sh ==> svncobranches
removepostfix()
{
filename=$(basename "$1")
echo "$filename" | sed 's/.sh$//'
}
上面代码在笔者机器上全部测试过,应该没有问题。
参考资料:
分享到:
相关推荐
这提高了开发效率,使得代码更加模块化和易于维护。例如,你可以创建一个`ls`命令类,实现列出目录内容的功能;再创建一个`cd`命令类,实现改变当前工作目录的功能。然后将这两个类实例注册到命令注册表中,你的...
**Shell基础——Shell入门资料** ...通过深入学习和实践,你可以熟练掌握Shell,提升在Linux系统中的操作效率,进行更复杂的系统管理和自动化任务。记得不断练习,多写脚本,将理论知识转化为实际技能。
本书《Shell脚本编程诀窍——适用于Linux、Bash等》由Steve Parker撰写,旨在帮助读者深入理解和掌握Shell脚本的编写技巧。 Shell脚本的基础是Bash(Bourne-Again SHell),它是大多数Linux发行版的默认Shell。Bash...
每个PDF文件可能对应上述一个或多个主题的详细讲解,通过深入学习和实践,你将能够编写出高效、灵活的Shell脚本,提升Linux和Unix系统的管理效率。记得结合实际操作来巩固理论知识,以达到最佳学习效果。
通过深入学习和实践这两个CHM文档提供的内容,你将能够编写出高效、灵活的Shell脚本来解决各种自动化任务,提高Linux系统管理的效率。在实际应用中,不断积累经验和技巧,你的Shell编程技能会更加炉火纯青。
《跟老男孩学Linux运维:Shell编程实战》这本书主要涵盖了Linux运维中的一个重要技能——Shell脚本编程。在Linux系统管理中,Shell脚本是自动化任务处理、系统维护和程序开发的重要工具。以下将深入探讨书中的核心...
《Linux与Unix Shell编程指南》是一本专注于操作系统交互式接口——Shell编程的教程,主要针对Linux和Unix系统。Shell作为用户与操作系统内核之间的桥梁,是进行系统管理、自动化任务执行以及程序开发的重要工具。本...
Linux Shell编程从入门到精通是一本专为初学者设计的指南,由张昊主编,旨在帮助读者快速掌握...通过阅读这本书,你将能够熟练地运用Shell进行系统自动化,提高工作效率,并为后续更深入的Linux系统学习打下坚实基础。
4. **条件语句和循环**:深入学习`if`、`else`、`case`结构以及`for`、`while`循环,用于编写条件化和重复执行的脚本。 5. **函数**:如何定义和调用Shell函数,以实现代码复用和组织。 6. **脚本编写**:掌握编写...
1. **函数**:Bash允许定义和调用用户自定义函数,有助于代码复用和模块化。 2. **数组**:Bash从版本4开始支持数组,可以存储一组相关数据。 3. **命令替换**:`$(command)`或`` `command` ``用于执行命令并获取其...
在Linux或Unix系统中,Shell脚本是一种强大的工具,用于自动化日常任务和管理系统。在"11.25 shell修改文件.rar"这个主题中,我们将深入探讨如何利用Shell脚本来修改文件内容,提升系统管理效率。 【描述】: "11.25...
《跟老男孩学Linux运维:Shell编程实战》这本书主要涵盖了Linux运维中的核心技能之一——Shell编程,旨在帮助读者深入理解并熟练掌握Shell脚本的编写技巧。在Linux系统管理中,Shell脚本扮演着至关重要的角色,它能够...
通过深入学习,你可以理解Linux内核如何调度进程、管理内存,以及如何利用shell编写高效、实用的脚本。 总之,理解Linux内核和shell编程对于任何IT专业人士来说都是一项基础且重要的技能。无论你是系统管理员、...
**Linux教程 Shell精华文章** 在Linux操作系统中,Shell扮演着至关重要的角色,它是一...深入学习Bash,对于任何Linux用户来说都是一项宝贵的投资。阅读《Linux教程 Shell精华文章》将帮助你全面理解并熟练运用Bash。
总的来说,这个“希尔-归并排序——模板类”是一个很好的学习资源,特别是对于初学者,它展示了如何在实际编程中应用数据结构(如希尔排序和归并排序)以及如何使用模板类来实现泛型编程。通过理解并分析这个代码,...