程序人生

详细解析Shell中的IFS变量

2018-06-01  本文已影响73人  洛奇看世界

题图:Photo by Jacob Postuma on Unsplash

这里的Shell主要指bash,学习bash的前前后后在IFS变量上吃了不少苦头,虽然花了不少时间,也知道大概如何使用,但并没有深入理解。翻了几本Shell相关的书,对IFS也都是一带而过,并没有做详细的阐述(IFS本身在Shell里面就是很小很小的一个知识点而已,也不值得这些书花大篇幅去解释);尝试百度“Shell IFS”,大多数结果也不甚满意。终于决定要自己完整的了解下IFS了。严格来说,本文是对IFS文档描述和使用的考证说明。

本文主要有以下几个话题:

1. 如何找到介绍IFS的资料?

这个章节挺废话的,但为什么还会有这个章节呢?我只是希望通过这个章节向有些朋友展示我是如何思考,找到解决这个问题的方法的。拿到一个问题,并不是所有朋友都一下子能拿出很好的解决办法,这其中必然有个思考尝试的过程,而这一节,就是想向你展示我是如何思考的。这个过程可能走了很多弯路,并非一下就能找到正确的答案,但仍希望我的思考能对你有一丝的借鉴意义。

想了解IFS,必然需要找到详细的资料才行,可是,如果从来没了解过IFS,从哪里找到介绍IFS的资料呢?

查找资料的第一反应是搜索,但直接以关键字"IFS"/"shell IFS"/"bash IFS"进行百度,得到一大堆告诉你如何使用"IFS"的文章,但这并不是我想要的。

平时习惯了命令行的"man"/"help"方式,但是使用"man IFS"/"help IFS"/"info IFS"都无果啊。

后来想了想,IFS只是Shell里面的一个环境变量,使用"man shell"或"man bash"应该能找到介绍。果不其然,"man bash"找到了关于IFS的介绍。

以下是在"man bash"结果中“Shell Variables”一节关于“IFS”的介绍:

在"Word Spliting"一节找到更多关于IFS的介绍:

命令行执行"man bash"显示的内容太多,不方便阅读,后来想到通过"man bash | grep IFS"过滤只与IFS相关的内容,无奈,这个操作没有任何结果。 为什么没有任何结果呢?试着将"man bash"的结果输出到文本文件看看就知道了,里面插入太多显示格式字符,导致连一个完整的IFS字符串都找不到。

因此想到的办法是搜索并下载bash的手册(manual)看看,这样通过bash手册查找IFS相关内容就方便多了。

Bash Reference Manual

2. Bash手册中关于IFS的介绍

这一节看起来也挺废话的,因为其中好多都是直接应用官方文档。我一直认为做技术同做学问一样,都应该是严肃的。这里借用脱袜子大神(torvalds)的一句很有名的话,“Talk is cheap, show me your code”,我想说得是“Talk is cheap, show me your evidence”。所以这一节从官方文档出发,介绍IFS为什么会有这些特性。

本文基于地址“https://www.gnu.org/software/bash/manual/bash.pdf”下载得到的是针对Bash 4.4版本的手册:

This is Edition 4.4, last updated 7 September 2016, of The GNU Bash Reference Manual, for Bash, Version 4.4.

尽管Bash 4.4版本的手册可能跟你运行的bash不匹配,但bash总台上是很稳定的,各版本间差异不会太大,尽管是不同的版本,但也不会影响对IFS内容的理解。

在PDF版的bash手册中搜索“IFS”, 总共在13个章节找到33个结果,其中最重要的地方有4个,包括:

以上列举的4点并非按照先后顺序,而是按照个人理解的重要程度排列

下面详细介绍这4点。

2.1 变量IFS的定义

Bash手册第71页,第5.1节“Bourne Shell Variables”简单说了什么是IFS变量,如下:


这里提到IFS作为Shell的内置变量,是一个用于分割字段的字符列表(注意,这里是字符列表,说明其中可以包含不止一个字符)。

2.2 使用IFS进行单词分割

Bash手册的第30页,第3.5.7节“Word Splitting”描述了基于IFS进行分割的细节,如下:


这里说得比较详细,是对IFS工作描述的重中之重,主要有以下几点:

2.3 特殊参数$*中使用IFS

