Bash Shell初学
关键字:Shell
一:编一个shell脚本前应该注意的几个问题
当开始一个新的脚本的时候,问自己以下几个问题:
· 需要从用户或者用户环境来取得任何信息吗?
· 怎么样来存放那些信息?
· 需要创建文件吗?哪里和文件需要拥有什么样的权限和所有权?
· 我将用到什么命令?当在一个不同的系统使用脚本的时候,所有这些系统在要求的版本下有这些命令吗?用户需要什么提示吗?什么时候和为什么?
建立一个目录 ~/scripts
来存放你的脚本将会是个好主意。把此目录添加到PATH
变量中:
export PATH=”$PATH:~/scripts”
如果不加入PATH的话就需要用./scripts来执行。
如果你想在当前脚本执行脚本而不想启动一个新的shell,你可以使用source:
source script_name.sh
二:调试Bash脚本
当一些事情不能按照计划进行,你需要确定到底是什么导致了脚本运行失败。Bash提供了大量的调试特性。最通常的做法是使用 -x
选项来启动子shell,这将让整个脚本在调试模式下进行。每个命令和他附加参数的信息会在执行之前被展开并且送到标准输出打印。现在有一个全新的Bash调式工具出现在SourceForge。然而现在,你需要一个已经打过补丁的Bash-2.05。新的特性可能会在bash-3.0中出现。
2调试部分脚本
使用bash内建命令set可以让那些确定没有错误的部分以正常模式运行,而只对有错误的部分显示其debug信息。比如我们不确定在commented-script1.sh
里面w命令会做些什么,那么我们可以把它象这样包含起来:
set -x # activate debugging from here
w
set +x # stop debugging from here
你可以在同样的脚本里多次打开关闭调试模式。
下表给出了其他有用的bash选项概貌:
表2.1. 设置调试选项概览
短符号 | 长符号 | 结果 |
set -f | set -o noglob | 禁止特殊字符用于文件名扩展。 |
set -v | set -o verbose | 打印读入shell的输入行。 |
set -x | set -o xtrace | 执行命令之前打印命令。 |
横线用来激活一个shell选项,再使用一次将解除。
以下例子,我们在命令行里面证明这些选项:
willy:~/scripts>
set-v
willy:~/scripts>
ls
ls
commented-scripts.sh script1.sh
willy:~/scripts>
set+v
set +v
willy:~/scripts>
ls*
commented-scripts.sh script1.sh
willy:~/scripts>
set-f
willy:~/scripts>
ls*
ls: *: No such file or directory
willy:~/scripts>
touch*
willy:~/scripts>
ls
• commented-scripts.sh script1.sh
willy:~/scripts>
rm*
willy:~/scripts>
ls
commented-scripts.sh script1.sh
或者,这些模式也可以在脚本里面指定,只需在第一行shell的声明中加入需要的选项。选项可以叠加,和通常UNIX命令一样:
#!/bin/bash -xv
每当你找到脚本中的错误部分,你可以在每个你不确定的命令之前增加echo语句,这样你就会明确的看到哪里或者为什么脚本没有正常工作。在更高级的脚本中, echo可以插入到在不同的阶段显示变量表,因此可以检查到错误:
echo “Variable VARNAME is now set to $VARNAME.”
三:Bash环境
全局变量或者环境变量存在于所有的shell里面。env或者printenv命令能够通常用于显示环境变量。
本地变量只存在于当前shell。使用内建的不带选项的set命令将显示所有变量的列表(包括环境变量)和函数。在shell中设置一个变量,使用
VARNAME=”value”
在等号周围放置空格会造成错误。在对变量赋值得时候把内容字符串用引号引起来是一个良好的习惯:这样会降低出错的机会。
一个变量的建立后就像上面的例子那样仅仅存在于当前shell。它是本地变量:当前shell的子进程不会意识到这个的存在。为了把变量传递到子shell,我们需要使用内建的export命令把他们 输出 出来。被输出出来的变量就像环境变量一样,设置和输出变量通常用下面一步来完成:
export VARNAME=”value”
一个子shell能够改变从父shell变量继承过来的变量,但是在子shell所作的改变对父shell也没有影响。
特殊Bash变量
字符 | 定义 |
$* | 展开为位置参数,从1开始。当扩展发生在双引号时,它展开成一个单独的词,每个参数的值由IFS特殊变量的第一个字符分隔。 |
$@ | 展开为位置参数,从1开始。当在双引号里展开时,每个参数展开成独立的词。 |
$# | 把位置参数展开为十进制数字。 |
$? | 展开成最近执行的前台管道程序的退出状态。 |
$- | 一个连字符展开为当前选项标志 内部命令集 或者那些shell自己的集(如-i)。A hyphen expands to the current option flags as specified upon invocation, by the set built-in command, or those set by the shell itself (such as the -i). |
$$ | 展开成shell的进程ID。 |
$! | 展开成最近在后台(异步)执行的命令的进程ID。 |
$0 | 展开成shell或者shell脚本名。 |
转义字符用于去除一个单个字符的特殊意义。一个非引用的反斜杠,/,在Bash中被用作转义字符。
括号扩展是一种可以生成任意字符串的机制。括号扩展可以嵌套。每个扩展开的字符串的结果是未分类的;从左至右的顺序是保留的:
franky ~>
echosp{el,il,al}l
spell spill spall
括号扩展在任何其他扩展之前执行,而且在结果中保留对其它扩展有特殊意义的任何字符。完全是严格按照原文。Bash不对扩展的上下文或者括号之间的文本进行任何句法的解释。为了防止和参数扩展的冲突,字符串 “${” 被认为对于括号扩展是非法的。
一个正确形式的括号扩展必须包含未引用的一对括号,且至少一个未被引用的逗号。错误形式的括号扩展保留不做改变。
如果一个字以非引用的波浪线 (“~”)开始,所有在第一个未引用的斜杠之前的字符(或者所有字符,如果没有未引用的斜杠)被认为是一个tilde-prefix(tilde前缀)。如果在tilde前缀中的没有字符被引用,那么在tilde前缀中的跟随tilde的字符就被当作一个可能的登陆名。如果这个登陆名是空字符串,这个tilde就使用shell变量HOME
的值来代替。如果HOME
没有预先设置,就用用户执行shell的home目录来进行替换。除非,tilde前缀已经用指定的登陆名替换掉了。
例子:
franky ~>
exportPATH
=”$PATH:~/testdir”
~/testdir
将会扩展成为 $HOME/testdir
,所以如果 $HOME
是 /var/home/franky
,目录 /var/home/franky/testdir
将会加入到PATH
变量中去。
3.7. Shell参数和变量扩展
参数扩展的基本形式是 “${PARAMETER}”。“PARAMETER” 的值是要被替换的。当 “PARAMETER” 是一个含有多于一个数字的位置参数的时候,需要使用括号,或者当 “PARAMETER” 后接一个不会被解释成它名字一部分的字符的时候。 以下结构允许建立尚不存在的变量:
${VAR:=value}
例子:
franky ~>
echo$FRANKY
franky ~>
echo${FRANKY:=Franky}
Franky
命令替换允许一个命令的输出来替换这个命令本身。命令替换在一个命令这样封装的时候发生:
$(command)
或者象这样使用:
‘command‘
算术扩展允许一个算术表达式的赋值和结果的替换。算术扩展的格式是:
$(( EXPRESSION ))
无论在什么地方,Bash用户应该尝试带上方括号使用这个语法:
$[ EXPRESSION ]
然而,这样将仅仅计算EXPRESSION的结果,且不进行测试:
franky ~>
echo$[365*24]
8760
一个别名允许使用一个字符串来代替一个字当它作为一个简单命令的第一个字时候。shell维护一个可以用alias和unalias内建命令来设置或者取消的别名列表。
3.11. 更多Bash选项
我们已经讨论了一些对调试脚本很有用处的Bash选项。本节,我们将更深入地了解Bash的选项。
使用 -o
选项来set显示所有的shell选项:
willy:~>
set-o
allexport off
braceexpand on
emacs on
errexit off
hashall on
histexpand on
history on
ignoreeof off
interactive-comments on
keyword off
monitor on
noclobber off
noexec off
noglob off
nolog off
notify off
nounset off
onecmd off
physical off
posix off
privileged off
verbose off
vi off
xtrace off
12. 改变选项
需要临时改变当前的环境,或者需要在脚本中使用,我们不如使用set。使用 - 来开启一个选项 + 来关闭:
willy:~/test>
set-o
noclobber
willy:~/test>
touchtest
willy:~/test>
date >test
bash: test: cannot overwrite existing file
willy:~/test>
set+o
noclobber
willy:~/test>
date >test
四:程序结构
4.1 if使用的表达式
下表包含了一个组成TEST-COMMAND命令或者命令列表,称作 “要素primaries” 的概览。这些primaries放置在方括号中来表示一个条件表达式的测试。
意义 | |
[ | 如果 |
[ | 如果 |
[ | 如果 |
[ | 如果 |
[ | 如果 |
[ | 如果 |
[ | 如果 |
[ | 如果 |
[ | 如果 |
[ | 如果 |
[ | 如果 |
[ | 如果 |
[ | 如果文件描述符 |
[ | 如果 |
[ | 如果 |
[ | 如果 |
[ | 如果 |
[ | 如果 |
[ | 如果 |
[ | 如果 |
[ | 如果 |
[ | 如果 |
[ | 如果 |
[ | 如果 |
[ | 如果shell选项 “OPTIONNAME” 开启则为真。 |
| “STRING” 的长度为零则为真。 |
| “STRING” 的长度为非零non-zero则为真。 |
[ STRING1 == STRING2 ] | 如果2个字符串相同。 “=” may be used instead of “==” for strict POSIX compliance则为真。 |
|
|
[ STRING1 < STRING2 ] | 如果 “STRING1” sorts before “STRING2” lexicographically in the current locale则为真。 |
[ STRING1 > STRING2 ] | 如果 “STRING1” sorts after “STRING2” lexicographically in the current locale则为真。 |
[ ARG1 OP ARG2 ] | “OP” is one of |
表达式可以借以下操作符组合起来
操作 | 效果 |
[ ! EXPR ] | 如果EXPR是false则为真。 |
[ ( EXPR ) ] | 返回EXPR的值。这样可以用来忽略正常的操作符优先级。 |
[ EXPR1 -a EXPR2 ] | 如果EXPR1 and EXPR2全真则为真。 |
[ EXPR1 -o EXPR2 ] | 如果EXPR1或者EXPR2为真则为真。 |
嵌套if语句可能比较美观,但是只要你面临可能采取的一系列的不同动作时,你可能会迷惑。要处理复杂条件时,使用case语法:
case EXPRESSION in CASE1} COMMAND-LIST;; CASE2) COMMAND-LIST;; ... CASEN) COMMAND-LIST;; esac
每个分支是一个符合pattern的表达式。在COMMAND-LIST中首先符合的的命令就执行。 “|” 符号用来分割多个pattern, “)” 操作符中断一个pattern。每个分支加上他们的后继命令称作一个 子句 。每个 子句 必须以 “;;” 结尾。每个case语句以esac语句结束。
case $space in
[1-6]*)
Message=”All is quiet.”
;;
[7-8]*)
Message=”Start thinking about cleaning out some stuff. There’s a partition that is $space % full.”
;;
9[1-8])
Message=”Better hurry with that new disk... One partition is $space % full.”
;;
99)
Message=”I’m drowning here! There’s a partition at $space %!”
;;
*)
Message=”I seem to be running with an nonexitent amount of disk space...”
;;
esac
4.3.使用内建命令read
read [options] NAME1 NAME2 ... NAMEN
4.4.for循环
for NAME [in LIST ]; do COMMANDS; done
4.5. while循环
while CONTROL-COMMAND; do CONSEQUENT-COMMANDS; done
4.6.until循环
until循环和while循环非常相似,除了循环执行直到TEST-COMMAND执行成功。只要这个命令测试失败,循环就继续。语法和while循环一样:
until TEST-COMMAND; do CONSEQUENT-COMMANDS; done
4.7.shift内建命令
shift语句通常在事先不知道参数个数的情况下使用,比如用户可以随他们喜欢给出任意数量的参数。这种情况下,参数通常在一个while循环中用 (( $# )) 测试条件来处理。只要参数的数量大于零那么条件测试就为真。 $1
变量和shift语句处理每个参数。
4.8.declare内建命令
选项 | 含义 |
-a | 变量为数组。 |
-f | 仅使用函数名。 |
-i | 把变量当作整数来对待;变量被赋值之后就进行算术计算(参见 第3.4.6节 “算术扩展”)。 |
-p | 显示每个变量的属性和值。当使用 -p选项,其他选项就被忽略。 |
-r | 使得变量变为只读。这些变量不能被后来的赋值与语句赋值,同样也不可以unset。 |
-t | 给于每个变量trace属性。 |
-x | Mark each variable for export to subsequent commands via the environment. |
默认下,如果没有特别指明,变量可以拥有任何类型的数据
4.9.数组
ARRAY[INDEXNR]=value
ARRAY=(value1 value2 ... valueN)
对数组的变量解引用
为了指明在一个数组中的项目的内容,为了指向一个数组中的一个项目的内容,使用{}。这样是必须的,正如你可以从下面的例子看出,来绕过扩展操作符的shell解释。如果索引的数字是 @ 或者 *,一个数组的所有的成员都将被引用。
[bob in ~]
ARRAY
=(one two three)
[bob in ~]
echo${ARRAY[*]}
one two three
[bob in ~]
echo$ARRAY[*]
one[*]
[bob in ~]
echo${ARRAY[2]}
three
[bob in ~]
ARRAY[3]
=four
[bob in ~]
echo${ARRAY[*]}
one two three four
删除数组变量
unset内建命令用来删除数组或者数组成员
4.10.函数语法
函数使用以下2种形式。
function FUNCTION { COMMANDS; }
或者
FUNCTION () { COMMANDS; }
函数体必须以分号或者新行结尾。
五:使用shell来传送信号
5.1.在Bash中的控制信号
标准组合键 | 意义 |
Ctrl+C | 中断信号,向在前台运行的作业发送SIGINT。 |
Ctrl+Y | 延迟悬挂特征,使一个试图从终端读取输入的运行中的进程停止。控制权返回到shell,用户可以从前台,后台或者杀死进程。延迟挂起只有在支持这种特性的操作系统才存在。 |
Ctrl+Z | suspend信号,向正在运行的程序发送SIGTSTP,因此停止程序并且把控制权返回给shell。 |
可能有这样的情况,你不想使用你的脚本的用户不合时宜地通过键盘来结束进程,比如因为必须提供输入或者必须进行某些清理工作。 trap语句捕获到这些序列且能够被编制出来在不活这些信号时候执行一系列的命令。
trap语句的语法是这样的:
trap [COMMANDS] [SIGNALS]
意味着trap命令会捕捉在SIGNALS列出的可能带有或者没有SIG前缀的信号,或者信号数字。如果一个信号是0或者EXIT,那么COMMANDS在shell退出时候执行。如果其中一个信号是DEBUG,COMMANDS列表在每个简单命令后执行。一个信号也可以指定为ERR;这样的情况下COMMANDS在每次一个简单命令以非零状态退出时执行。注意这些命令不会在非零退出状态来自一个if语句时执行,或者来自一个while或者until循环。如果一个逻辑AND (&&) 或者OR (||) 出现在非零退出状态中,所有都不会执行,或者当一个命令的退出状态使用 ! 操作符进行取反。
除非遭遇一个非法的信号,否则trap命令的返回状态是0。trap命令带一组选项,在Bash的info页面中有记录。
这里有个非常简单的例子,从用户处捕捉Ctrl+C, upon which a message is printed. 当你尝试着不指定KILL信号来杀掉这个程序时,什么都不会发生:
#!/bin/bash
# traptest.sh
trap “echo Booh!” SIGINT SIGTERM
echo “pid is $”
while : # This is the same as “while true”.
do
sleep 60 # This script is not really doing anything.
done
六:总结
以上只是对shell编程一个框架的介绍,其中还有很多深入的细节,例如正则表达式,grep,sed,awk,还需要深入学习。在此基础上基本顺利阅读大部分shell脚本程序了。