为单个语句设置 IFS

iru*_*var 50 bash

我知道可以为单个命令/内置的范围设置自定义 IFS 值。有没有办法为单个语句设置自定义 IFS 值?显然不是,因为根据下面的尝试,全局 IFS 值会受到影响

#check environment IFS value, it is space-tab-newline
printf "%s" "$IFS" | od -bc
0000000 040 011 012
             \t  \n
0000003
#invoke built-in with custom IFS
IFS=$'\n' read -r -d '' -a arr <<< "$str"
#environment IFS value remains unchanged as seen below
printf "%s" "$IFS" | od -bc
0000000 040 011 012
             \t  \n
0000003

#now attempt to set IFS for a single statement
IFS=$'\n' a=($str)
#BUT environment IFS value is overwritten as seen below
printf "%s" "$IFS" | od -bc
0000000 012
         \n
     0000001
Run Code Online (Sandbox Code Playgroud)

Sté*_*las 49

在某些 shell 中(包括bash):

IFS=: command eval 'p=($PATH)'
Run Code Online (Sandbox Code Playgroud)

(使用bash,您可以command在 sh/POSIX 仿真中省略if not)。但请注意,当使用不带引号的变量时,您通常还需要set -f,并且在大多数 shell 中没有局部作用域。

使用 zsh,您可以执行以下操作:

(){ local IFS=:; p=($=PATH); }
Run Code Online (Sandbox Code Playgroud)

$=PATH是强制分词,这在默认情况下未完成zsh(也未完成变量扩展时的通配符,因此set -f除非在 sh 仿真中,否则您不需要)。

但是,在 中zsh,您宁愿使用$pathwhich 是一个绑定到的数组$PATH,或者使用任意分隔符拆分:p=(${(s[:])PATH})或者p=("${(s[:]@)PATH}")保留空元素。

(){...}(或function {...}) 称为匿名函数,通常用于设置局部作用域。对于支持函数局部作用域的其他 shell,您可以执行类似的操作:

e() { eval "$@"; }
e 'local IFS=:; p=($PATH)'
Run Code Online (Sandbox Code Playgroud)

要在 POSIX shell 中实现变量和选项的局部范围,您还可以使用https://github.com/stephane-chazelas/misc-scripts/blob/master/locvar.sh提供的函数。然后您可以将其用作:

. /path/to/locvar.sh
var=3,2,2
call eval 'locvar IFS; locopt -f; IFS=,; set -- $var; a=$1 b=$2 c=$3'
Run Code Online (Sandbox Code Playgroud)

(顺便$PATH说一句,除了zsh在其他 shell 中,IFS 是字段分隔符,而不是字段分隔符,否则以上述方式拆分是无效的)。

IFS=$'\n' a=($str)
Run Code Online (Sandbox Code Playgroud)

只是两个作业,一个接一个就好了a=1 b=2

注释说明var=value cmd

在:

var=value cmd arg
Run Code Online (Sandbox Code Playgroud)

shell/path/to/cmd在一个新进程中执行并传入cmdand arginargv[]var=valuein envp[]。这不是真正的变量赋值,而是更多地将环境变量传递给执行的命令。在 Bourne 或 Korn shell 中,使用set -k,您甚至可以编写它cmd var=value arg

现在,这不适用于未执行的内置函数或函数。在 Bourne shell 中, in var=value some-builtin,var最终会在之后设置,就像 with 一样var=value。这意味着例如var=value echo foo(这是没有用的)的行为取决于是否echo内置。

POSIX 和/或ksh改变了这一点,因为 Bourne 行为只发生在称为special builtins 的一类内置函数中eval是一个特殊的内置read函数,不是。对于非特殊内置,var=value builtinvar只为这使得它表现类似于当外部命令运行到内建的执行。

command命令可用于删除那些特殊内置函数的特殊属性。POSIX 忽略的是,对于和内置函数,这意味着 shell 必须实现一个变量堆栈(即使它没有指定或范围限制命令),因为你可以这样做:eval.localtypeset

a=0; a=1 command eval 'a=2 command eval echo \$a; echo $a'; echo $a
Run Code Online (Sandbox Code Playgroud)

甚至:

a=1 command eval myfunction
Run Code Online (Sandbox Code Playgroud)