Bash手册的第20页,第3.4.2节“Special Parameters”介绍了特殊参数$*包含在双引号中时,组合的新字符串使用IFS的第1个字符进行连接,由于默认情况下IFS的第1个字符是空格,这就是为什么我们看到"$*"的结果是使用空格进行分隔,如下:

<<Linux命令行与Shell脚本编程大全》第2版的第276页是这样描述$和$@变量的:
$
和$@变量提供了对所有参数的快速访问,这两个都能够在单个变量中存储所有的命令行参数。
$变量会将命令行上提供的所有参数当做单个单词保存。每个词是指命令行上出现的每个值。基本上,$变量会将这些都当做一个参数,而不是多个对象。

反过来说,$@变量会将命令行上提供的所有参数当做同一字符串中的多个独立的单词。它允许你便利所有的值,将提供的每个参数分割开来。这通常通过for命令完成。

这里特别说了IFS对变量$*的扩展的影响,主要有3点:

2.4 数组引用中使用IFS

Bash手册的第92页,第6.7节“Arrays”介绍了IFS对数组元素引用的影响,如下:

这里强调引用数组元素时,还可以使用*和@下标,哈哈,没想到吧,跟命令行参数一样。 通常情况下是使用带下标的${name[subscript]}方式引用,但现在还可以使用*@来引用,如${name[*]}${name[@]}。 如果${name[*]}被包含在双引号内,则其将会用IFS的第1个字符连接数组的各个元素进行扩展,跟上1节使用双引号引用特殊参数$*一样。

3. IFS使用的一些例子

关于IFS的重点: IFS是shell的内置变量,IFS是一个字符列表,里面的每一个字符都会用来作为分隔符进行单词分割。

以下是使用IFS设置和分割的一些例子。

3.1 检查IFS的默认值

mbp:~ rocky$ echo -n "$IFS" | hexdump
0000000 20 09 0a
0000003

十六进制值0x20, 0x09和0x0a分别对应于空格(space), 水平制表符(tab)和换行符(newline)的值。

这里给echo使用“-n”参数避免在echo时在行位添加换行符。如下是不带“-n”的输出:

mbp:~ rocky$ echo "$IFS" | hexdump
0000000 20 09 0a 0a
0000004

跟前面的结果比较,这里最后多了一个字符0x0a。

3.2 IFS的修改和恢复

因为IFS是系统级变量,修改使用后记得要恢复原样,否则后续程序就会出现一些奇奇怪怪的异常,别怪我没告诉你啊,我自己曾经因为这个问题踩了个大坑。

这里以一个处理带空格的文件名来展示对IFS变量的修改。

操作目录下有一个名为"a b c.txt"的文件(字母a,b,c中间有两个空格):

mbp:shell rocky$ ls -lh
total 24
-rw-r--r--  1 rocky  admin     0B  5  6 01:21 a b c.txt
drwxr-xr-x  3 rocky  admin   442B  5  6 02:03 images
-rw-r--r--  1 rocky  admin   115B  5  6 02:04 test1.sh
-rw-r--r--  1 rocky  admin   202B  5  6 02:02 test2.sh
-rw-r--r--  1 rocky  admin   228B  5  6 02:03 test3.sh
mbp:shell rocky$ cat test1.sh
#!/bin/bash
echo "1.Test with default IFS:"
echo -n "$IFS" | hexdump
for item in `ls`
do
echo "file: $item"
done
mbp:shell rocky$
mbp:shell rocky$ bash test1.sh
1.Test with default IFS:
0000000 20 09 0a
0000003
file: a          <-- 错误的文件名
file: b          <-- 错误的文件名
file: c.txt      <-- 错误的文件名
file: images
file: test1.sh
file: test2.sh
file: test3.sh
mbp:shell rocky$ cat test2.sh
#!/bin/bash
echo "2.Test with new IFS:"
# 先打印默认的IFS
echo -n "$IFS" | hexdump
# 使用变量IFS_SAVE临时保存IFS
IFS_SAVE=$IFS
IFS=$'\n'
echo -n "$IFS" | hexdump
for item in `ls`
do
echo "file: $item"
done
# 从IFS_SAVE中恢复IFS
IFS=$IFS_SAVE
echo -n "$IFS" | hexdump
mbp:shell rocky$
mbp:shell rocky$ bash test2.sh
2.Test with new IFS:
0000000 20 09 0a  <-- 这里是原来默认的IFS
0000003
0000000 0a        <-- 这里是修改后的IFS,后面就使用'\n'来分割文件名
0000001
file: a b c.txt
file: images
file: test1.sh
file: test2.sh
file: test3.sh
0000000 20 09 0a  <-- 操作完后恢复默认的IFS
0000003
mbp:shell rocky$ cat test3.sh
#!/bin/bash
echo "3.Test with local IFS:"
function show_filename {
# 使用local变量IFS来保存临时设置,仅在函数内有效
local IFS=$'\n'
echo -n "$IFS" | hexdump
for item in `ls`
do
echo "file: $item"
done
}
# 先打印默认的IFS
echo -n "$IFS" | hexdump
# 函数内会更改IFS并进行操作,但函数内并不会进行恢复
show_filename
# 退出函数后再打印IFS看看
echo -n "$IFS" | hexdump
mbp:shell rocky$
mbp:shell rocky$ bash test3.sh
3.Test with local IFS:
0000000 20 09 0a  <-- 这里是原来默认的IFS
0000003
0000000 0a        <-- 这里是函数内local变量设置的IFS
0000001
file: a b c.txt
file: images
file: test1.sh
file: test2.sh
file: test3.sh
0000000 20 09 0a  <-- 退出函数后IFS没有被修改
0000003

