![高性能Linux服务器运维实战:shell编程、监控告警、性能优化与实战案例](https://wfqqreader-1252317822.image.myqcloud.com/cover/769/33643769/b_33643769.jpg)
2.1 正则表达式与变量
2.1.1 正则表达式的组成与应用1.什么是正则表达式
正则表达式(Regular Expression,RE)就是由一系列特殊字符组成的字符串。其中每个特殊字符都被称为元字符,这些元字符并不表示它们字面上的含义,而是被解释为一些特定的含义。
正则表达式是由普通字符和元字符共同组成的集合,这个集合用来匹配(或指定)模式。正则表达式的主要功能是文本查询和字符串操作,正则表达式可以匹配文本的一个字符或字符集合。
例如,a、b、1、2等字符属于普通字符,普通字符可以按照字面意思理解,如a只能理解为英文的小写字母a,没有其他隐藏含义。而*、^、[ ]等元字符,shell赋予了它们超越字面意思的意义,如*符号的字面意义只是一个符号,而实际上却表示了重复前面的字符0次或多次的隐藏含义。
2.正则表达式的组成
一个正则表达式包含下列一项或多项。
➢ 一个字符集:这里所指的字符集只包含普通字符,这些字符只表示它们的字面含义。正则表达式的最简单形式就是只包含字符集,而不包含元字符。
➢ 锚:锚指定了正则表达式所要匹配的文本在文本行中所处的位置,如^和$就是锚。
➢ 修饰符:它们扩大或缩小(修改)了正则表达式匹配文本的范围。修饰符包含星号、括号和反斜杠。
正则表达式中常用的一些符号以及对应的意义见表2-1。
表2-1 正则表达式中符号含义
![](https://epubservercos.yuewen.com/680E1C/17977546608668706/epubprivate/OEBPS/Images/50_01.jpg?sign=1739664015-ei3oQ4pjPFIyRQKSQJBosGlzcy5zC1Ou-0-62fb8c8cb1e34ff3547fed20bdf160c5)
其中:
➢ *用于匹配前面一个普通字符的0次或多次重复,例如,hel*o,*符号前面的普通字符是l,*字符就表示匹配l字符0次或多次,如字符串helo、hello、hellllllo都可以由hel*o来表示。
➢ .用于匹配任意一个字符,例如,…73. 表示前面3个字符为任意字符,第4和第5个字符是7和3,最后一个字符为任意字符,如xcb738、4J973U都能匹配上述字符串。
➢ ^用于匹配行首,表示行首的字符是^字符后面的那个字符,例如:^cloud表示匹配以cloud开头的行
➢ $匹配行尾,$放在匹配字符之后,例如,micky$表示匹配以micky结尾的所有行,^$表示匹配空白行。
➢ []匹配字符集合,在正则表达式中,将匹配中括号字符集中的某一个字符,例如:
✧ [xyz]将会匹配字符x、y、或z。
✧ [c-n]匹配字符c~n之间的任意一个字符。
✧ [B-Pk-y]匹配从B~P,或者从k~y之间的任意一个字符。
✧ [a-z0-9]匹配任意小写字母或数字。
✧ [^b-d]将会匹配范围在b~d之外的任意一个字符。这就是使用^对字符集取反的一个实例。
✧ 将多个中括号字符集组合使用,能够匹配一般的单词或数字,例如,[Yy][Ee][Ss]能够匹配yes、Yes、YES、yEs等,[0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9][0-9][0-9]可以匹配社保码。
下面来看第1个例子,操作如下:
![](https://epubservercos.yuewen.com/680E1C/17977546608668706/epubprivate/OEBPS/Images/50_02.jpg?sign=1739664015-qc7BNO6nZGiocgw9k9qWLcDjRKRqb9BO-0-a9e3cd846a543e2dd0bfa6944a83f1cc)
![](https://epubservercos.yuewen.com/680E1C/17977546608668706/epubprivate/OEBPS/Images/51_01.jpg?sign=1739664015-XUNgW3nLsXz7V2GAu32du5qQQkJedGe0-0-e98c464779108a1774d8d9fd6d9ee8e1)
从输出可以看出,精确匹配了exp2.txt文件中所有的非字符内容。
➢ \用来转义某个特殊含义的字符,这意味着,这个特殊字符将会被解释为字面含义。例如:
➢ \$将会被解释成字符$,而不是RE中匹配行尾的特殊字符。相似的,\\将会被解释为字符\。
➢ 转义的尖括号\<...\>用于匹配单词边界,尖括号必须被转义才含有特殊的含义,否则它就表示尖括号的字面含义。
➢ \<the\>完整匹配单词the,不会匹配them、there、other等。
➢ \{\}系列符号表示前一个字符的重复次数。
✧ \{n\}匹配前面字符出现n次,如JO\{3\}B匹配JOOOB。
✧ \{n,\}匹配前面字符至少出现n次,如JO\{3,\}B匹配JOOOB、JOOOOB、JOOOOOB等字符串。
✧ \{n,m\}匹配前面字符出现n次与m次之间,如JO\{3,6\}B匹配JOOOB、JOOOOOOB等字符串。
例如,[a-z]\{5\}匹配5个小写英文字母,如hello、house等。
继续看第2个例子,操作如下:
![](https://epubservercos.yuewen.com/680E1C/17977546608668706/epubprivate/OEBPS/Images/51_02.jpg?sign=1739664015-sOt7M0ylk8rCdcEiby85NjItXaf4itkW-0-3b295e412b66bcb7ce7fbf7062cd8bbb)
![](https://epubservercos.yuewen.com/680E1C/17977546608668706/epubprivate/OEBPS/Images/52_01.jpg?sign=1739664015-sdfJXo8IArC8bfcETOmKlbySmNt9i1TM-0-b58087dd45e638d08fd71f9e9aeb0bdd)
可以看出,grep数字中精确匹配了iivey这个单词。
2.1.2 shell中的变量与应用
1.变量的定义与分类
变量用于保存有用信息,如路径名、文件名、数字等。Linux用户使用变量定制其工作环境,使系统获知用户相关的配置。变量本质上是存储数据的一个或多个计算机内存地址。
shell中的变量可分为如下几种。
➢ 用户自定义变量,如,myname,这类变量是由用户自己定义、修改和使用。
➢ shell环境变量PATH,这类变量是由系统维护,用于设置用户的shell工作环境,只有少数的变量用户可以修改其值。
➢ 位置参数变量(Positional Parameters),这类变量通过命令行给程序传递执行参数,可用shift命令实现位置参数的迁移。
➢ 内部参数变量(Special Parameters),这类变量是Bash预定义的特殊变量,用户不能修改其值。
2.变量的赋值
变量是某个值的名称,引用变量值称为变量替换,$符号是变量替换符号,如variable是变量名,那么$variable就表示变量的值。
变量赋值有两种格式:
![](https://epubservercos.yuewen.com/680E1C/17977546608668706/epubprivate/OEBPS/Images/52_02.jpg?sign=1739664015-BxvlfhUNR6VnVdSzF4Wyl66FzrovB49y-0-18edf93b7c5d0c288c5f5ea0b9bec82b)
切记等号的两边不可以有空格;如果值(value)中包含空格,则必须用双引号括起来,没有空格时也可以用引号,效果和不用一样;变量名只能包括大小写字母(a~z和A~Z)、数字(0~9)、下画线(_)等符号,并且变量名不能以数字开头,否则视为无效变量名,变量区分大小写。
3.变量声明和使用
要使用变量,首先要进行变量的声明,因为shell变量是弱类型的,因此不用声明变量的类型,变量声明与赋值的格式如下:
![](https://epubservercos.yuewen.com/680E1C/17977546608668706/epubprivate/OEBPS/Images/53_01.jpg?sign=1739664015-IGL5xgEGOBwOpegiPcm3SXyiRxqXyDwH-0-28856ec46be8bdc8c624cb5e930fc05a)
变量一旦声明和赋值完成,就可以进行引用了,变量引用的方法有两种:
![](https://epubservercos.yuewen.com/680E1C/17977546608668706/epubprivate/OEBPS/Images/53_02.jpg?sign=1739664015-WAixjDqbiBF4FbCTDHx79vhxkMQXN9e8-0-d0dee10d896e384d770420308bd64dc2)
两种引用方法,在不同环境可进行不同选择。一般规则是:如果变量名为一个字符时建议使用方式一,多于一个字符时建议使用方式二。例如,$a、${abc}。
要显示变量,可以通过echo命令。echo命令可以显示单个变量取值,变量名前加$即可,例如:
![](https://epubservercos.yuewen.com/680E1C/17977546608668706/epubprivate/OEBPS/Images/53_03.jpg?sign=1739664015-loNkGPNG0PYHjxSHhFypjW2b2vZerTNC-0-446889740b6db349d3f3315101f17a8a)
这里仍然建议,输出引用变量时加{}。如果变量名多于一个字符,不加大括号可能会引起不必要的错误。
4.变量清除与只读
当变量不再使用时,可以通过unset命令进行清除,unset命令清除变量的格式为:
![](https://epubservercos.yuewen.com/680E1C/17977546608668706/epubprivate/OEBPS/Images/53_04.jpg?sign=1739664015-PJ6lvxCvE0yUzg9dPv0wZDHJud4VT6jA-0-54703c8dc21f006f6035d96494805cc6)
有时想让某个变量变成只读,变量一旦设置为只读,任何用户不能对此变量进行重新赋值,此时可以使用readonly命令,设置变量只读格式如下:
![](https://epubservercos.yuewen.com/680E1C/17977546608668706/epubprivate/OEBPS/Images/53_05.jpg?sign=1739664015-iSd859e3lUllifpSnOqzokBsrDcReira-0-854b55d3623eabb10d705c637e900edd)
下面来看个例子:
![](https://epubservercos.yuewen.com/680E1C/17977546608668706/epubprivate/OEBPS/Images/53_06.jpg?sign=1739664015-SvDwOA2qI8PBCejrmMZanlaOGvRl5yoX-0-3a4f14fcaf6f9f5723fde030cffee416)
可以看出,变量只读后,无法清除和重新赋值。
5.内部参数变量
内部参数分两类,一类是命令行参数相关的,见表2-2。
表2-2 内部参数变量与含义
![](https://epubservercos.yuewen.com/680E1C/17977546608668706/epubprivate/OEBPS/Images/54_01.jpg?sign=1739664015-wlU2z80jHHudiA49HpRuJNixqUYzaiQ3-0-34c66536424b08db6751a0c9fa0ec03d)
这里有两个变量需要注意:$*和$@都表示传递给脚本或函数的所有参数,但不被双引号(" ")包含时,都以$1、$2、…、$n的形式输出所有参数。但是当它们被双引号("")包含时,$*会将所有的参数作为一个整体,以$1$2 … $n的形式输出所有参数;而$@会将各个参数分开,以$1、$2、…、$n的形式输出所有参数。
另一类内部参数是与进程状态相关的,常见的参数见表2-3。
表2-3 与进程相关的内部参数与含义
![](https://epubservercos.yuewen.com/680E1C/17977546608668706/epubprivate/OEBPS/Images/54_02.jpg?sign=1739664015-sJMB0btB9QO8iuVgAxz4aHokZsFC8Kus-0-de9384cb24b36430ef448fa3274f3ed7)
下面看一个例子myscript1.sh,脚本内容如下:
![](https://epubservercos.yuewen.com/680E1C/17977546608668706/epubprivate/OEBPS/Images/54_03.jpg?sign=1739664015-Tu7mT0aiEPEFyVDHpMIijCYZZP4hqGkX-0-d77b77ae2fd11affe524797aa3a71446)
这个例子集中演示了位置参数变量、内部参数变量的含义和输出。执行脚本,后面跟上对应的参数,即可看到对应的不同变量的输出。操作如下:
![](https://epubservercos.yuewen.com/680E1C/17977546608668706/epubprivate/OEBPS/Images/54_04.jpg?sign=1739664015-5DN091dP2U9OKoCiuTmksoL5xRB4ggCh-0-e96f6933eed868395cebd9c4b26a481c)
![](https://epubservercos.yuewen.com/680E1C/17977546608668706/epubprivate/OEBPS/Images/55_01.jpg?sign=1739664015-z4Yti5UEg4OS2yPXq4Db1RQW6rG7rnWc-0-12c7c70d2ce69311fa680cba4f8415e9)
再来介绍一个IFS变量,shell脚本中有个变量叫IFS(Internal Field Seprator),内部域分隔符,IFS的默认值为空白(包括空格、Tab和新行),例如:
![](https://epubservercos.yuewen.com/680E1C/17977546608668706/epubprivate/OEBPS/Images/55_02.jpg?sign=1739664015-ay9SnfjwXjmhA078q3Uuoin64LTzpE9L-0-e850d931794bf176e1f2173a2499a6c1)
直接输出IFS是看不到的,把IFS转化为八进制就可以看到了,040是空格,011是Tab,012是换行符\n。最后一个012是因为echo默认是会换行的。
这里再来总结下$*和$@的区别,$*会根据IFS的不同来组合值,而$@则会将值用空格来组合值,推荐使用$@,而不是$*。
下面给出一个脚本myscript2.sh,通过输出就能看到它们的差别:
![](https://epubservercos.yuewen.com/680E1C/17977546608668706/epubprivate/OEBPS/Images/55_03.jpg?sign=1739664015-IqHhJquRsNoH811qTW0tKRof4EhpOVwv-0-f832b80e2fa28d820e15b586fc54065d)
执行上面脚本,输出如下:
![](https://epubservercos.yuewen.com/680E1C/17977546608668706/epubprivate/OEBPS/Images/55_04.jpg?sign=1739664015-MggeMocTGT4RD3yBjjvqwchwctVOlh7U-0-ceda7e712a9d500b22b4c9051bc74061)
从这个脚本的执行结果,可以看出$@、$*以及与IFS的关系。
6.位置参数变量
位置参数是一种特殊的shell变量,用于从命令行向shell脚本传递参数。
$1表示第1个参数、$2表示第2个参数等,$0表示脚本的名字,从${10}开始,参数号需要用大括号括起来,如${10}、${11}、${100}等。那么位置参数主要用在什么地方呢?常用的环境有两个:退出/返回从shell命令/脚本的命令行接受参数或在调用shell函数时为其传递参数。
7.退出/返回状态
shell中有多种退出/返回状态,在写shell脚本的时候经常用到这些状态,那么如何获取脚本的退出/返回状态呢?可以通过$?来实现。$?用来退出/返回上一条语句或脚本执行的状态,常见状态如下:
![](https://epubservercos.yuewen.com/680E1C/17977546608668706/epubprivate/OEBPS/Images/56_01.jpg?sign=1739664015-LUyRrLydDhK2yfj7uuxweQOdQ7f4kEr4-0-99ebfa99768bef9a07bc48734af9f83b)
其实可以在shell脚本中设置退出/返回状态,通过exit命令来实现。exit命令用于退出/返回脚本或当前shell,在退出/返回的时候,可以设置退出/返回状态码,方法如下:
![](https://epubservercos.yuewen.com/680E1C/17977546608668706/epubprivate/OEBPS/Images/56_02.jpg?sign=1739664015-rhi6nYgGM0y0Vl5q7a85OegkUEIL5FV6-0-219c5b9d60fb81a998746c10b4f8040b)
其中,n是一个从0~255的整数,0表示成功退出/返回,非零表示遇到某种失败,返回值被保存在状态变量$?中。常见的退出/返回状态码见表2-4。
表2-4 常见的退出/返回状态码与含义
![](https://epubservercos.yuewen.com/680E1C/17977546608668706/epubprivate/OEBPS/Images/56_03.jpg?sign=1739664015-6qkg2oiDNgH57zwzC6LvVaBDM74APw1Z-0-3a70fd6ff66b325c7fa12da3aa182176)
看下面几个例子:
![](https://epubservercos.yuewen.com/680E1C/17977546608668706/epubprivate/OEBPS/Images/56_04.jpg?sign=1739664015-iRmqZ9qfSCoI2yJ3wunACmf2xRZIkPeM-0-3ddfdfab525a37f330592ef5ee8215fd)
![](https://epubservercos.yuewen.com/680E1C/17977546608668706/epubprivate/OEBPS/Images/57_01.jpg?sign=1739664015-8yjpbeKGIWVE6Iq8toWRF51kjiQIrsf8-0-a07cf8fbe0a8c4d31d8ce3ae0d000367)
通过命令的执行和输出,可以看出,每个退出/返回状态码的含义,明白了这些退出/返回状态码,就可以在写shell的时候进行调用,以判断命令是否执行成功。
8.命令替换
命令替换是指将命令的输出作为命令替换位置的文本,命令替换的作用是抽取一个命令的输出,然后使用=操作赋值到一个变量供以后使用。命令替换在shell编程中经常用到,有两种使用格式,分别是:
![](https://epubservercos.yuewen.com/680E1C/17977546608668706/epubprivate/OEBPS/Images/57_02.jpg?sign=1739664015-JwJuVjz58r1zIJCcEqThQijhnlrlCwqr-0-10cbe87a3799fc0a1a38ee53b69083ca)
注意是反引号,也就是键盘〈Esc〉下面的那个键,看下面的例子:
![](https://epubservercos.yuewen.com/680E1C/17977546608668706/epubprivate/OEBPS/Images/57_03.jpg?sign=1739664015-tnzg7DD0NEqMxdqbfFJBLoJGNr23TqBB-0-01395661288b94628758fab5f871acc8)
两个例子都是将Nginx的进程数统计出来,然后赋给httpnum和httpnum1变量,在shell脚本中,变量这样定义后,下面就可以直接引用了。两种方式各有优缺点,推荐使用第2种方式。
9.read命令
read命令用来接收键盘输入内容为变量赋值,具体用法如下:
![](https://epubservercos.yuewen.com/680E1C/17977546608668706/epubprivate/OEBPS/Images/57_04.jpg?sign=1739664015-GuMawe2RXo3k25qh5LtbaB0QSpI10D9b-0-62a0e93a3d8b7f26e549ff37b35f8a27)
若省略变量名,则会将输入的内容存入默认REPLY变量中。看下面例子:
![](https://epubservercos.yuewen.com/680E1C/17977546608668706/epubprivate/OEBPS/Images/57_05.jpg?sign=1739664015-ta2YirItoRIBfH2ooNy0DLYs0Y7XcixF-0-d43267e2f1f32d76ca5885a18af4230d)
可以结合不同的引号为变量赋值,规则如下所述。
➢ 双引号"":允许通过$符号引用其他变量值。
➢ 单引号'':禁止引用其他变量值,$视为普通字符,因此引用变量时不要用单引号。
➢ 反撇号``:将命令执行的结果输出给变量。
看下面这个shell脚本myscript3.sh:
![](https://epubservercos.yuewen.com/680E1C/17977546608668706/epubprivate/OEBPS/Images/58_01.jpg?sign=1739664015-D3KEAIfvptojgS6u2RoheAAIRFnt9Tkg-0-6ab1bf2cd45a28b61603c51cb1560edf)
最后来执行脚本看看输出结果:
![](https://epubservercos.yuewen.com/680E1C/17977546608668706/epubprivate/OEBPS/Images/58_02.jpg?sign=1739664015-okias9m5JA40yMdi0c906UVAbCwn8Ds5-0-4996586a0dce76050550b10950544656)
2.1.3 变量测试、截取与替换
1.变量测试的用法
shell支持变量测试和默认赋值,当一个变量不存在的时候,可以默认给此变量进行赋值。变量测试和赋值有多种方式,常见的有4种情况,见表2-5。
表2-5 变量测试的几种用法与含义
![](https://epubservercos.yuewen.com/680E1C/17977546608668706/epubprivate/OEBPS/Images/58_03.jpg?sign=1739664015-LLhs5RRgGUBfCWtZEHJbRKAWUMZH2ig7-0-ed4939441f1ecaebde1cac55d8dc4252)
看下面这个操作过程,更能清晰地理解每个变量测试的含义:
![](https://epubservercos.yuewen.com/680E1C/17977546608668706/epubprivate/OEBPS/Images/59_01.jpg?sign=1739664015-zZ39vmOH8kQB7jh6mxtskd84yTdnSYH7-0-dd744f42763e93407ba0753fb0aeb7ea)
2.字符串长度与截取
awk和sed可以进行文本中字符串的过滤、筛选和替换。其实,shell本身也支持这种功能,下面就来看看shell中字符串长度与截取的方法,见表2-6。
表2-6 字符串长度与截取的用法与含义
![](https://epubservercos.yuewen.com/680E1C/17977546608668706/epubprivate/OEBPS/Images/59_02.jpg?sign=1739664015-hwx1HlmTfXV6BVkNQOW3jMUn9liVAcqm-0-77f63d970e7f6a23db8cf3784bff1c96)
看下面这个例子,操作过程如下:
![](https://epubservercos.yuewen.com/680E1C/17977546608668706/epubprivate/OEBPS/Images/59_03.jpg?sign=1739664015-rF7cmJ28c5aVIyNW2xebFsDGcmk1frEV-0-ab181d5acdb6b0ec7cca0930358066ad)
![](https://epubservercos.yuewen.com/680E1C/17977546608668706/epubprivate/OEBPS/Images/60_01.jpg?sign=1739664015-iGHo9xRY1ogero7EtFXvNc7VeeO9kbAh-0-45e532a127556a468b2d377c249126a8)
此外,shell还支持字符串替换,见表2-7。
表2-7 字符串替换用法
![](https://epubservercos.yuewen.com/680E1C/17977546608668706/epubprivate/OEBPS/Images/60_02.jpg?sign=1739664015-ZrxVDrBo5xMYqMBD37gHuCl9pQKNZGn4-0-656ca72b6df0c832945ef5073aee79e5)
需要注意的是old中可以使用通配符。var可以是@或*,表示对每个位置参数进行替换,继续看下面的例子,操作过程如下:
![](https://epubservercos.yuewen.com/680E1C/17977546608668706/epubprivate/OEBPS/Images/60_03.jpg?sign=1739664015-HTmQNLKPKqPLGl7HOnaB3K3fC9zwOj5K-0-8b31b70eabeb29be9ccbe106df08db37)
3.变量的间接引用
先来看一个例子:
![](https://epubservercos.yuewen.com/680E1C/17977546608668706/epubprivate/OEBPS/Images/60_04.jpg?sign=1739664015-6CXPsx5O1ssz1X5V2Xt5TU4um8HOvmlD-0-c0177b01346234ad7f9874797f879437)
这个例子中,想让$str2输出Hello World,那么echo $str2将输出什么呢?是否能够满足要求?显然不能,上面这个输出中,最后echo $str2会输出的值为str1,那么如何才能输出所需要的值呢?可以这样执行:
![](https://epubservercos.yuewen.com/680E1C/17977546608668706/epubprivate/OEBPS/Images/60_05.jpg?sign=1739664015-OV9VQYbR1WrLeg1hdB5RSJqGNx5x3WMQ-0-9b7a844368379a231c07c6efda86e6d9)
上面两个命令都能实现将str1的值间接的赋给str2,最后结果为Hello World,满足间接赋值要求。再来看个例子:
![](https://epubservercos.yuewen.com/680E1C/17977546608668706/epubprivate/OEBPS/Images/61_01.jpg?sign=1739664015-65nLbL2jLkcNFfkfMx3MODhQCObcVkHe-0-9e189bcdffef65de46d2ba7f3d58f479)
那么看下面这几个组合输出什么内容:
![](https://epubservercos.yuewen.com/680E1C/17977546608668706/epubprivate/OEBPS/Images/61_02.jpg?sign=1739664015-mmWoH7xxV6IKIxePe2sxLwYH1LPhPVCr-0-5c88f3bb55396e310861d2be6caaa6f8)
来执行看看结果:
![](https://epubservercos.yuewen.com/680E1C/17977546608668706/epubprivate/OEBPS/Images/61_03.jpg?sign=1739664015-VFzXW57PtENNXJ6v5HcpKzRRwZfBtnEd-0-c7a53e66f552b3e1f2cd14568bb3babb)
很显然,第2个是满足需要的,通过间接引用变量,实现了变量值的替换。
4.同时输出多行信息
同时输出多行信息也经常会使用,有两种方法可以实现,第1种是使用echo命令,用法如下:
![](https://epubservercos.yuewen.com/680E1C/17977546608668706/epubprivate/OEBPS/Images/61_04.jpg?sign=1739664015-uEHCYSuEuYxmwb5zTMvC4jWEQgiB3H3S-0-51263e061bf0e927dd7fd78084aba10c)
注意,多行内容中不能出现双引号,否则echo提前结束,若确实需要使用双引号,需使用转义字符\。同时输出多行信息的第2个方法是使用here file,方法如下:
![](https://epubservercos.yuewen.com/680E1C/17977546608668706/epubprivate/OEBPS/Images/61_05.jpg?sign=1739664015-TiBMK9xwynDUKkcBPSd2bzaolXUUxZUH-0-8d9e9029e3ed1971bff2c5d47fec7c8b)
注意:END可以是任意字符串,只要上下一致即可,多行内容中不能出现内容为END开始的行,否则cat提前结束。