myfunction作为一个函数使用或设置$a和潜在呼叫command eval

这是一个真正的俯瞰,因为ksh(该规范主要是基于)没有实现它(和AT&Tkshzsh仍然没有),但现在,除了这两个,大多数shell执行它。不同 shell 的行为有所不同,但在以下方面:

a=0; a=1 command eval a=2; echo "$a"
Run Code Online (Sandbox Code Playgroud)

尽管。local在支持它的 shell 上使用是实现局部作用域的更可靠的方法。


msw*_*msw 16

来自 Kernighan 和 Pike 的“Unix 编程环境”的标准保存和恢复:

#!/bin/sh
old_IFS=$IFS
IFS="something_new"
some_program_or_builtin
IFS=${old_IFS}
Run Code Online (Sandbox Code Playgroud)

  • 如果之前未设置,则无法正确恢复 `$IFS`。 (10认同)
  • 谢谢和+1。是的,我知道这个选项,但如果你知道我的意思,我想知道是否有“更清洁”的选项 (2认同)
  • 如果未设置,Bash 会将其视为 `$'\t\n'' '`,如下所述:http://wiki.bash-hackers.org/syntax/expansion/wordsplit#internal_field_separator_ifs (2认同)
  • @davide,那就是`$' \t\n'`。空间必须是第一个,因为它用于`"$*"`。请注意,它在所有类似 Bourne 的 shell 中都是一样的。 (2认同)

hel*_*hod 10

将您的脚本放入一个函数并调用该函数,并将命令行参数传递给它。由于 IFS 是本地定义的,因此对其的更改不会影响全局 IFS。

main() {
  local IFS='/'

  # the rest goes here
}

main "$@"
Run Code Online (Sandbox Code Playgroud)


小智 7

对于这个命令:

IFS=$'\n' a=($str)
Run Code Online (Sandbox Code Playgroud)

有一个替代解决方案:给第一个赋值 ( IFS=$'\n') 一个要执行的命令(一个函数):

$ split(){ a=( $str ); }
$ IFS=$'\n' split
Run Code Online (Sandbox Code Playgroud)

那会将 IFS 放入环境中调用 split,但不会保留在当前环境中。

这也避免了始终冒险使用 eval。


sls*_*sls 5

这个问题的片段:

IFS=$'\n' a=($str)
Run Code Online (Sandbox Code Playgroud)

被解释为从左到右计算的两个单独的全局变量赋值,等效于:

IFS=$'\n'; a=($str)
Run Code Online (Sandbox Code Playgroud)

或者

IFS=$'\n'
a=($str)
Run Code Online (Sandbox Code Playgroud)

这既解释了为什么IFS修改了全局$str变量,也解释了为什么使用 的新值将 的词拆分为数组元素IFS

您可能会想使用子外壳来限制IFS修改的效果,如下所示:

str="value 0:value 1"
a=( old values )
( # Following code runs in a subshell
 IFS=":"
 a=($str)
 printf 'Subshell IFS: %q\n' "${IFS}"
 echo "Subshell: a[0]='${a[0]}' a[1]='${a[1]}'"
)
printf 'Parent IFS: %q\n' "${IFS}"
echo "Parent: a[0]='${a[0]}' a[1]='${a[1]}'"
Run Code Online (Sandbox Code Playgroud)

但是你很快就会注意到 的修改a也仅限于子shell:

Subshell IFS: :
Subshell: a[0]='value 0' a[1]='value 1'
Parent IFS: $' \t\n'
Parent: a[0]='old' a[1]='values'
Run Code Online (Sandbox Code Playgroud)

接下来,您可能会想使用@msw之前的答案中的解决方案来保存/恢复 IFS ,或者尝试使用@helpermethod建议local IFS内部函数。但是很快,您就会发现自己遇到了各种各样的麻烦,尤其是如果您是一位需要对行为不端的调用脚本保持稳健的库作者:

  • 如果IFS最初未设置怎么办?
  • 如果我们正在运行set -u(又名set -o nounset)怎么办?
  • 如果IFS通过 设为只读declare -r IFS怎么办?
  • 如果我需要保存/恢复机制来处理递归和/或异步执行(例如处理trap程序)怎么办?

请不要保存/恢复 IFS。相反,坚持临时修改: