1.5 各种引号的正确使用姿势
1)单引号与双引号
在编写脚本时我们经常需要用到引号,而Shell支持多种引号,如""(双引号)、''(单引号)、``(反引号)、\(转义符号)。这么多的符号,都是在什么情况下使用的呢?下面我们看几个案例。
[root@centos7~]# touch a b c #创建三个文件,分别是a、b、c [root@centos7~]# touch "a b c" #创建一个文件,空格是文件名的一部分
这里可以看出双引号的作用是引用一个整体,计算机会把引号中的所有内容当作一个整体看待。而不使用双引号时,创建的是三个不同的文件。当后期需要删除文件时,也会出现类似的问题。
[root@centos7~]# ls a a b c b c
这样的输出结果很容易让人误解,这里到底有几个文件?文件名到底是什么?
[root@centos7~]# rm -rf a b c #删除三个文件,分别是a、b和c [root@centos7~]# ls a b c [root@centos7~]# rm a b c rm: 无法删除"a": 没有那个文件或目录 rm: 无法删除"b": 没有那个文件或目录 rm: 无法删除"c": 没有那个文件或目录
因为这里没有使用双引号,所以系统理解的是需要删除a、b和c这三个文件,但其实现在系统中没有这三个文件,而只有一个文件,名称为“a b c”,其中空格也是文件名的一部分,这个文件应怎么删除呢?
[root@centos7~]# rm "a b c" rm:是否删除普通空文件"a b c"? y
通过使用双引号,成功删除了这个文件。在Linux系统中,除了可以使用双引号引用一个整体,还可以使用单引号引用一个整体,同时单引号还有另外一个功能,即可以屏蔽特殊符号(将特殊符号的特殊含义屏蔽,转化为字符表面的名义)。
[root@centos7~]# touch "a b c" [root@centos7~]# touch 'a b c'
上面两条命令因为没有特殊符号,所以使用双引号或单引号的作用是一样的。但是,当有特殊符号时,单引号和双引号不能互换,比如下面的例子。
[root@centos7~]# echo #
在Shell中,#符号有特殊含义,是注释符号。#符号及#符号后面的内容都会被程序理解为注释,而不会被执行,这条命令本来想通过屏幕输出一个#符号,但实际的输出结果却是空白行。如果我们希望输出这个#号,则可以使用单引号,将#符号的特殊含义屏蔽掉。
[root@centos7~]# echo '####'
另外,在Shell中$符号有提取变量值的特殊含义,而当我们需要直接使用$这个符号时,也需要使用单引号的屏蔽功能。
[root@centos7~]# test=18 [root@centos7~]# echo $test RMB 18 RMB [root@centos7~]# echo '$test RMB' #使用单引号后$符号就变成了一个普通符号 $test RMB
其实,在Linux中具有屏蔽功能的除单引号外,还有\符号,虽然\符号也可以实现屏蔽转义的功能,但\符号仅可以转义其后面的第一个符号,而单引号可以屏蔽引号内所有的特殊符号,如下所示。
[root@centos7~]# a=11 [root@centos7~]# b=22 [root@centos7~]# echo '$a$b' $a$b [root@centos7~]# echo \$a$b $a22 [root@centos7~]# echo \$a\$b $a$b [root@centos7~]# echo # #输出的是空白行,因为#符号被理解为注释 [root@centos7~]# echo '#' #正常输出#符号 # [root@centos7~]# echo $$ #显示当前进程的进程号 12384 [root@centos7~]# echo '$$' #屏蔽后正常输出$$符号 $$ [root@centos7~]# echo '&' #&符号默认为后台进程,需要屏蔽 & [root@centos7 /]# echo * #*符号代表当前目录下的所有文件 bin boot dev etc home lib lib64 media mnt nnb opt proc root run sbin srv sys tmp tt usr var [root@centos7 /]# echo '*' #屏蔽后正常输出字符 * [root@centos7~]# echo ~ #~符号默认代表用户的根目录 /root [root@centos7~]# echo '~'
~ [root@centos7~]# echo '()' ()
2)命令替换
最后,我们来了解``符号(反引号),反引号是一个命令替换符号,它可以使用命令的输出结果替代命令,下面我们看一个例子。
[root@centos7~]# tar -czf /root/log.tar.gz /var/log/
使用上面这条命令可以把/var/log目录下的所有数据备份到/root目录下,但是备份的文件名是固定的。如果需要系统执行计划任务,实现在每周星期五备份一次数据,然后新的备份就会把原有的备份文件覆盖(因为文件名是固定的)。到最后发现其实仅备份了最后一周的数据,前面的所有数据全部丢失!怎么解决这个问题呢?
[root@centos7~]# tar -czf /root/log-`date +%Y%m%d`.tar.gz /var/log/
这条命令依然使用tar命令进行备份。但是,因为使用了``符号实现命令替换,所以这里备份的文件名不再是date,而是date命令执行后的输出结果,即使用命令的输出结果替换date命令本身的字符串,最后备份的文件名类似log-20180725.tar.gz。文件名中具体的时间根据执行命令时的计算机系统时间而定。再看几个例子。
[root@centos7~]# echo "当前系统账户登录数量:`who | wc -l`" 当前系统账户登录数量:8 [root@centos7~]# cat /var/run/atd.pid #查看atd进程的进程号 1068 [root@centos7~]# kill `cat /var/run/atd.pid` #杀死atd进程 [root@centos7~]# rpm -ql at #查看at软件的文件列表 [root@centos7~]# ls -l `rpm -ql at` #查看文件列表的详细信息 [root@centos7~]# ls /etc/*.conf #查看/etc/目录下的所有以conf结尾的文件 /etc/asound.conf /etc/host.conf /etc/locale.conf /etc/pcp.conf /etc/sysctl.conf /etc/brltty.conf /etc/idmapd.conf /etc/logrotate.conf /etc/pnm2ppa.conf … …
[root@centos7~]# tar -czf x.tar.gz `ls /etc/*.conf` #将多个文件压缩打包为 一个文件 [root@centos7~]# tar -tf x.tar.gz #查看压缩包中的文件列表
反引号虽然很好用,但也有其自身的缺陷,比如容易跟单引号混淆,不支持嵌套(反引号中再使用反引号),为了解决这些问题,人们又设计了$()组合符号,功能也是命令替换,而且支持嵌套功能,如下面的案例所示。
[root@centos7~]# echo "当前系统账户登录数量:$(who | wc -l)" [root@centos7~]# ping -c2 $(hostname) [root@centos7~]# touch $(date +%Y%m%d).txt [root@centos7~]# echo "当前系统进程数量: $(ps aux | wc -l)" [root@centos7~]# echo $(echo 我是1级嵌套$(echo 我是2级嵌套))
1.6 千变万化的变量
水不流动则为死水,如果脚本使用的常量全是永恒不变的,那么脚本的功能就不够灵活,仅是一个可以满足特定需求的固定脚本。如果水流动了,就会出现千姿百态的形态;脚本如果使用了变量,也会变得更加灵活和多变。就像现实生活中气温和气压是实时变化的数据量一样,脚本需要处理的计算机数据往往也是实时变化的。在Linux系统中,变量分为系统预设变量和用户自定义变量。
首先,我们来看看自定义变量如何定义和调用。在Linux系统中,自定义变量的定义格式为变量名=变量值,变量名仅是用来找到变量值的一个标识而已,它本身没有任何其他功能。在定义变量时,变量名仅可以使用字母(大小写都可以)、数字和下画线(_)组合,而且不可以使用数字开头。此外,在工作中定义变量名时最好使用比较容易理解的单词或拼音,切记不要使用随意的字符给变量命名,没有规律的变量名会让脚本的可阅读性变得极差!表1-4中列举了几个合法和非法的变量名示例,需要注意的是,定义变量时等号两边不可以有空格。
表1-4 变量名示例
其次,当需要读取变量值时,需要在变量名前添加一个美元符号“$”;而当变量名与其他非变量名的字符混在一起时,需要使用{}分隔。
最后,如果需要取消变量的定义,则可以使用unset命令删除变量。
[root@centos7~]# hello = 123 #错误定义,等号两边不可以有空格 bash: hello: command not found... [root@centos7~]# test=123 #定义变量,变量名为test,值为123 [root@centos7~]# echo $test #调用变量,提取变量的值 [root@centos7~]# echo $testRMB
上面这条命令的返回值为空,因为没有定义一个名称是testRMB的变量,而且实际需要输出的应该是123RMB。此时就需要使用{}分隔变量名和其他字符。
[root@centos7~]# echo ${test}RMB #正确返回123RMB 123RMB [root@centos7~]# echo $test-yuan 123-yuan [root@centos7~]# echo $test:yuan 123:yuan [root@centos7~]# echo $test yuan 123 yuan [root@centos7~]# unset test #取消变量定义 [root@centos7~]# echo $test #返回的结果为空
虽然这三条命令都没有使用{}分隔变量名与其他字符,但最后返回值也不为空白,因为Shell变量名称仅可以由字母、数字、下画线组成,不可能包括特殊符号(如横线、冒号、空格等),所以系统不会把特殊符号当作变量名的一部分,系统会理解变量名为test,后面是其他跟变量名无关的字符串。下面我们看一个简单的使用变量的案例。
脚本案例解析如下。
这个脚本中定义了三个变量,三个变量值都是命令的返回结果,因此每次执行脚本时变量值都有可能发生变化。但是,不管变量值怎么变化,脚本都可以在最后正常地输出这些变量值。
第一个变量,localip存储本机eth0网卡的IP地址,这里假设系统中有eth0网卡并且配置了IP地址。第二个变量,mem存储本机内存剩余的容量。第三个变量,cpu存储本机CPU 15min内的平均负载。
在获取这三个变量值的语句中,都使用了tr和cut命令,tr -s后面使用引号引用了一个空格,作用是将管道传送的数据中连续的多个空格合并为一个空格。如果-s选项后面使用引号引用其他的字符,则效果也一样,可以把多个连续的特定字符合并为一个字符。而使用cut命令,可以帮助我们获取数据的特定列(使用-f选项指定需要获取的列数),并且可以通过-d选项设置以什么字符为列的分隔符。具体参考下面的案例。
[root@centos7~]# echo "aaa bbb" | tr -s "a" #将多个连续的a合并为一个a a bbb [root@centos7~]# echo "a---b---c" | tr -s "-" #将多个连续的-合并为一个- a-b-c [root@centos7~]# echo "A B C" | cut -d" " -f2 #以空格为分隔符,获取第二列 B [root@centos7~]# echo "A-B-C" | cut -d"-" -f3 #以-为分隔符,获取第三列 C [root@centos7~]# echo "AcBcC" | cut -d"c" -f2 #以c为分隔符,获取第二列 B
上面介绍的是用户自定义变量,接下来了解系统预设变量。系统预设变量,顾名思义就是系统已经预先设置好的变量,不需要用户自己定义便可以直接使用的变量。系统预设变量基本都是以大写字母或使用部分特殊符号为变量名。表1-5中列举了系统中常见的系统预设变量。
表1-5 常见的系统预设变量
系统预设变量可以细分为:环境变量、位置变量、预定义变量、自定义变量。在实际编写脚本时能够在合适的地方应用合适的变量即可,这里不再细化讲解。
编写脚本案例并调用这些系统预设变量,查看执行效果。
脚本解析如下。
当前登录的账户是root,所以$USER的值为root。默认root的账户ID号为0,所以$UID的值为0。管理员根目录为/root。因此,$HOME的值为/root。执行脚本时当前工作目录为管理员根目录,$PWD的值也是/root。$RANDOM每次执行脚本都可能返回不同的值,这里返回的值为11951。每次执行脚本时进程的进程号也是随机的,这里$$的结果是29772。$0显示当前脚本名称为sys_var.sh, $1是执行脚本的第1个参数(这里执行脚本时给了4个参数:A C 8 D),因此$1的值为A; $2是执行脚本的第2个参数,也就是C,其他位置变量依此类推。$*会显示所有参数的内容。
因为“$*”将所有参数视为一个整体,因此创建了一个名称为“A C 8 D”的文件,空格也是文件名的一部分。而“$@”将所有参数视为独立的个体,因为touch名称创建了4个文件,分别是A、C、8、D,使用ls -l命令可以查看得更清楚。
[root@centos7~]# ls -l -rw-r--r-- 1 root root 0 8月 1 23:45 8 -rw-r--r-- 1 root root 0 8月 1 23:45 A -rw-r--r-- 1 root root 0 8月 1 23:45 A C 8 D -rw-r--r-- 1 root root 0 8月 1 23:45 C -rw-r--r-- 1 root root 0 8月 1 23:45 D
“$? ”返回上一条命令的退出状态代码,脚本中先执行ls /etc/passwd,当这个命令被正确地执行后,“$0”返回的结果为0。而当执行ls /etc/pass命令时,因为pass文件不存在,所以该命令报错无法找到该文件。此时,“$? ”返回的退出状态码为2(正确为0,错误为非0,但根据错误的情况不同,每个程序返回的具体数字也会有所不同)。