4.1 Shell中特殊且重要的变量
4.1.1 Shell中的特殊位置参数变量
在Shell中存在一些特殊且重要的变量,例如:$0、$1、$#,我们称之为特殊位置参数变量。要从命令行、函数或脚本执行等处传递参数时,就需要在Shell脚本中使用位置参数变量。表4-1为常用的特殊位置参数变量的说明。
表4-1 常用的特殊位置参数变量说明
1. $1 $2…$9 ${10} ${11}..特殊变量实践
范例4-1:测试$n(n为1..15)的实践。
编写如下的p.sh脚本,输入内容为“echo $1”,并执行测试:
[root@oldboy scripts]# cat p.sh echo $1 #<==脚本功能是打印脚本传递的第一个参数的值。 [root@oldboy scripts]# sh p.sh oldboy #<==传入一个oldboy字符串参数,赋值给脚 本中的$1。 oldboy #<==把传入的oldboy参数赋值给脚本中的$1并输出,因此输出结果为oldboy。 [root@oldboy scripts]# sh p.sh oldboy oldgirl #<==传入两个字符串参数,但脚本不 会接收第二个参数,参数默认是以空格分隔。 oldboy #<==只输出了oldboy,因为脚本里没有加入$2,因此,无法接收第二个参数oldgirl 字符串。 [root@oldboy scripts]# sh p.sh "oldboy oldgirl" #<==加引号扩起来的内容传参, 会作为一个字符串参数。 oldboy oldgirl #<==虽然都打印了,但是这些内容是作为一个参数传递给$1的。
范例4-2:在脚本中同时加入$1和$2,并进行测试。
[root@oldboy scripts]# cat p.sh echo $1 $2 [root@oldboy scripts]# sh p.sh longge bingbing #<==同时传入两个字符串参数。 longge bingbing [root@oldboy scripts]# sh p.sh "longge bingbing" oldgirl #<==传入两个字符串参 数,对第一个有空格的多个字符串用双引号引起来。 longge bingbing oldgirl
范例4-3:设置15个位置参数($1~$15),用于接收命令行传递的15个参数。
[root@oldboy ~]# echo \${1..15} #<==利用大括号输出15个位置参数,学会了 该命令就不用手敲代码了。 $1 $2 $3 $4 $5 $6 $7 $8 $9 $10 $11 $12 $13 $14 $15 [root@oldboy ~]# echo \${1..15} >n.sh #<==利用大括号输出15个位置参数并定向到 文件n.sh里。 [root@oldboy ~]# cat n.sh $1 $2 $3 $4 $5 $6 $7 $8 $9 $10 $11 $12 $13 $14 $15 [root@oldboy scripts]# cat n.sh echo $1 $2 $3 $4 $5 $6 $7 $8 $9 $10 $11 $12 $13 $14 $15 #<==增加echo命令打 印所有参数,这是最终的测试代码,前面的都是为了写代码,读者也可以用vim编辑录入。 [root@oldboy scripts]# echo {a..z} #<==测试打印26个字母a~z并以空格分隔。 a b c d e f g h i j k l m n o p q r s t u v w x y z [root@oldboy scripts]# sh n.sh {a..z} #<==传入26个字母a~z,以空格分隔,作为 26个参数。 a b c d e f g h i a0 a1 a2 a3 a4 a5 #<==位置参数的数字大于9后,输出的内容就不对了。
其实,当我们使用vim编辑脚本时,利用vim的高亮功能就会看到脚本呈现异常的颜色显示,如图4-1所示。
图4-1 vim高亮功能呈现脚本的异常
当位置参数数字大于9时,需要用大括号将数字括起来,如下:
[root@oldboy scripts]# cat n.sh echo $1 $2 $3 $4 $5 $6 $7 $8 $9 ${10} ${11} ${12} ${13} ${14} ${15} #<==数字大于9,必须给数字加大括号才能输出正确内容。
图4-2是加上括号后的高亮颜色,可以看到,颜色已经是正常的了,vim的语法高亮显示对编程很有帮助,有关vim的开发环境配置,见第16章。
图4-2 长方形线内为正常的颜色显示
以下是有关“$1, $2, $3…”这些位置参数的系统生产场景案例。对此,读者可以多参考rpcbind、NFS两个软件启动的脚本,这两个服务的启动脚本简单、规范。若是最小化安装的系统,则表示没有安装rpcbind、NFS,可以通过执行yum instlla nfs-utils rpcbind -y来安装。
在生产场景中,执行/etc/init.d/rpcbind start之后,rpcbind脚本后携带的start参数会传给脚本里的“$1”进行判断,脚本中传递参数的关键case语句节选如下:
case "$1" in #<==这里的$1用于接收执行此脚本命令行的第一个参数,规范用法是用双引号引起来。 start) #<==如果$1接收的值匹配start,则执行下文的start函数及内部的指令。 start #<==调用脚本中的start函数。 RETVAL=$? #<==这里是记录start函数执行的返回值,$?也是重要的变量,暂时可 以忽略,后面有介绍。 ;; stop) #<==如果$1接收的值匹配stop,则执行下文的stop函数及内部的指令。 stop RETVAL=$? ;; status) #<==如果$1接收的值匹配status,则执行下文的status函数及内部的指令。 status $prog RETVAL=$? ;; ……省略部分内容
说明:读者只需要关注特殊变量($1)的内容,case等其他语句后文会细讲。
2. $0特殊变量的作用及变量实践
$0的作用为取出执行脚本的名称(包括路径),下面是该功能的实践。
范例4-4:获取脚本的名称及路径。
[root@oldboy scripts]# cat n.sh echo $0
若不带路径执行脚本,那么输出结果就是脚本的名字,如下:
[root@oldboy scripts]# sh n.sh n.sh #<==$0获取的值就是脚本的名字,因此这里输出了n.sh。
若使用全路径执行脚本,那么输出结果就是全路径加上脚本的名字,如下:
[root@oldboy scripts]# sh /server/scripts/n.sh /server/scripts/n.sh #<==如果执行的脚本中带有路径,那么$0获取的值就是脚本的名字加路径。
当要执行的脚本为全路径时,$0也会带着路径。此时如果希望单独获取名称或路径,则可用范例4-5的方法。
范例4-5:dirname及basename命令自身的功能和用法。
[root@oldboy scripts]# dirname /server/scripts/n.sh /server/scripts #<==dirname命令的作用是获取脚本的路径。 [root@oldboy scripts]# basename /server/scripts/n.sh n.sh #<==basename命令的作用是获取脚本的名字。
说明:以后读者可以根据需求,用不同的命令获取对应的结果。
范例4-6:利用$0和上述命令(dirname、basename)分别取出脚本名称和脚本路径。
[root@oldboy scripts]# cat n.sh dirname $0 basename $0 [root@oldboy scripts]# sh /server/scripts/n.sh /server/scripts #<==这就是dirname $0的输出结果。 n.sh #<==这就是basename $0的输出结果。
有关“$0”这个位置参数的系统生产场景案例如下,其中采用rpcbind系统脚本。
[root@oldboy scripts]# tail -6 /etc/init.d/rpcbind #<==查看结尾6行。 echo $"Usage: $0 {start|stop|status|restart|reload|force- reload|condrestart|try-restart}" #<==$0的基本生产场景就是,当用户的输入不符合脚本的要求时,就打印脚本的名字及使用帮助。 RETVAL=2 ;; esac exit $RETVAL [root@oldboy scripts]# /etc/init.d/rpcbind #<==不带任何参数执行rpcbind脚本。 Usage: /etc/init.d/rpcbind {start|stop|status|restart|reload|force- reload|condrestart|try-restart} #<==上文/etc/init.d/rpcbind就是$0从脚本命令行获取的值,当用户输入不符合脚本设定的 要求时,打印脚本名字及预期的使用帮助。
3. $#特殊变量获取脚本传参个数的实践
范例4-7:通过$#获取脚本传参的个数。
[root@oldboy scripts]# cat q.sh echo $1 $2 $3 $4 $5 $6 $7 $8 $9 echo $# #<==此行是打印脚本命令行传参的个数。 [root@oldboy scripts]# sh q.sh {a..z} #<==传入26个字符作为26个参数。 a b c d e f g h i #<==只接收了9个变量,所以打印9个字符。 26 #<==传入26个字符作为26个参数,因此这里的数字为26,说明传入了26个参数。
范例4-8:根据用户在命令行的传参个数判断用户的输入,不合要求的给予提示并退出。
这是一个针对$0、$1、$#等多位置参数的综合型企业案例,脚本中可能包含了部分读者没有掌握的技术,这里只需要理解这几个位置参数就可以了,对于其他知识后面会有详细讲解。
首先来看条件表达式判断语句的写法,如下:
[root@oldboy scripts]# cat t1.sh [ $# -ne 2 ] && { #<==如果执行脚本传参的个数不等于2, echo "muse two args" #<==则给用户提示正确的用法。 exit 1 #<==由于不满足要求,因此退出脚本,返回值为1。 } echo oldgirl #<==满足了参数个数的传参要求后,就执行判断后的程序脚本,即打印oldgirl。 [root@oldboy scripts]# sh t1.sh muse two args #<==如果不加参数执行脚本,即不符合脚本要求,则直接给出提示。 [root@oldboy scripts]# sh t1.sh arg1 arg2 oldgirl #<==当参数满足要求后,打印oldgirl字符串。
然后是if判断语句的写法,如下:
[root@oldboy scripts]# cat t2.sh if [ $# -ne 2 ] #<==如果执行脚本传参的个数不等于2, then echo "USAGE:/bin/sh $0 arg1 arg2" #<==则给用户提示正确用法,注意此处的$0,打印 脚本名字及路径。 exit 1 #<==若不满足要求,则退出脚本,返回值为1。 fi echo $1 $2 #<==若参数满足要求,则打印$1和$2获取到的传参的字符串。 [root@oldboy scripts]# sh t2.sh #<==若不加参数执行脚本,则直接给出提示。 USAGE:/bin/sh t2.sh arg1 arg2 #<==t2.sh就是脚本中$0获取的值。 [root@oldboy scripts]# sh t2.sh oldboy oldgirl oldboy oldgirl #<==若参数满足要求,则打印$1和$2获取的字符串,即oldboy和oldgirl。
4. $*和$@特殊变量功能及区别说明
首先,请翻到本章的开头再重新温习一下$*和$@的作用,然后再来看范例。
范例4-9:利用set设置位置参数(同命令行脚本的传参)。
[root@oldboy scripts]# set -- "I am" handsome oldboy. #<==通过set设置三个 字符串参数,“--”表示清除所有的参数变量,重新设置后面的参数变量。 [root@oldboy scripts]# echo $# #<==输出参数的个数。 3 #<==共三个参数。 [root@oldboy scripts]# echo $1 #<==打印第一个参数值。 I am [root@oldboy scripts]# echo $2 #<==打印第二个参数值。 handsome [root@oldboy scripts]# echo $3 #<==打印第三个参数值。 oldboy.
测试$*和$@,注意,此时不带双引号:
[root@oldboy scripts]# echo $* #<==打印$*。 I am handsome oldboy. [root@oldboy scripts]# echo $@ #<==打印$@。 I am handsome oldboy. [root@oldboy scripts]# for i in $*; do echo $i; done #<==使用for循环输出$*测试。 I #<==($*)不加双引号,因此会输出所有参数,然后第一个参数"I am"也拆开输出了。 am handsome oldboy. [root@oldboy scripts]# for i in $@; do echo $i; done #<==使用for循环输出$@测试。 I #<==($@)不加双引号,因此会输出所有参数,然后第一个参数"I am"也拆开输出了。 am handsome oldboy.
测试"$*"和"$@",注意,此时带有双引号:
[root@oldboy scripts]# echo "$*" I am handsome oldboy. [root@oldboy scripts]# echo "$@" I am handsome oldboy. [root@oldboy scripts]# for i in "$*"; do echo $i; done #<==在有双引号的情况下"$*",参数里引号中的内容当作一个参数输出了! I am handsome oldboy. [root@oldboy scripts]# for i in "$@"; do echo $i; done #<==在有双引号的情况下,每个参数均以独立的内容输出。 I am #<==有双引号算一个参数。 handsome oldboy. #<==这才真正符合我们传入的参数需求,set -- "I am" handsome oldboy. [root@oldboy scripts]# for i; do echo $i; done #<==去掉in 变量列表,相当于有引 号的in "$@"。 I am handsome oldboy. #<==这才真正符合我们传入的参数需求,set -- "I am" handsome oldboy. [root@oldboy 02]# for i in $*; do echo $i; done #<==($*)不加双引号,因此会输出 所有参数,然后第一个参数"I am"也拆开输出了。 I am handsome oldboy. [root@oldboy scripts]# shift #<==用shift将位置参数移位(左移)。 [root@oldboy scripts]# echo $# 2 [root@oldboy scripts]# echo $1 #<==这里就打印原来$2的值了。 handsome [root@oldboy scripts]# echo $2 #<==这里就打印原来$3的值了。 oldboy.
有关set和eval命令的使用案例(特殊位置变量用法)见http://oldboy.blog.51cto. com/2561410/1175971。
4.1.2 Shell进程中的特殊状态变量
表4-2针对Shell进程的特殊状态变量进行了说明。
表4-2 Shell进程的特殊状态变量说明
提示:查找上述知识的方法为使用man bash命令,然后搜关键字“Special Parameters”。
1. $?特殊变量功能实践
范例4-10:执行命令后获取返回值(切换到oldboy用户下进行测试)。
[oldboy@oldboy ~]$ pwd #<==执行pwd命令,然后用“echo $? ”查看执行命令的状态返回值。 /home/oldboy [oldboy@oldboy ~]$ echo $? 0 #<==返回0,表示上一个命令的执行是成功的。 [oldboy@oldboy ~]$ ls /root #<==列表root目录的内容, ls: cannot open directory /root: Permission denied #<==提示权限不够。 [oldboy@oldboy ~]$ echo $? 2 #<==返回值为非0,表示上一个命令(ls /root)执行错误。注意:对于不同的错误,返回值是 不同的。 [oldboy@oldboy ~]$ rm -fr /root #<==删除/root目录及其子目录。 rm: cannot remove `/root': Permission denied #<==提示权限不够。 [oldboy@oldboy ~]$ echo $? 1 #<==返回值为1(非0)。 [oldboy@oldboy ~]$ oldboy #<==执行一个不存在的命令。 -bash: oldboy: command not found #<==提示命令找不到。 [oldboy@oldboy ~]$ echo $? 127 #<==返回值为127(非0)。
不同命令的执行结果中,“$? ”的返回值不尽相同,但在工作场景中,常用的就是0和非0两种状态,0表示成功运行,非0表示运行失败。
范例4-11:根据返回值来判断软件的安装步骤是否成功。
若使用源码编译安装软件,可以在每个步骤的结尾获取“$? ”来判断命令执行成功与否。例如:编译Nginx Web服务软件,执行make命令之后,新手不太容易确定编译是否正确,这时就可以使用“echo $? ”命令查看其返回值是否为0。下面是Nginx Web的基本安装过程,其中就是通过获取命令的返回值来确定命令的执行状况的。
[root@oldboy tools]# yum install pcre-devel openssl-devel -y [root@oldboy tools]# wget -q http://nginx.org/download/nginx-1.10.1.tar.gz [root@oldboy tools]# tar xf nginx-1.10.1.tar.gz [root@oldboy tools]# cd nginx-1.10.1 [root@oldboy nginx-1.10.1]# ./configure --prefix=/application/nginx-1.10.1 --user=nginx --group=nginx --with-http_ssl_module --with-http_stub_status_ module ……省略部分配置过程 nginx http proxy temporary files: "proxy_temp" nginx http fastcgi temporary files: "fastcgi_temp" nginx http uwsgi temporary files: "uwsgi_temp" nginx http scgi temporary files: "scgi_temp" [root@oldboy nginx-1.10.1]# echo $? 0 [root@oldboy nginx-1.10.1]# make ……省略部分编译过程 sed -e "s|%%PREFIX%%|/application/nginx-1.10.1|" \ -e "s|%%PID_PATH%%|/application/nginx-1.10.1/logs/nginx.pid|" \ -e "s|%%CONF_PATH%%|/application/nginx-1.10.1/conf/nginx.conf|" \ -e "s|%%ERROR_LOG_PATH%%|/application/nginx-1.10.1/logs/error.log|" \ < man/nginx.8 > objs/nginx.8 make[1]: Leaving directory `/home/oldboy/tools/nginx-1.10.1' [root@oldboy nginx-1.10.1]# echo $? 0 test -d '/application/nginx-1.10.1/logs' \ || mkdir -p '/application/nginx-1.10.1/logs' make[1]: Leaving directory `/home/oldboy/tools/nginx-1.10.1' [root@oldboy nginx-1.10.1]# echo $? 0
对于新手来说,在安装服务时,可以通过获取执行命令的返回值来确定命令的执行状态,从而快速确定命令是否执行成功。不过,有经验的技术人员不需要获取返回值,通过命令的最后过程输出就可以快速判断是否成功。
范例4-12:通过获取“$? ”的返回值确定网站备份是否正确。
提示:当对服务器的数据进行备份时,我们会在执行完关键命令,例如tar或cp后,通过获取返回值来判断命令是否成功,备份数据是否完整。
[root@oldboy ~]# cd /etc/ [root@oldboy etc]# tar zcf /opt/services.tar.gz ./services #<==打包备份 [root@oldboy etc]# echo $? #<==检查备份后的$?是否为0,如果为0则表示上一tar命 令执行成功,工作中会写成Shell脚本。 0
范例4-13:通过脚本控制命令及脚本执行后的返回值。
[root@oldboy scripts]# cat test4.sh [ $# -ne 2 ] && { #<==若参数个数不等于2, echo "must be two args." #<==则输出提示。 exit 119 #<==终止程序运行并以指定的119状态值退出程序,赋值给当前 Shell的“$? ”变量。 } echo oldgirl [root@oldboy scripts]# sh test4.sh #<==执行脚本。 must be two args. [root@oldboy scripts]# echo $? 119 #<==返回值为119,这个就是脚本中exit传过来的返回值。 [root@oldboy scripts]# sh test4.sh a1 a2 #<==若满足参数要求, oldgirl #<==则跳过不合要求的提示及以119状态退出的两条命令。 [root@oldboy scripts]# echo $? 0 #<==返回值为0,这个就是echo oldgirl正确执行后,“$? ”的结果。
在企业场景下,“$? ”返回值的用法如下:
1)判断命令、脚本或函数等程序是否执行成功。
2)若在脚本中调用执行“exit 数字”,则会返回这个数字给“$? ”变量。
3)如果是在函数里,则通过“return 数字”把这个数字以函数返回值的形式传给“$? ”。
范例4-14:查看系统脚本的应用情况,脚本名为/etc/init.d/rpcbind。
这里利用sed打印/etc/init.d/rpcbind脚本的第50-73行,然后分析脚本里“$? ”的使用情况!
[root@oldboy scripts]# sed -n '63,73p' /etc/init.d/rpcbind stop() { echo -n $"Stopping $prog: " killproc $prog #<==这是停止rpcbind的命令。 RETVAL=$? #<==将上述命令的返回值“$? ”赋值给RETVAL变量,用于后面的判断。 echo [ $RETVAL -eq 0 ]&&{ #<==这里就是判断,如果返回值为0,则执行下面的指令。 rm -f /var/lock/subsys/$prog rm -f /var/run/rpcbind* } return $RETVAL #<==如果返回值不等于0,则跳过条件表达式的判断,在这里直接作 为返回值传给执行stop函数的脚本。 }
提示:有关特殊位置和进程的状态变量,可以多参考这个简单却功能强大的脚本。
2. $$特殊变量功能及实践
范例4-15:获取脚本执行的进程号(PID)。
[root@oldboy scripts]# cat test_pid.sh #<==编写一个简单的脚本。 echo $$ >/tmp/a.pid #<==获取$$的值,并重定向到/tmp/a.pid里。 sleep 300 #<==休息300秒,模拟守护进程不退出。 [root@oldboy scripts]# ps -ef|grep test_pid|grep -v grep [root@oldboy scripts]# sh test_pid.sh & #<==在后台运行脚本,&符号表示在后台运行。 [1] 10397 #<==这是脚本的进程号。 [root@oldboy scripts]# ps -ef|grep test_pid|grep -v grep root 10397 10292 0 15:57 pts/0 00:00:00 sh test_pid.sh #<==这是脚本 的进程号。 [root@oldboy scripts]# cat /tmp/a.pid 10397 #<==这是$$对应的值。
提示:到这里大家应该明白了吧,$$就是获取当前执行的Shell脚本的进程号。
范例4-16:实现系统中多次执行某一个脚本后的进程只有一个(此为$$的企业级应用)。
说明:有时执行定时任务脚本的频率比较快,并不知道上一个脚本是否真的执行完毕,但是,业务要求同一时刻只能有一个同样的脚本在运行,此时就可以利用$$获取上一次运行的脚本进程号,当程序重新运行时,根据获得的进程号,清理掉上一次的进程,运行新的脚本命令,脚本如下:
[root@oldboy scripts]# cat pid.sh # ! /bin/sh pidpath=/tmp/a.pid #<==定义pid文件。 if [ -f "$pidpath" ] #<==如果pid文件存在,则执行then后面的命令。 then kill `cat $pidpath` >/dev/null 2>&1 #<==杀掉与前一个进程号对应的进程。 rm -f $pidpath #<==删除pid文件。 fi echo $$ >$pidpath #<==将当前Shell进程号记录到pid文件里。 sleep 300
执行结果如下:
[root@oldboy scripts]# ps -ef|grep pid.sh|grep -v grep [root@oldboy scripts]# sh pid.sh & #<==后台运行脚本。 [1] 10617 [root@oldboy scripts]# ps -ef|grep pid.sh|grep -v grep #<==查看启动的脚本进程。 root 10617 10462 0 16:20 pts/1 00:00:00 sh pid.sh #<==只有一个。 [root@oldboy scripts]# sh pid.sh & #<==多次运行脚本,每次都会将上一次运行的杀掉。 [2] 10624 [root@oldboy scripts]# sh pid.sh & #<==多次运行脚本,每次都会将上一次运行的杀掉。 [3] 10628 [1] Terminated sh pid.sh [root@oldboy scripts]# ps -ef|grep pid.sh|grep -v grep root 10628 10462 0 16:20 pts/1 00:00:00 sh pid.sh #<==发现无论运行 多少次脚本,都只有一个进程。 [2]- Terminated sh pid.sh [root@oldboy scripts]# ps -ef|grep pid.sh|grep -v grep root 10628 10462 0 16:20 pts/1 00:00:00 sh pid.sh #<==发现无论运行多 少次脚本,都只有一个进程。
提示:这是一个生产案例的简单模拟,脚本用于执行启动或定时任务时,相同的脚本中只能有一个在运行,当新脚本运行时,必须关闭未运行完或未退出的上一次的同名脚本进程。
3. $_特殊变量功能说明及实践
$_的作用是获得上一条命令的最后一个参数值,此功能用得不多,了解即可。
范例4-17:$_参数的示例。
[root@oldboy scripts]# /etc/init.d/rpcbind start oldboy [root@oldboy scripts]# echo $_ #<==打印上一条命令的最后一个参数值,即oldboy。 oldboy [root@oldboy scripts]# /etc/init.d/rpcbind stop oldgirl Stopping rpcbind: [ OK ] [root@oldboy scripts]# echo $_ #<==打印上一条命令的最后一个参数值,即oldgirl。 oldgirl
4. $!特殊变量功能说明及实践
$!的功能类似于$$,只不过作用是获取上一次执行脚本的pid,对此,了解即可。
范例4-18:$!的功能示例。
[root@oldboy scripts]# ps -ef|grep pid.sh|grep -v grep [root@oldboy scripts]# sh pid.sh & #<==后台运行pid.sh脚本。 [1] 10760 [root@oldboy scripts]# echo $! #<==获取前一次执行脚本pid.sh的进程号。 10760 [root@oldboy scripts]# ps -ef|grep pid.sh|grep -v grep root 10760 10462 0 16:44 pts/1 00:00:00 sh pid.sh