美文网首页程序人生
详细解析Shell中的IFS变量

详细解析Shell中的IFS变量

作者: 洛奇看世界 | 来源:发表于2018-06-01 00:17 被阅读73次

    题图:Photo by Jacob Postuma on Unsplash

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

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

    • 如果想了解我是如何找到介绍IFS资料的,请跳转到第1节,如何找到介绍IFS的资料?
    • 如果想知道Bash手册是如何介绍IFS的,请跳转到第2节, Bash手册中关于IFS的介绍;
    • 如果想看一些IFS使用相关的例子,请跳转到第3节, IFS使用的一些例子;
    • 如果想看一些IFS的结论,请跳转到第4节;

    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个,包括:

    • 第71页,第5.1节(“5.1 Bourne Shell Variables”)对shell变量IFS的定义
    • 第30页,第3.5.7节(“3.5.7 Word Splitting”)对字符分割中使用IFS的介绍
    • 第20页,第3.4.2节(“3.4.2 Special Parameters”)对特殊参数使用IFS的介绍
    • 第92页,第6.7节(“6.7 Arrays”)对数组引用中使用IFS的介绍

    以上列举的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工作描述的重中之重,主要有以下几点:

    • Shell把变量IFS内的每一个字符都当做是一个分割符(delimeter),用这些字符作为每一个字段的结束符来进行分割。
    • 如果IFS没有设置,或者IFS的值被设置为“ \t\n”(space, tab和 newline),那么操作对象的开始和结束处的所有space, tab和newline序列都将被忽略,但是操作对象中间的space, tab和newline序列会作为界定符工作。
    • 如果IFS值不是默认值(例如程序中对IFS进行设置过),只有出现在IFS内的空白字符(可能是space, tab或newline中的一个或几个)才会在单词开始和结束处被忽略,这里说的是单词,而不是整个操作对象。
    • IFS内的非空白字符多个连续出现时,每个非空白字符会被当做单独的分隔符看待,但是多个连续的空白字符会被当做一个分隔符看待。
    • 如果IFS为空(“null”),则不会进行单词分割。

    2.3 特殊参数$*中使用IFS

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

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

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

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

    • 当用双引号(“double quotes”)来引用特殊变量$*时,会使用IFS变量的第1个字符来连接$*参数的每一个部分,即"$*"相当于"$1c$2c...",其中c是IFS变量的第一个字符。
    • 如果没有设置IFS,则c为空格字符(space),实际上默认情况下IFS变量的第1个字符就是空格字符。
    • 如果IFS为空(null),则$*内各参数会直接连接在一起。

    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
    
    • 第1种方式:使用一个中间变量保存原始值,然后修改IFS,操作完成后再使用中间变量恢复IFS。
    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
    
    • 第2种方式:使用local来声明要使用的IFS变量来覆盖全局变量,由于local变量只在局部有效,所以操作完不需要恢复IFS。
    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的一些结论:

    • IFS本身是一个包含1个或多个字符的列表
    • 不确定IFS内容时使用“echo -n "$IFS" | hexdump”将其以十六进制的方式打印出来看看
    • 多个IFS内的非空白分割字符出现在一起时,每个分割符单独起作用;但如果IFS内的空白字符多个连续出现时,会将多个连续空白字符整体当做一个分隔符
    • IFS既可以用于单个元素分割,也和以用于多个元素组合为单个元素。对于使用*作为下标引用数组类元素(包括特殊参数$*和数组, 都是数组类元素)时,双引号包含的引用会将多个元素扩展组合生成单个元素,但这个新元素的内部是使用IFS的第1个字符进行连接。

    相关文章

      网友评论

        本文标题:详细解析Shell中的IFS变量

        本文链接:https://www.haomeiwen.com/subject/mjzcsftx.html