我总是很犹豫是否要乱搞,$IFS
因为它正在破坏全局。
但通常它使得将字符串加载到 bash 数组中变得漂亮和简洁,而对于 bash 脚本来说,简洁性很难实现。
因此,我认为如果我尝试将 的起始内容“保存”$IFS
到另一个变量,然后在使用完$IFS
某些内容后立即恢复它,可能比什么都不做要好。
这实用吗?或者它本质上毫无意义,我应该直接设置IFS
回其后续使用所需的任何内容?
答案1
一般来说,将条件恢复为默认值是一个很好的做法。
然而,在这种情况下,情况并非如此。
为什么?:
- 每次脚本启动时(在 bash 中)IFS 设置为
$' \t\n'
。 - 只需执行
unset IFS
即可就好像它被设置为默认值一样。
另外,存储 IFS 值也存在问题。
如果原始 IFS 未设置,则代码IFS="$OldIFS"
会将 IFS 设置为""
,而不是取消设置。
要实际保留 IFS 的值(即使未设置),请使用以下命令:
${IFS+"false"} && unset oldifs || oldifs="$IFS" # correctly store IFS.
IFS="error" ### change and use IFS as needed.
${oldifs+"false"} && unset IFS || IFS="$oldifs" # restore IFS.
答案2
您可以根据需要保存并分配给 IFS。这样做并没有什么错。在临时、快速的修改之后保存其值以供恢复的情况并不罕见,就像您的数组分配示例一样。
正如 @llua 在对您的问题的评论中提到的那样,只需取消设置 IFS 将恢复默认行为,相当于分配空格制表符换行符。
值得考虑的是如何可能会出现更多问题不是显式设置/取消设置 IFS 比这样做更重要。
从 POSIX 2013 版开始,2.5.3 Shell 变量:
在调用 shell 时,实现可能会忽略环境中 IFS 的值,或者环境中不存在 IFS,在这种情况下,shell 在调用时应将 IFS 设置为 <space> <tab> <newline> 。
符合 POSIX 标准的调用 shell 可能会也可能不会从其环境继承 IFS。由此可知:
- 可移植脚本无法通过环境可靠地继承 IFS。
- 打算仅使用默认拆分行为(或在 的情况下加入
"$*"
)但可能在从环境初始化 IFS 的 shell 下运行的脚本必须显式设置/取消设置 IFS 以防御环境入侵。
注意:重要的是要理解,对于本次讨论,“调用”一词具有特定的含义。仅当使用其名称(包括 shebang #!/path/to/shell
)显式调用 shell 时,才会调用 shell。子 shell(例如可能由$(...)
或创建的子cmd1 || cmd2 &
shell)不是被调用的 shell,并且其 IFS(以及其大部分执行环境)与其父级相同。被调用的 shell 将 的值设置$
为其 pid,而子 shell 继承它。
这不仅仅是一篇迂腐的论文;这方面确实存在分歧。下面是一个简短的脚本,它使用几个不同的 shell 来测试该场景。它将修改后的 IFS(设置为:
)导出到调用的 shell,然后打印其默认 IFS。
$ cat export-IFS.sh
export IFS=:
for sh in bash ksh93 mksh dash busybox:sh; do
printf '\n%s\n' "$sh"
$sh -c 'printf %s "$IFS"' | hexdump -C
done
IFS 通常不会标记为导出,但是,如果是,请注意 bash、ksh93 和 mksh 如何忽略其环境的IFS=:
,而 dash 和 busybox 则遵循它。
$ sh export-IFS.sh
bash
00000000 20 09 0a | ..|
00000003
ksh93
00000000 20 09 0a | ..|
00000003
mksh
00000000 20 09 0a | ..|
00000003
dash
00000000 3a |:|
00000001
busybox:sh
00000000 3a |:|
00000001
一些版本信息:
bash: GNU bash, version 4.3.11(1)-release
ksh93: sh (AT&T Research) 93u+ 2012-08-01
mksh: KSH_VERSION='@(#)MIRBSD KSH R46 2013/05/02'
dash: 0.5.7
busybox: BusyBox v1.21.1
尽管 bash、ksh93 和 mksh 不会从环境中初始化 IFS,但它们会重新导出修改后的 IFS。
如果出于某种原因您需要通过环境可移植地传递 IFS,则无法使用 IFS 本身来执行此操作;您需要将值分配给不同的变量并将该变量标记为导出。然后,孩子们需要将该值显式分配给他们的 IFS。
答案3
你对破坏全局犹豫不决是正确的。不用担心,可以编写干净的工作代码,而无需修改实际的全局IFS
,或进行繁琐且容易出错的保存/恢复舞蹈。
你可以:
为单个调用设置 IFS:
IFS=value command_or_function
或者
在子 shell 中设置 IFS:
(IFS=value; statement) $(IFS=value; statement)
例子
要从数组中获取逗号分隔的字符串:
str="$(IFS=, ; echo "${array[*]-}")"
注意:这只是为了通过提供
-
来保护空数组set -u
未设置时的默认值(在本例中该值是空字符串) 。该
IFS
修改仅适用于由$()
命令替换。这是因为子 shell 具有调用 shell 变量的副本,因此可以读取它们的值,但子 shell 执行的任何修改只会影响子 shell 的副本,而不会影响父 shell 的变量。您可能还会想:为什么不跳过子 shell 并执行以下操作:
IFS=, str="${array[*]-}" # Don't do this!
这里没有命令调用,而是将这一行解释为两个独立的后续变量赋值,就好像它是:
IFS=, # Oops, global IFS was modified str="${array[*]-}"
最后,让我们解释一下为什么这个变体不起作用:
# Notice missing ';' before echo str="$(IFS=, echo "${array[*]-}")" # Don't do this!
该
echo
命令确实会被调用,其IFS
变量设置为,
,但echo
不关心或使用IFS
。扩展"${array[*]}"
为字符串的魔力是由(子)shell 本身在echo
调用之前完成的。要将整个文件(不包含
NULL
字节)读入名为 的单个变量VAR
:IFS= read -r -d '' VAR < "${filepath}"
注意: 与和
IFS=
相同,都将 IFS 设置为空字符串,这与 非常不同:如果未设置,内部使用的所有 bash 功能的行为与默认值完全相同。IFS=""
IFS=''
unset IFS
IFS
IFS
IFS
$' \t\n'
设置
IFS
为空字符串可确保保留前导和尾随空格。or告诉
-d ''
read-d ""
仅在一个NULL
字节上停止其当前调用,而不是通常的换行符。$PATH
沿着分隔符分割:
:IFS=":" read -r -d '' -a paths <<< "$PATH"
这个例子纯粹是说明性的。在沿着分隔符分割的一般情况下,各个字段可能包含该分隔符(的转义版本)。想象一下尝试读入文件的一行,
.csv
该文件的列本身可能包含逗号(以某种方式转义或引用)。对于这种情况,上面的代码片段将无法按预期工作。也就是说,您不太可能
:
在$PATH
.虽然 UNIX/Linux 路径名允许包含:
,但如果您尝试将它们添加到路径中并在其中存储可执行文件,bash 似乎无论如何都无法处理此类路径$PATH
,因为没有代码可以解析转义/引用的冒号:bash 4.4 的源代码。最后,请注意,该代码片段将尾随换行符附加到结果数组的最后一个元素(如 @StéphaneChazelas 在现已删除的注释中调用的那样),并且如果输入是空字符串,则输出将是单个元素数组,其中元素由换行符 (
$'\n'
) 组成。
动机
对于最简单的脚本来说,old_IFS="${IFS}"; command; IFS="${old_IFS}"
涉及全局的基本方法将按预期工作。IFS
然而,一旦增加任何复杂性,它就很容易崩溃并导致微妙的问题:
- 如果
command
是一个 bash 函数,它也修改全局变量IFS
(直接或隐藏在它调用的另一个函数中),并且在这样做时错误地使用相同的全局old_IFS
变量来执行保存/恢复,您会遇到错误。 - 正如所指出的在@Gilles 的评论中,如果未设置 的原始状态
IFS
,则简单的保存和恢复将不起作用,并且如果常用(误)使用的set -u
(又名set -o nounset
)shell 选项有效,甚至会导致彻底失败。 - 某些 shell 代码可能与主执行流异步执行,例如使用信号处理程序(请参阅 参考资料
help trap
)。如果该代码还修改了全局变量IFS
或假设它具有特定值,则可能会出现微妙的错误。
您可以设计一种更强大的保存/恢复序列(例如中提出的序列)这个另一个答案以避免部分或全部这些问题。但是,无论您临时需要自定义IFS
.这降低了代码的可读性和可维护性。
类库脚本的其他注意事项
IFS
对于 shell 函数库的作者来说尤其值得关注,他们需要确保他们的代码稳健地工作,无论调用者施加的全局状态(IFS
、shell 选项等)如何,并且根本不干扰该状态(调用者可能依赖使其始终保持静态)。
编写库代码时,您不能依赖于IFS
任何特定值(甚至不是默认值),甚至根本不能依赖于被设置。相反,您需要显式设置IFS
其行为依赖于IFS
.
如果IFS
在每一行代码中显式设置为必要的值(即使这恰好是默认值),其中该值很重要,使用此答案中描述的两种机制中的任意一种适合本地化效果,那么该代码既是独立于全局状态并避免完全破坏它。这种方法还有一个额外的好处,即IFS
以最小的文本成本(与最基本的保存/恢复相比),使其对于阅读脚本的人来说非常明确,这对于这个命令/扩展来说非常重要。
到底什么代码受到影响IFS
?
幸运的是,没有那么多IFS
重要的场景(假设你总是引用你的扩展):
"$*"
和"${array[*]}"
扩展- 调用
read
内置目标多个变量 (read VAR1 VAR2 VAR3
) 或数组变量 (read -a ARRAY_VAR_NAME
) read
当涉及到 中出现的前导/尾随空白或非空白字符时,针对单个变量IFS
的调用- 分词(例如未加引号的扩展,你可能想像瘟疫一样避免它)
- 其他一些不太常见的情况(参见:IFS @Greg 维基)
答案4
这实用吗?或者它本质上毫无意义,我应该直接将 IFS 设置回后续使用所需的任何值?
$' \t\n'
当您所要做的只是
OIFS=$IFS
do_your_thing
IFS=$OIFS
或者,如果您不需要在其中设置/修改任何变量,则可以调用子 shell:
( IFS=:; do_your_thing; )