SHELL编程之特殊符号
文章转自SHELL编程之特殊符号
微博ID:orroz
微信公众号:Linux系统技术
前言
本文是shell编程系列的第四篇,集中介绍了bash编程可能涉及到的特殊符号的使用。学会本文内容可以帮助你写出天书一样的bash脚本,并且顺便解决以下问题:
- 输入输出重定向是什么原理?
- exec 3<> /tmp/filename是什么鬼?
- 你玩过bash的关联数组吗?
- 如何不用if判断变量是否被定义?
- 脚本中字符串替换和删除操作不用sed怎么做?
- ” “和’ ‘有什么不同?
- 正则表达式和bash通配符是一回事么?
这里需要额外注意的是,相同的符号出现在不同的上下文中可能会有不同的含义。我们会在后续的讲解中突出它们的区别。
重定向(REDIRECTION)
重定向也叫输入输出重定向。我们先通过基本的使用对这个概念有个感性认识。
输入重定向
大家应该都用过cat命令,可以输出一个文件的内容。如:cat /etc/passwd。如果不给cat任何参数,那么cat将从键盘(标准输入)读取用户的输入,直接将内容显示到屏幕上,就像这样:
|
|
可以通过输入重定向让cat命令从别的地方读取输入,显示到当前屏幕上。最简单的方式是输入重定向一个文件,不过这不够“神奇”,我们让cat从别的终端读取输入试试。我当前使用桌面的终端terminal开了多个bash,使用ps命令可以看到这些终端所占用的输入文件是哪个:
|
|
通过第二列可以看到,不同的bash所在的终端文件是哪个,这里的pts/3就意味着这个文件放在/dev/pts/3。我们来试一下,在pts/2对应的bash中输入:
|
|
然后切换到pts/3所在的bash上敲入字符串,在pts/2的bash中能看见相关字符:
|
|
这只是个输入重定向的例子,一般我们也可以直接cat < /etc/passwd,表示让cat命令不是从默认输入读取,而是从/etc/passwd读取,这就是输入重定向,使用”<“。
输出重定向
绝大多数命令都有输出,用来显示给人看,所以输出基本都显示在屏幕(终端)上。有时候我们不想看到,就可以把输出重定向到别的地方:
[zorro@zorrozou-pc0 bash]$ ls /
bin boot cgroup data dev etc home lib lib64 lost+found mnt opt proc root run sbin srv sys tmp usr var
[zorro@zorrozou-pc0 bash]$ ls / > /tmp/out
[zorro@zorrozou-pc0 bash]$ cat /tmp/out
bin
boot
cgroup
data
dev
……
使用一个”>”,将原本显示在屏幕上的内容给输出到了/tmp/out文件中。这个功能就是输出重定向。
报错重定向
命令执行都会遇到错误,一般也都是给人看的,所以默认还是显示在屏幕上。这些输出使用”>”是不能进行重定向的:
[zorro@zorrozou-pc0 bash]$ ls /1234 > /tmp/err
ls: cannot access ‘/1234’: No such file or directory
可以看到,报错还是显示在了屏幕上。如果想要重定向这样的内容,可以使用”2>”:
[zorro@zorrozou-pc0 bash]$ ls /1234 2> /tmp/err
[zorro@zorrozou-pc0 bash]$ cat /tmp/err
ls: cannot access ‘/1234’: No such file or directory
以上就是常见的输入输出重定向。在进行其它技巧讲解之前,我们有必要理解一下重定向的本质,所以要先从文件描述符说起。
文件描述符(file descriptor)
文件描述符简称fd,它是一个抽象概念,在很多其它体系下,它可能有其它名字,比如在C库编程中可以叫做文件流或文件流指针,在其它语言中也可以叫做文件句柄(handler),而且这些不同名词的隐含意义可能是不完全相同的。不过在系统层,还是应该使用系统调用中规定的名词,我们统一把它叫做文件描述符。
文件描述符本质上是一个数组下标(C语言数组)。在内核中,这个数组是用来管理一个进程打开的文件的对应关系的数组。就是说,对于任何一个进程来说,都有这样一个数组来管理它打开的文件,数组中的每一个元素和文件是映射关系,即:一个数组元素只能映射一个文件,而一个文件可以被多个数组元素所映射。
|
|
shell在产生一个新进程后,新进程的前三个文件描述符都默认指向三个相关文件。这三个文件描述符对应的数组下标分别为0,1,2。0对应的文件叫做标准输入(stdin),1对应的文件叫做标准输出(stdout),2对应的文件叫做标准报错(stderr)。但是实际上,默认跟人交互的输入是键盘、鼠标,输出是显示器屏幕,这些硬件设备对于程序来说都是不认识的,所以操作系统借用了原来“终端”的概念,将键盘鼠标显示器都表现成一个终端文件。于是stdin、stdout和stderr就最重都指向了这所谓的终端文件上。于是,从键盘输入的内容,进程可以从标准输入的0号文件描述符读取,正常的输出内容从1号描述符写出,报错信息被定义为从2号描述符写出。这就是标准输入、标准输出和标准报错对应的描述符编号是0、1、2的原因。这也是为什么对报错进行重定向要使用2>的原因(其实1>也是可以用的)。
明白了以上内容之后,很多重定向的数字魔法就好理解了,比如:
|
|
这相当于只看报错信息。
|
|
这相当于只看正确输出信息。
|
|
将标准报错输出的,重定向到标准输出再输出。
|
|
“>>”表示追加重定向。
相信大家对&>>、1>&2、?2>&3、6>&8、>>file 2>&1这样的写法应该也都能理解了。进程可以打开多个文件,多个描述符之间都可以进行重定向。当然,输入也可以,比如:3<表示从描述符3读取。下面我们罗列一下其他重定向符号和用法:
Here Document:
语法:
|
|
这是一种特殊的输入重定向,重定向的内容并不是来自于某个文件,而是从当前输入读取,直到输入中写入了delimiter字符标记结束。用法:
|
|
这个例子可以看到,最后cat输出的内容都是在上面写入的内容,而且内容中不包括EOF,因为EOF是标记输入结束的字符串。这个功能在脚本中通常可以用于需要交互式处理的某些命令的输入和文件编辑,比如想在脚本中使用fdisk命令新建一个分区:
|
|
当然这个脚本大家千万不要乱执行,可能会修改你的分区表。其中要输入的内容,相信熟悉fdisk命令的人应该都能明白,我就不多解释了。
Here strings:
语法:
|
|
使用方式:
|
|
其实就是将<<<符号后面的字符串当成要输入的内容给cat,而不是定向一个文件描述符。这样是不是就相当于把cat当echo用了?
文件描述符的复制:
复制输入文件描述符:[n]<&word
如果n没有指定数字,则默认复制0号文件描述符。word一般写一个已经打开的并且用来作为输入的描述符数字,表示将制订的n号描述符在制定的描述符上复制一个。如果word写的是“-”符号,则表示关闭这个文件描述符。如果word指定的不是一个用来输入的文件描述符,则会报错。
复制输出文件描述符:[n]>&word
复制一个输出的描述符,字段描述参考上面的输入复制,例子上面已经讲过了。这里还需要知道的就是1>&-表示关闭1号描述符。
文件描述符的移动:
移动输入描述符:[n]<&digit-
移动输出描述符:[n]>&digit-
这两个符号的意思都是将原有描述符在新的描述符编号上打开,并且关闭原有描述符。
描述符新建:
新建一个用来输入的描述符:[n]<word
新建一个用来输出的描述符:[n]>word
新建一个用来输入和输出的描述符:[n]<>word
word都应该写一个文件路径,用来表示这个文件描述符的关联文件是谁。
下面我们来看相关的编程例子:
|
|
以上脚本要注意的地方是,一般输入输出重定向都是放到命令后面作为后缀使用,所以如果单纯改变脚本的描述符,需要在前面加exec命令。这种用法也叫做描述符魔术。某些特殊符号还有一些特殊用法,比如:
|
|
表示清空文件,当然也可以写成
|
|
因为”:”是一个内建命令,跟true是同样的功能,所以没有任何输出,所以这个命令清空文件的作用。
脚本参数处理
我们在之前的例子中已经简单看过相关参数处理的特殊符号了,再来看一下:
|
|
执行结果:
|
|
可以罗列一下:
$0:命令名。
$n:n是一个数字,表示第n个参数。
$#:参数个数。
$*:所有参数列表。
$@:同上。
实际上大家可以认为上面的0,1,2,3,#,*,@,?都是一堆变量名。跟aaa=1000定义的变量没什么区别,只是他们有特殊含义。所以$@实际上就是对@变量取值,跟$aaa概念一样。所以上述所有取值都可以写成${}的方式,因为bash中对变量取值有两种写法,另外一种是${aaa}。这种写法的好处是对变量名字可以有更明确的界定,比如:
|
|
内建命令shift可以用来对参数进行位置处理,它会将所有参数都左移一个位置,可以用来进行参数处理。使用例子如下:
|
|
执行效果:
|
|
其他的特殊变量还有:
$?:上一个命令的返回值。
$$:当前shell的PID。
$!:最近一个被放到后台任务管理的进程PID。如:
|
|
$-:列出当前bash的运行参数,比如set -v或者-i这样的参数。
$:”“算是所有特殊变量中最诡异的一个了,在bash脚本刚开始的时候,它可以取到脚本的完整文件名。当执行完某个命令之后,它可以取到,这个命令的最后一个参数。当在检查邮件的时候,这个变量帮你保存当前正在查看的邮件名。
数组操作
bash中可以定义数组,使用方法如下:
|
|
命令行扩展
大括号扩展
用类似枚举的方式创建一些目录:
|
|
可能还有这样用的:
|
|
这个命令的意思是:mv test/a.conf test/c.conf
~符号扩展
~:在bash中一般表示用户的主目录。cd ~表示回到主目录。cd ~zorro表示回到zorro用户的主目录。
变量扩展
我们都知道取一个变量值可以用$或者${}。在使用${}的时候可以添加很多对变量进行扩展操作的功能,下面我们就分别来看看。
${aaa:-1000}
这个表示如果变量aaa是空值或者没有赋值,则此表达式取值为1000,aaa变量不被更改,以后还是空。如果aaa已经被赋值,则原值不变:
|
|
${aaa:=1000}
跟上面的表达式的区别是,如果aaa未被赋值,则赋值成=后面的值,其他行为不变:
|
|
${aaa:?unset}
判断变量是否未定义或为空,如果符合条件,就提示?后面的字符串。
|
|
${aaa:+unset}
如果aaa为空或者未设置,则什么也不做。如果已被设置,则取+后面的值。并不改变原aaa值:
|
|
${aaa:10}
取字符串偏移量,表示取出aaa变量对应字符串的第10个字符之后的字符串,变量原值不变。
|
|
${aaa:10:15}
第二个数字表示取多长:
|
|
${!B*}
${!B@}
取出所有以B开头的变量名(请注意他们跟数组中相关符号的差别):
|
|
${ #aaa }
取变量长度:
|
|
${ parameter#word}
变量paramenter看做字符串从左往右找到第一个word,取其后面的字串:
|
|
这里需要注意的是,word必须是一个路径匹配的字符串,比如:
|
|
这个表示删除路径中匹配到的第一个zorro左边的所有字符,而这样是无效的:
|
|
因为此时zorro不是一个路径匹配。另外,这个表达式只能删除匹配到的左边的字符串,保留右边的。
${ parameter##word}
这个表达式与上一个的区别是,匹配的不是第一个符合条件的word,而是最后一个:
|
|
${ parameter%word}
${ parameter%%word}
这两个符号相对于上面两个相当于#号换成了%号,操作区别也从匹配删除左边的字符变成了匹配删除右边的字符,如:
|
|
以上#号和%号分别是匹配删除哪边的,容易记不住。不过有个窍门是,可以看看他们分别在键盘上的$的哪边?在左边的就是匹配删除左边的,在右边就是匹配删除右边的。
${ parameter/pattern/string}
字符串替换,将pattern匹配到的第一个字符串替换成string,pattern可以使用通配符,如:
|
|
${ parameter//pattern/string}
意义同上,不过变成了全局替换:
|
|
${parameter^pattern}
${parameter^^pattern}
${parameter,pattern}
${parameter,,pattern}
[zorro@zorrozou-pc0 bash]$ echo $aaa
abcdefg
[zorro@zorrozou-pc0 bash]$ echo ${aaa^}
Abcdefg
[zorro@zorrozou-pc0 bash]$ echo ${aaa^^}
ABCDEFG
[zorro@zorrozou-pc0 bash]$ aaa=ABCDEFG
[zorro@zorrozou-pc0 bash]$ echo ${aaa,}
aBCDEFG
[zorro@zorrozou-pc0 bash]$ echo ${aaa,,}
abcdefg
[zorro@zorrozou-pc0 bash]$ echo ls
ls
[zorro@zorrozou-pc0 bash]$ echo ls
3 arg1.sh array.sh auth_if.sh cat.sh for2.sh hash.sh name.sh ping.sh redirect.sh shift.sh until.sh
alias.sh arg.sh auth_case.sh case.sh exit.sh for.sh if_1.sh na.sh prime select.sh test while.sh
[zorro@zorrozou-pc0 bash]$ echo $((123+345))
468
[zorro@zorrozou-pc0 bash]$
[zorro@zorrozou-pc0 bash]$
[zorro@zorrozou-pc0 bash]$ echo $((345-123))
222
[zorro@zorrozou-pc0 bash]$ echo $((345*123))
42435
[zorro@zorrozou-pc0 bash]$ echo $((345/123))
2
[zorro@zorrozou-pc0 bash]$ echo $((345%123))
99
[zorro@zorrozou-pc0 bash]$ i=1
[zorro@zorrozou-pc0 bash]$ echo $((i++))
1
[zorro@zorrozou-pc0 bash]$ echo $((i++))
2
[zorro@zorrozou-pc0 bash]$ echo $i
3
[zorro@zorrozou-pc0 bash]$ i=1
[zorro@zorrozou-pc0 bash]$ echo $((++i))
2
[zorro@zorrozou-pc0 bash]$ echo $((++i))
3
[zorro@zorrozou-pc0 bash]$ echo $i
3
id++ id–
++id –id
- ! ~
**
- ! ~
- / %
- << >>
<= >= < >
== !=
&
^
|
&&
||
expr?expr:expr
= *= /= %= += -= <<= >>= &= ^= |=12另外可以进行算数运算的还有内建命令let:- << >>
[zorro@zorrozou-pc0 bash]$ i=0
[zorro@zorrozou-pc0 bash]$ let ++i
[zorro@zorrozou-pc0 bash]$ echo $i
1
[zorro@zorrozou-pc0 bash]$ i=2
[zorro@zorrozou-pc0 bash]$ let i=i**2
[zorro@zorrozou-pc0 bash]$ echo $i
4
[zorro@zorrozou-pc0 bash]$ i=0
[zorro@zorrozou-pc0 bash]$ ((i++))
[zorro@zorrozou-pc0 bash]$ echo $i
1
[zorro@zorrozou-pc0 bash]$ ((i+=4))
[zorro@zorrozou-pc0 bash]$ echo $i
5
[zorro@zorrozou-pc0 bash]$ ((i=i**7))
[zorro@zorrozou-pc0 bash]$ echo $i
78125
[zorro@zorrozou-pc0 bash]$ diff <(df -h) <(df)
1,10c1,10
< Filesystem Size Used Avail Use% Mounted on
< dev 7.8G 0 7.8G 0% /dev
< run 7.9G 1.1M 7.8G 1% /run
< /dev/sda3 27G 13G 13G 50% /
< tmpfs 7.9G 500K 7.8G 1% /dev/shm
< tmpfs 7.9G 0 7.9G 0% /sys/fs/cgroup
< tmpfs 7.9G 112K 7.8G 1% /tmp
< /dev/mapper/fedora-home 99G 76G 18G 82% /home
< tmpfs 1.6G 16K 1.6G 1% /run/user/120
< tmpfs 1.6G 16K 1.6G 1% /run/user/1000
Filesystem 1K-blocks Used Available Use% Mounted on
dev 8176372 0 8176372 0% /dev
run 8178968 1052 8177916 1% /run
/dev/sda3 28071076 13202040 13420028 50% /
tmpfs 8178968 500 8178468 1% /dev/shm
tmpfs 8178968 0 8178968 0% /sys/fs/cgroup
tmpfs 8178968 112 8178856 1% /tmp
/dev/mapper/fedora-home 103081248 79381728 18440256 82% /home
tmpfs 1635796 16 1635780 1% /run/user/120
tmpfs 1635796 16 1635780 1% /run/user/1000
|
|
?(pattern-list):匹配所给pattern的0次或1次;
*(pattern-list):匹配所给pattern的0次以上包括0次;
+(pattern-list):匹配所给pattern的1次以上包括1次;
@(pattern-list):匹配所给pattern的1次;
!(pattern-list):匹配非括号内的所给pattern。
|
|
[zorro@zorrozou-pc0 bash]$ shopt -u extglob
[zorro@zorrozou-pc0 bash]$ ls /etc/(a)
/etc/netdata:
apps_groups.conf charts.d.conf netdata.conf
/etc/pcmcia:
config.opts
[zorro@zorrozou-pc0 bash]$ shopt -u extglob
[zorro@zorrozou-pc0 bash]$ ls /etc/(a)
-bash: syntax error near unexpected token `(‘
[zorro@zorrozou-pc0 bash]$ type !
! is a shell keyword
[zorro@zorrozou-pc0 bash]$ echo hello
hello
[zorro@zorrozou-pc0 bash]$ echo $?
0
[zorro@zorrozou-pc0 bash]$ ! echo hello
hello
[zorro@zorrozou-pc0 bash]$ echo $?
1
[root@zorrozou-pc0 zorro]# [[ -f /etc/passwd ]]
[root@zorrozou-pc0 zorro]# echo $?
0
[root@zorrozou-pc0 zorro]# [[ -f /etc/pass ]]
[root@zorrozou-pc0 zorro]# echo $?
1
[zorro@zorrozou-pc0 bash]$ ls
3 arg1.sh array.sh auth_if.sh cat.sh for2.sh hash.sh name.sh ping.sh read.sh select.sh test while.sh
alias.sh arg.sh auth_case.sh case.sh exit.sh for.sh if_1.sh na.sh prime redirect.sh shift.sh until.sh
[zorro@zorrozou-pc0 bash]$ touch *sh
[zorro@zorrozou-pc0 bash]$ touch *sh
[zorro@zorrozou-pc0 bash]$ ls
3 arg1.sh array.sh auth_if.sh cat.sh for2.sh hash.sh name.sh ping.sh read.sh select.sh shift.sh until.sh
alias.sh arg.sh auth_case.sh case.sh exit.sh for.sh if_1.sh na.sh prime redirect.sh ‘*sh’ test while.sh
[zorro@zorrozou-pc0 bash]$ touch \
[zorro@zorrozou-pc0 bash]$ ls
‘\’ alias.sh arg.sh auth_case.sh case.sh exit.sh for.sh if_1.sh na.sh prime redirect.sh ‘*sh’ test while.sh
3 arg1.sh array.sh auth_if.sh cat.sh for2.sh hash.sh name.sh ping.sh read.sh select.sh shift.sh until.sh
[zorro@zorrozou-pc0 bash]$ rm *sh
```
可以成功避免这种误操作的习惯是,不要用特殊字符作为文件名或者目录名,不要给自己犯错误的机会!
另外”也是非常重要的转义字符,\只能转义其后面的一个字符,而”可以转义其扩起来的所有字符。另外””也能起到一部分的转义作用,只是它的转义能力没有”强。”和
“”的区别是:”可以转义所有字符,而””不能对$字符、命令置换“和\转义字符进行转义。
最后
先补充一个关于正则表达式的说明:
很多初学者容易将bash的特殊字符和正则表达式搞混,尤其是*、?、[]这些符号。实际上我们要明白,正则表达式跟bash的通配符和特殊符号没有任何关系。bash本身并不支持正则表达式。那些支持正在表达式的都是外部命令,比如grep、sed、awk这些高级文件处理命令。正则表达式是由这些命令自行处理的,而bash并不对正则表达式做任何解析和解释。
关于正则表达式的话题,我们就不在bash编程系列文章中讲解了,不过未来可能会在讲解sed、awk这样的高级文本处理命令中说明。
通过本文我们学习了bash的特殊符号相关内容,主要包括的知识点为:
- 输入输出重定向以及描述符魔术。
- bash脚本的命令行参数处理。
- bash脚本的数组和关联数组。
- bash的各种其他扩展特殊字符操作。
- 转义字符介绍。
- 正则表达式和bash特殊字符的区别。