“备份” $ IFS变量是否明智?


19

我总是很犹豫,$IFS因为它正在破坏全球市场。

但是,这通常使将字符串加载到bash数组中变得简洁明了,而且对于bash脚本而言,简洁性很难实现。

因此,我认为如果尝试将“开始”内容“保存” $IFS到另一个变量,然后在用完$IFS某些东西后立即将其还原,那总比没有好。

这可行吗?还是从本质上讲毫无意义,我应该直接将其设置IFS为后续使用所需的任何内容?


为什么不实际呢?
布莱奇利'16

因为取消设置IFS可以很好地完成工作。
llua '16

1
对于那些说可以将IFS设置为无效的人,请记住这是视情况而定:stackoverflow.com/questions/39545837/…。根据我的经验,最好将IFS手动设置为Shell解释器的默认值,即$' \t\n'如果您正在使用bash。unset $IFS只是并不总是将其还原为您期望的默认值。
Darrel Holt

Answers:


9

您可以根据需要保存并分配给IFS。这样做没有错。像数组分配示例一样,在临时的,迅速的修改之后,保​​存其值以进行恢复并不少见。

正如@llua在对您的问题的评论中提到的那样,只需取消设置IFS即可恢复默认行为,等同于分配space-tab-newline。

值得考虑的是,没有显式设置/未设置IFS可能比这样做更成问题。

从POSIX 2013版本2.5.3 Shell变量开始

在调用外壳程序时,实现可能会忽略环境中IFS的值,或者环境中缺少IFS,在这种情况下,外壳程序在调用时应将IFS设置为<space> <tab> <newline> 。

兼容POSIX的被调用Shell可能会也可能不会从其环境继承IFS。由此可见:

  • 可移植脚本无法通过环境可靠地继承IFS。
  • 旨在仅使用默认拆分行为(或使用join的情况"$*"),但可以在从环境初始化IFS的外壳下运行的脚本,必须明确设置/取消设置IFS,以防御环境入侵。

注意:重要的是要理解,在此讨论中,“调用”一词具有特定的含义。仅当使用其名称(包括#!/path/to/shellshebang)显式调用shell时,才调用shell 。子外壳程序(例如可能由$(...)或创建的子cmd1 || cmd2 &外壳程序)不是被调用的外壳程序,其IFS(及其大多数执行环境)与父级外壳程序相同。被调用的外壳将其值设置$为其pid,而子外壳则继承它。


这不仅仅是一个书呆子的问题。在这方面确实存在分歧。这是一个简短的脚本,它使用几种不同的外壳测试场景。它将修改后的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=:,而破折号和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。


我明白了,所以,如果我可以解释一下,可以说在大多数情况下显式地指定要使用的值更容易移植IFS,因此即使尝试“保留”其原始值通常也不是很有效。
史蒂文·卢

1
最重要的问题是,如果您的脚本使用IFS,则应明确设置/取消设置IFS以确保其值就是您想要的值。通常,如果有任何未引用的参数扩展,未引用的命令替换,未引用的算术扩展read,或对的双引号引用,则脚本的行为取决于IFS $*。该列表只是我的头上,所以它可能并不全面(尤其是考虑到现代外壳的POSIX扩展时)。
赤脚IO

10

通常,将条件恢复为默认值是一种好习惯。

但是,在这种情况下,不需要那么多。

为什么?:

另外,存储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.

IFS不能真正被取消。如果取消设置,则外壳程序会将其还原为默认值。因此,保存时实际上不需要检查它。
filbranden '18

请注意,如果在父上下文(函数上下文)中而不是在当前上下文中声明了本地IFS,则在bashunset IFS无法取消设置IFS。
斯特凡Chazelas

5

您应该犹豫是否破坏全球。不用担心,有可能编写干净的工作代码而无需修改实际的global IFS,也无需进行繁琐且容易出错的保存/恢复操作。

您可以:

  • 为单个调用设置IFS:

    IFS=value command_or_function

    要么

  • 在子shell中设置IFS:

    (IFS=value; statement)
    $(IFS=value; statement)

例子

  • 要从数组获取逗号分隔的字符串:

    str="$(IFS=, ; echo "${array[*]-}")"

    注意:-只能set -u通过在未设置时提供默认值(在这种情况下该值为空字符串)来保护空数组

    IFS修改仅适用于由$() 命令替换产生的子外壳内部。这是因为子外壳程序具有调用外壳程序变量的副本,因此可以读取其值,但是子外壳程序执行的任何修改仅影响子外壳程序的副本,而不会影响父外壳程序的变量。

    您可能还在想:为什么不跳过子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[*]}"为字符串的魔力是在echo调用之前由(sub)shell自身完成的。

  • 将整个文件(不包含NULL字节)读入名为的单个变量中VAR

    IFS= read -r -d '' VAR < "${filepath}"

    注意: IFS=IFS=""和相同IFS='',所有都将IFS设置为空字符串,这与unset IFS:有很大不同:如果IFS未设置,则内部使用的所有bash功能的行为与默认值IFS完全相同。IFS$' \t\n'

    设置IFS为空字符串可确保保留前导和尾随空格。

    -d ''-d ""告诉读取只停止在其当前调用NULL字节,而不是通常的换行符。

  • 要拆分$PATH沿其:分隔符:

    IFS=":" read -r -d '' -a paths <<< "$PATH"

    这个例子纯粹是说明性的。在通常情况下,您将沿着分隔符进行拆分,各个字段可能包含该分隔符(该版本的转义版本)。考虑尝试读取.csv文件的某行,该文件的各列本身可能包含逗号(以某种方式转义或引用)。以上代码段不适用于此类情况。

    也就是说,您不太可能在中遇到此类包含:路径$PATH。虽然UNIX / Linux路径名允许包含a :,但如果您尝试将bash添加到自己的路径中$PATH并在其中存储可执行文件,bash似乎将无法处理这些路径,因为没有代码可以解析转义/引用的冒号:bash 4.4的源代码

    最后,请注意,此代码段将尾随换行符附加到结果数组的最后一个元素(如@StéphaneChazelas在现在删除的注释中所指出的那样),并且如果输入为空字符串,则输出将为单个元素数组,其中元素将由换行符($'\n')组成。

动机

对于最简单的脚本old_IFS="${IFS}"; command; IFS="${old_IFS}",触及全局的基本方法IFS将按预期工作。但是,一旦增加任何复杂性,它就很容易分解并引起细微的问题:

  • 如果commandbash函数也修改了全局变量IFS(直接或隐藏在它调用的另一个函数内部),并且在这样做时错误地使用相同的全局old_IFS变量进行保存/恢复,则会出现错误。
  • @Gilles在此注释中所指出的,如果IFS未设置的原始状态,则如果常用(错误)使用set -u(aka set -o nounset)shell选项,则幼稚的保存和恢复将不起作用,甚至会导致彻底失败。生效。
  • 某些shell代码可能与主要执行流异步执行,例如使用信号处理程序(请参阅参考资料help trap)。如果该代码还修改了全局IFS或假定其具有特定值,则可能会得到一些细微的错误。

您可以设计一个更健壮的保存/恢复序列(例如在另一个答案中提出的序列,以避免某些或所有这些问题。但是,无论何时您需要临时自定义,都必须重复这段嘈杂的样板代码IFS。降低代码的可读性和可维护性。

类似库的脚本的其他注意事项

IFSShell函数库的作者尤其需要关注的是,他们需要确保其代码稳定运行,而不管其调用程序所IFS施加的全局状态(,shell选项...)如何,并且也根本不打扰该状态(调用程序可能依赖于此)使其始终保持静态)。

在编写库代码时,您不能依赖于IFS具有任何特定值(甚至不是默认值),也不能完全被设置。相反,您需要为IFS行为取决于的任何代码段显式设置IFS

如果IFS在此值所影响的代码的每一行中都将值显式设置为必要值(即使恰好是默认值),则使用此答案中描述的两种机制中的任何一种都适合于确定效果,那么代码都是独立于全局状态,避免完全破坏全局状态。这种方法的另一个好处是,使阅读该脚本的人非常明确,而该脚本IFS恰好以最小的文本成本(甚至是最基本的保存/恢复)对这一命令/扩展至关重要。

IFS无论如何,什么代码受到影响?

幸运的是,没有那么多重要的场景IFS(假设您总是引用自己的扩展):

  • "$*""${array[*]}"扩展
  • read针对多个变量(read VAR1 VAR2 VAR3)或数组变量(read -a ARRAY_VAR_NAME)的内置调用
  • read涉及到出现在中的前导/后跟空白字符或非空白字符时针对单个变量的调用IFS
  • 单词拆分(例如用于无引号的扩展名,您可能希望避免像瘟疫一样
  • 其他一些不太常见的场景(请参阅:IFS @ Greg的Wiki

我不能说我理解将$ PATH沿其:分隔符进行拆分的假设所有组件都不包含:self语句。分隔符:何时:会包含这些组件?
斯特凡Chazelas

@StéphaneChazelas好吧,它:是在大多数UNIX / Linux文件系统上的文件名中使用的有效字符,因此完全可以使用名称包含的目录:。也许某些shell :通过使用诸如之类的东西在PATH中进行了转义\:,然后您会看到出现的列不是实际的定界符(看来bash不允许这种转义。当遍历$PATH搜索:in 时使用的低级函数)一个C字符串:git.savannah.gnu.org/cgit/bash.git/tree/general.c#n891)。
sls

我修改了答案,希望可以使分裂$PATH示例:更加清晰。
sls

1
欢迎来到SO!感谢您提供如此深入的解答:)
史蒂文·卢

1

这可行吗?还是从本质上讲毫无意义,我应该直接将IFS设置回其后续使用所需的位置?

$' \t\n'当您要做的只是为什么要冒输入错误设置IFS的风险时

OIFS=$IFS
do_your_thing
IFS=$OIFS

另外,如果不需要在以下范围内设置/修改任何变量,则可以调用子shell:

( IFS=:; do_your_thing; )

这很危险,因为如果IFS最初未设置,则无法使用。
吉尔斯(Gilles)“所以
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.