这是“备份” $IFS 变量的明智方法吗?

这是“备份” $IFS 变量的明智方法吗?

我总是很犹豫是否要乱搞,$IFS因为它正在破坏全局。

但通常它使得将字符串加载到 bash 数组中变得漂亮和简洁,而对于 bash 脚本来说,简洁性很难实现。

因此,我认为如果我尝试将 的起始内容“保存”$IFS到另一个变量,然后在使用完$IFS某些内容后立即恢复它,可能比什么都不做要好。

这实用吗?或者它本质上毫无意义,我应该直接设置IFS回其后续使用所需的任何内容?

答案1

一般来说,将条件恢复为默认值是一个很好的做法。

然而,在这种情况下,情况并非如此。

为什么?:

另外,存储 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 IFSIFSIFSIFS$' \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; )

相关内容