3.3 IFS使用单个字符进行分割

IFS是一个字符列表,即使待分割字符串中有碰巧有多个分隔符在一起,他大爷的还是按单个字符分割。

亲,再次说明IFS是一个字符列表啊,我以前好长一段时间都不明白将IFS=$' \t\n'这样是什么意思。这里是说将space, tab, newline这3个字符作为分隔符。

假如有一个语句是这样的:var=abc12345 IFS=12,你猜这里的“IFS=12”是什么意思?他丫的就是将字符“1”和“2”这两个字符设置为分隔符啊,验证如下:

mbp:shell rocky$ var=abc12345 IFS=12
mbp:shell rocky$ echo -n "$IFS" | hexdump -C
00000000  31 32                                             |12|
00000002
mbp:shell rocky$ for item in $var; \
> do \
>   echo "<$item>";\
> done;
<abc>
<>
<345>

这里先定义了一个字符串var=abc12345,然后设置IFS=12,通过后面的hexdump我们看到IFS的实际内容已经变成了“1”和“2”两个字符。

然后用新的IFS来分割字符串“abc12345”,显然前面“abc”和后面的“345”都被分割为单独的字符串了。 从输出可见,中间还有一个空字符串,这个空串就是从1和2两个字符中间分割得到的。

所以,即使多个分隔符挨在一起,仍然是按照单个分隔符进行分割,没有你想的那么智能呢。

但有一种情况特殊,默认情况下IFS的值为空白分隔符" \t\n"(即space, tab和newline),按照手册3.5.7节中的说法,会将挨在一起的多个空白分隔符看做一个分隔符。

mbp:shell rocky$ var=$'abc \n45'
mbp:shell rocky$ echo -n "$IFS" | hexdump
0000000 20 09 0a
0000003
mbp:shell rocky$ for item in $var; \
> do \
>   echo "<$item>"; \
> done;
<abc>
<45>

这里字符串var的内部有两个分隔符(空格和换行符)挨在一起,但最后var被当做一个分割符进行分割得到了两个子串。

空格符、制表符(\t)、换行符(\n)这三个空白符在 IFS 中会被特殊对待,Shell 会把它们按照任意顺序任意数量组合成的字符串作为分隔符,而不是单个字符作为分隔符。

前面的例子提到的都是字符串的分割受IFS设置的影响,下面两个例子讲多个数据元素合并为一个时也受IFS设置的影响。

3.4 特殊参数$*受IFS影响

手册的3.4.2节讲参数$*被双引号包含时,其结果受IFS第一个字符的影响。下面列举一个例子来验证下:

mbp:shell rocky$ cat test5.sh
#!/bin/bash
# 1\. 使用默认的IFS
# 打印当前的IFS
echo -n "$IFS" | hexdump
# 以非双引号的方式引用$*
echo \$*=$*
# 以双引号的方式引用$*
echo "\"\$*\"=$*"
# 2\. 修改IFS为'-'进行测试
# 修改IFS并打印出来
IFS=$'-'
echo -n "$IFS" | hexdump
# 以非双引号的方式引用$*
echo \$*=$*
# 以双引号的方式引用$*
echo "\"\$*\"=$*"
mbp:shell rocky$
# 这里传入1,2,3,4,5共计5个参数
mbp:shell rocky$ bash test5.sh 1 2 3 4 5
0000000 20 09 0a  <-- 默认的IFS值
0000003
$*=1 2 3 4 5      <-- 以非双引号的方式($*)
"$*"=1 2 3 4 5    <-- 以双引号的方式("$*")
0000000 2d        <-- 修改后的IFS
0000001
$*=1 2 3 4 5      <-- 以非双引号的方式($*)
"$*"=1-2-3-4-5    <-- 以双引号的方式("$*")

可见,当修改IFS以后,对$使用双引号("$")会影响到合成的结果。

3.5 数组元素${array[*]}受IFS影响

mbp:shell rocky$ cat test6.sh
#!/bin/bash
# 定义数组var,包含1,2,3,4,5共5个元素
var=(1 2 3 4 5)
# 1\. 使用默认的IFS
echo -n "$IFS" | hexdump
# 以非双引号的方式引用
echo \${var[*]}=${var[*]}
# 以双引号的方式引用
echo "\"\${var[*]}\"=${var[*]}"
# 修改IFS
IFS=$'-'
# 使用需改的IFS
echo -n "$IFS" | hexdump
# 以非双引号的方式引用
echo \${var[*]}=${var[*]}
# 以双引号的方式引用
echo "\"\${var[*]}\"=${var[*]}"
mbp:shell rocky$
mbp:shell rocky$ bash test6.sh
0000000 20 09 0a       <-- 默认的IFS值
0000003
${var[*]}=1 2 3 4 5    <-- 以非双引号的方式(${var[*]})
"${var[*]}"=1 2 3 4 5  <-- 以双引号的方式("${var[*]}")
0000000 2d             <-- 修改后的IFS
0000001
${var[*]}=1 2 3 4 5    <-- 以非双引号的方式(${var[*]})
"${var[*]}"=1-2-3-4-5  <-- 以双引号的方式("${var[*]}")

3.6 建议以类似IFS=$'string'的方式来设置IFS

上面的各个例子中都是使用IFS=$'string'(例如:IFS=$' \t\n')的奇怪的方式来设置IFS,既然'\n'是常量,为什么前面还要使用$符号呢?

这里跟$'string'的特殊引用方式有关,详细解释参考Bash手册的第3.1.2.4节“ANSI-C Quoting”,这一节提到$'string'的引用方式会被当做特别对待,使用这种方式的值会使用反斜杠转义的字符。 对于IFS=$' \t\n'就包含了对“\t”和“\n”两个转义。

因此你能看到如下的两种方式是有区别的:

# 使用"string"的方式,无法使反斜杠转义后续字符
mbp:shell rocky$ IFS=" \t\n"
mbp:shell rocky$ echo -n "$IFS" | hexdump
0000000 20 5c 74 5c 6e
0000005

# 使用'string'的方式,无法使反斜杠转义后续字符
mbp:shell rocky$ IFS=' \t\n'
mbp:shell rocky$ echo -n "$IFS" | hexdump
0000000 20 5c 74 5c 6e
0000005

# 使用$'string'的方式,成功使用反斜杠转义
mbp:shell rocky$ IFS=$' \t\n'
mbp:shell rocky$ echo -n "$IFS" | hexdump
0000000 20 09 0a
0000003

从以上验证的结果可见,只有第三种方式,IFS才成功包含了转义字符,其结果为期望的space,tab和newline三个字符;二前面两种方式都原样包含了字符串中的5个字符。

非常抱歉,第一次在公众号写技术文章,贴代码这事真的让人想死啊!这次先按纯文本的方式发一篇,后续学习下公众号如何贴代码。

4. IFS的一些结论

以下是我对使用IFS的一些结论:

上一篇下一篇

猜你喜欢

热点阅读