这篇博文接前面两篇介绍shell中使用正则和shell脚本输入处理,有问题及时在本文下或CSDN留言。
shell中使用正则
关于正则表达式的基础知识,这里不展开,只说怎么用。
正则表达式类型
正则表达式包括基本正则表达式和拓展正则表达式
基本表达式:
元字符
元字符 | 描述 |
---|---|
. | 句号匹配任意单个字符除了换行符。 |
[ ] | 字符种类。匹配方括号内的任意字符。 |
[^ ] | 否定的字符种类。匹配除了方括号里的任意字符 |
* | 匹配>=0个重复的在*号之前的字符。 |
+ | 匹配>=1个重复的+号前的字符。 |
? | 标记?之前的字符为可选. |
{n,m} | 匹配num个大括号之前的字符或字符集 (n <= num <= m). |
(xyz) | 字符集,匹配与 xyz 完全相等的字符串. |
| | 或运算符,匹配符号前或后的字符. |
\ | 转义字符,用于匹配一些保留的字符 `[ ] ( ) { } . * + ? ^ $ \ |
^ | 从开始行开始匹配. |
$ | 从末端开始匹配. |
点运算符 .
.
是元字符中最简单的例子。 .
匹配任意单个字符,但不匹配换行符。 例如,表达式.ar
匹配一个任意字符后面跟着是a
和r
的字符串。
1 | ".ar" => The car parked in the garage. |
字符集
字符集也叫做字符类。 方括号用来指定一个字符集。 在方括号中使用连字符来指定字符集的范围。 在方括号中的字符集不关心顺序。 例如,表达式[Tt]he
匹配 the
和 The
。
1 | "[Tt]he" => The car parked in the garage. |
方括号的句号就表示句号。 表达式 ar[.]
匹配 ar.
字符串
1 | "ar[.]" => A garage is a good place to park a car. |
否定字符集
一般来说 ^
表示一个字符串的开头,但它用在一个方括号的开头的时候,它表示这个字符集是否定的。 例如,表达式[^c]ar
匹配一个后面跟着ar
的除了c
的任意字符。
1 | "[^c]ar" => The car parked in the garage. |
重复次数
后面跟着元字符 +
,*
or ?
的,用来指定匹配子模式的次数。 这些元字符在不同的情况下有着不同的意思。
*
号
*
号匹配 在*
之前的字符出现大于等于0
次。 例如,表达式 a*
匹配0或更多个以a开头的字符。表达式[a-z]*
匹配一个行中所有以小写字母开头的字符串。
1 | "[a-z]*" => The car parked in the garage #21. |
*
字符和.
字符搭配可以匹配所有的字符.*
。 *
和表示匹配空格的符号\s
连起来用,如表达式\s*cat\s*
匹配0或更多个空格开头和0或更多个空格结尾的cat字符串。
1 | "\s*cat\s*" => The fat cat sat on the concatenation. |
+
号
+
号匹配+
号之前的字符出现 >=1 次。 例如表达式c.+t
匹配以首字母c
开头以t
结尾,中间跟着至少一个字符的字符串。
1 | "c.+t" => The fat cat sat on the mat. |
?
号
在正则表达式中元字符 ?
标记在符号前面的字符为可选,即出现 0 或 1 次。 例如,表达式 [T]?he
匹配字符串 he
和 The
。
1 | "[T]he" => The car is parked in the garage. |
1 | "[T]?he" => The car is parked in the garage. |
{}
号
在正则表达式中 {}
是一个量词,常用来限定一个或一组字符可以重复出现的次数。 例如, 表达式 [0-9]{2,3}
匹配最少 2 位最多 3 位 0~9 的数字。
1 | "[0-9]{2,3}" => The number was 9.9997 but we rounded it off to 10.0. |
我们可以省略第二个参数。 例如,[0-9]{2,}
匹配至少两位 0~9 的数字。
1 | "[0-9]{2,}" => The number was 9.9997 but we rounded it off to 10.0. |
如果逗号也省略掉则表示重复固定的次数。 例如,[0-9]{3}
匹配3位数字
1 | "[0-9]{3}" => The number was 9.9997 but we rounded it off to 10.0. |
(...)
特征标群
特征标群是一组写在 (...)
中的子模式。(...)
中包含的内容将会被看成一个整体,和数学中小括号( )的作用相同。例如, 表达式 (ab)*
匹配连续出现 0 或更多个 ab
。如果没有使用 (...)
,那么表达式 ab*
将匹配连续出现 0 或更多个 b
。再比如之前说的 {}
是用来表示前面一个字符出现指定次数。但如果在 {}
前加上特征标群 (...)
则表示整个标群内的字符重复 N 次。
我们还可以在 ()
中用或字符 |
表示或。例如,(c|g|p)ar
匹配 car
或 gar
或 par
.
1 | "(c|g|p)ar" => The car is parked in the garage. |
|
或运算符
或运算符就表示或,用作判断条件。
例如 (T|t)he|car
匹配 (T|t)he
或 car
。
1 | "(T|t)he|car" => The car is parked in the garage. |
转码特殊字符
反斜线 \
在表达式中用于转码紧跟其后的字符。用于指定 { } [ ] / \ + * . $ ^ | ?
这些特殊字符。如果想要匹配这些特殊字符则要在其前面加上反斜线 \
。
例如 .
是用来匹配除换行符外的所有字符的。如果想要匹配句子中的 .
则要写成 \.
以下这个例子 \.?
是选择性匹配.
1 | "(f|c|m)at\.?" => The fat cat sat on the mat. |
锚点
在正则表达式中,想要匹配指定开头或结尾的字符串就要使用到锚点。^
指定开头,$
指定结尾。
^
号
^
用来检查匹配的字符串是否在所匹配字符串的开头。
例如,在 abc
中使用表达式 ^a
会得到结果 a
。但如果使用 ^b
将匹配不到任何结果。因为在字符串 abc
中并不是以 b
开头。
例如,^(T|t)he
匹配以 The
或 the
开头的字符串。
1 | "(T|t)he" => The car is parked in the garage. |
1 | "^(T|t)he" => The car is parked in the garage. |
$
号
同理于 ^
号,$
号用来匹配字符是否是最后一个。
例如,(at\.)$
匹配以 at.
结尾的字符串。
1 | "(at\.)" => The fat cat. sat. on the mat. |
简写字符集
简写 | 描述 |
---|---|
. | 除换行符外的所有字符 |
\w | 匹配所有字母数字,等同于 [a-zA-Z0-9_] |
\W | 匹配所有非字母数字,即符号,等同于: [^\w] |
\d | 匹配数字: [0-9] |
\D | 匹配非数字: [^\d] |
\s | 匹配所有空格字符,等同于: [\t\n\f\r\p{Z}] |
\S | 匹配所有非空格字符: [^\s] |
\f | 匹配一个换页符 |
\n | 匹配一个换行符 |
\r | 匹配一个回车符 |
\t | 匹配一个制表符 |
\v | 匹配一个垂直制表符 |
\p | 匹配 CR/LF(等同于 \r\n ),用来匹配 DOS 行终止符 |
零宽度断言(前后预查)
先行断言和后发断言都属于非捕获簇(不捕获文本 ,也不针对组合计进行计数)。 先行断言用于判断所匹配的格式是否在另一个确定的格式之前,匹配结果不包含该确定格式(仅作为约束)。
例如,我们想要获得所有跟在 $
符号后的数字,我们可以使用正后发断言 (?<=\$)[0-9\.]*
。 这个表达式匹配 $
开头,之后跟着 0,1,2,3,4,5,6,7,8,9,.
这些字符可以出现大于等于 0 次。
零宽度断言如下:
符号 | 描述 |
---|---|
?= | 正先行断言-存在 |
?! | 负先行断言-排除 |
?<= | 正后发断言-存在 |
?<! | 负后发断言-排除 |
?=...
正先行断言
?=...
正先行断言,表示第一部分表达式之后必须跟着 ?=...
定义的表达式。
返回结果只包含满足匹配条件的第一部分表达式。 定义一个正先行断言要使用 ()
。在括号内部使用一个问号和等号: (?=...)
。
正先行断言的内容写在括号中的等号后面。 例如,表达式 (T|t)he(?=\sfat)
匹配 The
和 the
,在括号中我们又定义了正先行断言 (?=\sfat)
,即 The
和 the
后面紧跟着 (空格)fat
。
1 | "(T|t)he(?=\sfat)" => The fat cat sat on the mat. |
?!...
负先行断言
负先行断言 ?!
用于筛选所有匹配结果,筛选条件为 其后不跟随着断言中定义的格式。 正先行断言
定义和 负先行断言
一样,区别就是 =
替换成 !
也就是 (?!...)
。
表达式 (T|t)he(?!\sfat)
匹配 The
和 the
,且其后不跟着 (空格)fat
。
1 | "(T|t)he(?!\sfat)" => The fat cat sat on the mat. |
?<= ...
正后发断言
正后发断言 记作(?<=...)
用于筛选所有匹配结果,筛选条件为 其前跟随着断言中定义的格式。 例如,表达式 (?<=(T|t)he\s)(fat|mat)
匹配 fat
和 mat
,且其前跟着 The
或 the
。
1 | "(?<=(T|t)he\s)(fat|mat)" => The fat cat sat on the mat. |
?<!...
负后发断言
负后发断言 记作 (?<!...)
用于筛选所有匹配结果,筛选条件为 其前不跟随着断言中定义的格式。 例如,表达式 (?<!(T|t)he\s)(cat)
匹配 cat
,且其前不跟着 The
或 the
。
标志
标志也叫模式修正符,因为它可以用来修改表达式的搜索结果。 这些标志可以任意的组合使用,它也是整个正则表达式的一部分。
标志 | 描述 |
---|---|
i | 忽略大小写。 |
g | 全局搜索。 |
m | 多行修饰符:锚点元字符 ^ $ 工作范围在每行的起始。 |
忽略大小写 (Case Insensitive)
修饰语 i
用于忽略大小写。 例如,表达式 /The/gi
表示在全局搜索 The
,在后面的 i
将其条件修改为忽略大小写,则变成搜索 the
和 The
,g
表示全局搜索。
1 | "The" => The fat cat sat on the mat. |
1 | "/The/gi" => The fat cat sat on the mat. |
全局搜索 (Global search)
修饰符 g
常用于执行一个全局搜索匹配,即(不仅仅返回第一个匹配的,而是返回全部)。 例如,表达式 /.(at)/g
表示搜索 任意字符(除了换行)+ at
,并返回全部结果。
1 | "/.(at)/" => The fat cat sat on the mat. |
1 | "/.(at)/g" => The fat cat sat on the mat. |
多行修饰符 (Multiline)
多行修饰符 m
常用于执行一个多行匹配。
像之前介绍的 (^,$)
用于检查格式是否是在待检测字符串的开头或结尾。但我们如果想要它在每行的开头和结尾生效,我们需要用到多行修饰符 m
。
例如,表达式 /at(.)?$/gm
表示小写字符 a
后跟小写字符 t
,末尾可选除换行符外任意字符。根据 m
修饰符,现在表达式匹配每行的结尾。
1 | "/.at(.)?$/" => The fat |
1 | "/.at(.)?$/gm" => The fat |
贪婪匹配与惰性匹配 (Greedy vs lazy matching)
正则表达式默认采用贪婪匹配模式,在该模式下意味着会匹配尽可能长的子串。我们可以使用 ?
将贪婪匹配模式转化为惰性匹配模式。
1 | "/(.*at)/" => The fat cat sat on the mat. |
1 | "/(.*?at)/" => The fat cat sat on the mat. |
以上正则基础知识内容来源于learn-regex
下面介绍下POSIX字符类:
POSIX字符类
[:alnum:]:匹配字面和数字字符。等同于A
Z,az,0~9[:alpha:]:匹配字母字符。等同于A
Z,az[:blank:]:匹配空格或制表符
[:cntrl:]:匹配控制字符
[:digit:]:匹配十进制数字。等同于0~9[:graph:]:匹配ASCII码值范围33~126的字符。与[:print:]相似,但不包括空格字符
[:print:]:与[:graph:]相同,但多了空格字符
[:lower:]:匹配小写字母,等同于a~z
[:upper:]:匹配大写字母,等同于A~Z
[:space:]:匹配空白字符(空格和制表符)
[:xdigit:]:匹配十六进制数字。等同于0
9,AF,a~f
POSIX字符类通常需要引用或双方括号([[]])括起来
bash正则
bash中使用”=~”表示正则表达式比较操作符,正则表达式匹配成功,返回状态码0,匹配失败,返回状态码1
1 | #读取用户从键盘的输入,存入变量num |
在bash中使用正则匹配的语法为
1 | grep "regex" 文件 |
脚本输入处理
参数处理
输入参数大小写不敏感
之前博文中已经讲过给函数传递参数的知识点,给脚本传递参数也是类似的,当我们执行脚本的时候也可以通过位置参数传递参数,内部通过case或”[[“条件命令,来确定shell执行路径,但是shell对于参数默认是大小写敏感的,如果我们想使脚本大小写不敏感,可以在脚本首行中加入shopt -s nocasematch
来开启
nocasematch选项,在末尾加入shopt -u nocasematch
来关闭
nocasematch选项,也可以通过正则匹配大小写来实现,不过这种不是很方便
使用shift命令处理命令行参数
当脚本只有一个命令行参数时,内部用case来分不同情况很方便,但是当脚本有多个参数时,使用case就不方便了,这时候可以使用shift
命令在一个变量中一个接一个的获取多个命令行参数,语法如下:
1 | shift [n] |
n必须是一个小于或等于”$#”(参数个数)的非负整数。n为0或者大于”$#”,位置参数都不会改变,如果不指定n,默认设为1,如果n大于”$#”或小于0,此命令返回状态码将大于0,否则为0.
当我们使用shift 1 命令时,位置参数将移动一位,原来$2位置参数赋值给$1,以此类推,原来的变量$1的值将被废弃。同理,如果我们使用shift 5,位置参数原来$6的参数值赋值给$1,$7的值赋值给$2,以此类推,原来的变量$1-$5的值将被废弃。
在每次移动之后,特殊变量$#(位置参数个数)的值也会调整,而特殊变量$0(当前运行脚本名称)不参与移位操作。如果我们读取$1的值,然后运行命令shift
,再次读取特殊变量$1的值,将得到$2的值,然后再次运行命令shift,再次读取特殊变量$1的值,将得到$3的值,依次类推。因此,只要$#的值不为0,就可以在while循环中进行迭代,获取特殊变量$1的值,运行shift命令,然后再次读取$1的值,来依次获取所有传递的命令行参数。
1 | #创建test.sh文件如下: |
有时候我们传入的参数个数和shift后面的n不是整除关系,比如传入的参数个数是7个,n是5,当第一次随时用shift5时,会将$6的值给到$1,但是第二次执行时,由于只剩下两个元素,$#的值变为2,第二次移位不会再发生,while循环将无限进行下去。
为了解决以上问题,我们可以判断每次执行shift命令的结果,如果正确执行了,就继续,没有执行就退出循环,通过特殊变量$?判断命令返回码是否为0,为0说明正确执行,否则没有正确执行。
1 | while [ $# -ne 0 ]; |
使用for循环读取多个参数
当命令行参数比较多的时候,使用$1,$2一个一个参数获取,编程就不是很方便了,这个时候可以通过for循环,配合$*及$@来列出传递给脚本的所有命令行参数。
1 | if [ ! -n "$1" ]; then |
上面脚本中,引用$*没有加双引号,是因为如果加了双引号,其值将被扩展为包含所有位置参数的值的单个字符串,这样for循环只能进行一次,参考特殊参数
读取脚本名称
在shell脚本中,特殊变量$0是脚本的名称,在脚本中我们运行一段脚本之前可以将脚本名输出,当脚本输入参数有误时,也可以将脚本名及使用方法打印出来,总之,读取脚本名有其特定的应用场合。
选项处理
我们在执行脚本时候,如果有多个参数,有时候我们脚本的要求不仅要参数个数正确,还需要参数顺序正确。为此,我们通过处理,使shell脚本的执行命令可以通过选项指定参数顺序和类型。比如一个脚本需要三个参数,一个配置文件,一个输入文件,一个输出文件,我们可以通过-c选项指定配置文件,通过-in指定输入文件,通过-out指定输出文件,那么执行脚本时候就可以
1 | xxx.sh -c my.cof -in input.txt -out out.txt |
下面介绍如何在shell脚本中实现命令行选项的处理
使用case语句处理命令行选项
当参数只有一个的时候,使用case来处理选项是很方便的,比如一个脚本copy.sh执行时一次只能有一个参数,当使用-c时,拷贝指定的文件,当时用-m时移动指定的文件,那么我们的命令行参数有两个,一个是选项(-c /-m),另一个是文件名,在脚本中我们就可以通过对第一个参数进行验证,来执行不同的指令,此处不再举例。
使用getopts处理多命令行选项
注意:getopts
不能解析GNU风格的长选项(–myoption)或XF86风格的长选项(-myoptions)
假设有如下脚本,调用此脚本时指定如下选项参数:
1 | test.sh -x -f /etc/test.conf -r ./source.txt ./destination.txt |
上述选项参数可以划分为如下逻辑组:
-x,-r都是一个单独的选项,后面不跟参数
-f也是一个选项,且选项后跟一个参数/etc/test.conf,并且参数与选项之间用空格隔开
./source.txt ./destination.txt是不与任何选项关联的两个参数
当我们在test.sh脚本中使用getopts来处理命令行选项和参数,上面的命令还可以写成
1 | test.sh -xrf /etc/test.conf ./source.txt ./destination.txt |
getopts
可以识别所有这些选项格式,指定的选项可以是大写或者小写字母,或者数字,其他字符也能识别,但是不推荐使用。在处理命令行选项和参数时,需要多次调用getopts,且该命令本身不会更改位置参数的设置,要想将位置参数移位,必须使用shift命令来处理位置参数。
当没有内容可以解析时,getopts会设置一个退出状态FALSE,所以在循环中可以很容易使用:
1 | while getopts ...; do |
getopts将会解析选项和它们可能的参数,它将在第一个非选项参数(不以连字符”-“开头的,且不是它前面的任何选项的参数的字符串)的位置停止解析。当遇到双连字符”–”(表示选项的结束)时,也将停止解析。
getopts会使用到以下3个变量
OPTIND
: 存放下一个要处理的参数的索引,这是getopts在调用过程中记住自己状态的方式,同样可以用于移位使用getopts处理后的位置参数。OPTIND初始被设为1,想再次使用getopts解析任何内容,需要将其重置为1.
OPTARG
: 这个变量被设置为由getopts找到的选项所对应的参数。
OPTERR
: 值为0或1,指示bash是否应该显示由getopts产生的错误信息。在每个shell启动时,它的值被初始化为1,如果不想看到相关信息,请确保设置为0
getopts命令的基本语法是:getopts OPTSTRING VARNAME ARGS…
OPTSTRING
:告诉getopts会有哪些选项和在哪会有参数(用选项后加冒号”:”表示)
VANAME
: 告诉getopts哪个变量用于选项
ARGS
: 告诉getopts解析这些可选的参数,而不是位置参数
比如下面的命令告诉getopts查找-f、-A和-x
1 | getopts fAx VARNAME |
下面的命令告诉getopts命令,-A选项后面会有一个参数:
1 | getopts fA:x VARNAME |
默认情况下,getopts命令是解析当前shell或者函数的位置参数,也可以指定自己的参数让getopts来解析。
getopts支持两种错误报告模式,详细模式和抑制错误报告模式。
详细模式下,如果getopts遇到无效选项或者需要的参数没找到,VARNAME值会被设置为问号(?),并且变量OPTARG不会被设置;
抑制错误报告模式下:如果遇到无效选项,VARNAME值会被设置为问号(?),并且OPTARG会被设置为选项字符;如果需要的参数没有找到,VARNAME值会被设置为冒号(:),并且变量OPTARG中会包含选项字符。
1 | #test.sh |
从上面脚本执行情况可以看出,无效的选项不会停止处理:如果我们希望遇到无效选项就开始停止处理,必须做一些完善操作(在正确的位置执行退出操作);多个相同的选项是可能的,如果想禁止重复的选项,必须在脚本中做一些检查操作。
下面是一个接受多参数的示例:
1 | #定义变量vflag |
使用getopt处理多命令行选项
getopt
命令与getopts
命令功能相似,也是用于解析命令行的选项和参数,不同的是getopt
是linux下的命令行工具,并且支持命令行的长选项(–)另外,在脚本中调用方式也不同
使用getopt语法如下:
getopt options – optstring parameters
getopt options -o|–options optstring options – parameters
下面是一个getopt的例子
1 | getopt f:vl -vl -f/local/filename.conf param |
上例中,f:vl对应getopt语法中的optstring(选项字符串),”-vl -f/local/filename.conf param”对应语法中的parameters(命令的参数),因此getopt会按照optstring的设置,将paramteers解析为相应的选项和参数
所以上例子中,-vl被解析为-v和-l,f后面有一个冒号(:)所以-f/local/filename.conf被解析为”-f /local/filename.conf”,然后解析后的命令行选项和参数之间使用双连字符(–)分隔
1 | -v -l -f /local/filename.conf --param |
下面看一个shell脚本解析的例子:
1 | #将getopt命令解析后的内容设置到位置参数 |
这是一个简单的通过位置参数循环解析命令行参数的例子,下面示例使用getopt来解析,下面脚本改编自上面getopts的例子:
1 | #定义变量vflag |
getopt还有一个功能是支持长选项(–long-options),下面通过一个例子来看下如何解析:
1 | #定义变量param2 |
上述脚本中,我们使用getopt命令getopt -o a::bc: --long arga::,argb,argc: -n 'test.sh' -- "$@"
来处理指定给脚本的命令行参数。-o选项表示告诉getopt识别哪些短(一个字符)选项。–long(或-l)选项告诉getopt识别哪些长(多个字符)选项,-n选项告诉getopt在报告错误时使用什么文件名(或程序名)
getopt命令的-o选项所指定的选项字符串遵循如下规则:
- 每个字符发表一个选项
- 字符后面跟一个冒号(:)表示选项需要一个参数,两个冒号(::)表示选项有个可选参数
例如上面的脚本,”a::”表示识别选项-a,且-a具有一个可选参数,如果在命令行指定参数给-a选项,那么选项-a与参数之间不能有任何空格。”b”表示识别选项-b,没有参数,”c:”表示识别选项-c,有一个参数。下面看几个指定脚本短选项运行的示例
1 | [root@test ~]# ./test.sh -c 456 |
getopt命令的–long选项遵循如下规则:
- 每个选项之间由逗号(,)分隔
- 字符串后面跟一个冒号(:)表示选项需要一个参数,跟两个冒号(::)表示有可选参数
下面看几个指定脚本长选项运行的示例:
1 | [root@test ~]# ./test.sh --arga --argb --argc 456 |
获得用户输入
bash下通过内部命令read
接收用户键盘的输入,并可以将输入的内容赋值给一个变量
基本的读取
read命令基本读取的语法如下:
1 | read -p prompt variable1 variable2 |
-p选项用于输出提示信息,来提示用户输入,read命令会每次从标准输入或者(-u 指定的文件描述符中)读取一行内容,它会将第一个单词赋值给变量variable1,第二个赋值给变量variable2,以此类推。如果输入的单词少于指定的变量数,剩下的变量会被设为空,环境变量IFS中的字符作为分隔符来将输入的内容分隔为单词。下面是一个示例:
1 | read -p "请输入您的用户名:" username |
输入超时
通过-t选项可以指定在某个时间内输入,如果没有输入就超时退出,比如上面的脚本
1 | read -t 5 -p "请输入您的用户名:" username |
每个参数等待5秒,如果没有输入提示下一个参数,直到退出
隐藏方式读取
-s选项可以隐藏用户的输入,比如密码输入的时候,比如上面的脚本
1 | read -s -p "请输入您的用户名:" username |
从文件中读取
read命令从文件中读取数据的方法主要有两种:通过文件描述符一行一行的读取文件内容(本节不讲),一种是在循环中使用命令cat filename
,其语法如下:
for data in $(cat filename)
do
执行命令
done
默认情况下这种方法是逐个单词的读取文件内容,因为在读取时它以环境变量IFS的值为分隔符,默认值为空格|tab|newline,所以默认回以空格为界限读取,如果想要一次读取一行需要修改环境变量IFS的值。下面演示一个循环按行读取文件内容的例子
首先创建一个文件at.txt如下
1 | 1 3 5 7 9 |
其次,构建如下脚本
1 | #使用临时变量保存$IFS的值 |
执行脚本
1 | ./test.sh at.txt |
假如上面脚本中我们不修改环境变量IFS的值,如下:
1 | #如果指定的命令行参数个数不为1,则显示脚本的使用方法,然后退出 |
再次执行脚本如下:
1 | ./test.sh at.txt |
使用while循环也可以读取文件内容,但是会消除每行原有的格式,去掉重复的空格和制表符,而将for循环结合环境变量$IFS使用可以保留每行原有的格式,按照自己的需求来决定使用哪种方式。