为单个语句设置IFS


42

我知道可以为单个命令/内置命令的范围设置自定义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

Answers:


39

在某些shell中(包括bash):

IFS=: command eval 'p=($PATH)'

(使用bash,您可以省略commandsh / POSIX仿真中的if)。但是请注意,在使用不带引号的变量时,通常还需要set -f,而且大多数shell中都没有本地范围。

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

(){ local IFS=:; p=($=PATH); }

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

(){...}(或function {...})称为匿名函数,通常用于设置本地范围。对于其他在功能上支持局部作用域的shell,您可以执行以下操作:

e() { eval "$@"; }
e 'local IFS=:; p=($PATH)'

要在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'

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

IFS=$'\n' a=($str)

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

注释说明var=value cmd

在:

var=value cmd arg

shell将执行/path/to/cmd一个新的进程和传球cmd,并argargv[]var=valueenvp[]。那实际上不是变量分配,而是更多将环境变量传递给执行的命令。在Bourne或Korn外壳程序中set -k,您甚至可以编写它cmd var=value arg

现在,这不适用于未执行的内建函数或函数。在Bourne Shell中,在中var=value some-builtinvar最终设置为,就像var=value单独设置一样。例如,这意味着var=value echo foo(无效)的行为根据是否echo内置而有所不同。

POSIX和/或ksh更改了Bourne行为,仅发生在称为特殊内建类的内建类类别中。eval是一个特殊的内置read函数,不是。对于非特殊内置命令,仅var=value builtin设置var为执行该命令,使其行为类似于运行外部命令时的行为。

command命令可用于删除那些特殊内置函数的特殊属性。POSIX忽略的是对于和内置程序,这意味着外壳将必须实现变量栈(即使它没有指定或作用域限制命令),因为您可以这样做:eval.localtypeset

a=0; a=1 command eval 'a=2 command eval echo \$a; echo $a'; echo $a

甚至:

a=1 command eval myfunction

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

这是一个真正的俯瞰,因为ksh(该规范主要是基于)没有实现它(和AT&T kshzsh仍然没有),但现在,除了这两个,大多数shell执行它。外壳之间的行为有所不同,例如:

a=0; a=1 command eval a=2; echo "$a"

虽然。local在支持它的shell上使用是实现本地范围的一种更可靠的方法。


奇怪的是,仅按照POSIX的要求,在Dash,pdksh和bash中IFS=: command eval …设置IFS持续时间eval,而在ksh 93u中则不设置。看到ksh是奇数不合规一出是不寻常的。
吉尔(Gilles)'所以

12

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

#!/bin/sh
old_IFS=$IFS
IFS="something_new"
some_program_or_builtin
IFS=${old_IFS}

2
谢谢,并+1。是的,我知道此选项,但是如果您知道我的意思,我想知道是否存在“更清洁”的选项
iruvar 2013年

您可以使用分号将其卡在一行中,但我认为这不是更干净的方法。如果您想表达的所有内容都得到特殊的句法支持,那可能会很好,但是我们可能不得不学习木工或sumptin而不是编码;)
msw

9
$IFS如果以前未设置,则无法正确还原。
斯特凡Chazelas

2
如果它未设置,猛砸把它当作$'\t\n'' ',这里解释:wiki.bash-hackers.org/syntax/expansion/...
达维德

2
@davide,那就是$' \t\n'。空间必须是第一个,因为它用于"$*"。请注意,在所有类似Bourne的壳中,它都是相同的。
斯特凡Chazelas

8

将脚本放入函数中,然后调用该函数,将命令行参数传递给该函数。由于IFS是在本地定义的,因此对其进行的更改不会影响全局IFS。

main() {
  local IFS='/'

  # the rest goes here
}

main "$@"

6

对于此命令:

IFS=$'\n' a=($str)

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

$ split(){ a=( $str ); }
$ IFS=$'\n' split

这将使IFS处于调用拆分的环境中,但不会保留在当前环境中。

这也避免了总是很危险地使用eval。


在ksh93和mksh中,以及在POSIX模式下的bash和zsh中,仍然保留POSIX要求的$IFS设置$'\n'
斯特凡Chazelas

4

@helpermethod提出的答案当然是一种有趣的方法。但这也是一个陷阱,因为在BASH中,局部变量作用域从调用者扩展到被调用函数。因此,在main()中设置IFS将导致该值持久保存到从main()调用的函数中。这是一个例子:

#!/usr/bin/env bash
#
func() {
  # local IFS='\'

  local args=${@}
  echo -n "$FUNCNAME A"
  for ((i=0; i<${#args[@]}; i++)); do
    printf "[%s]: %s" "${i}" "${args[$i]}"
  done
  echo

  local f_args=( $(echo "${args[0]}") )
  echo -n "$FUNCNAME B"
  for ((i=0; i<${#f_args[@]}; i++)); do
    printf "[%s]: %s" "${i}" "${f_args[$i]}  "
  done
  echo
}

main() {
  local IFS='/'

  # the rest goes here
  local args=${@}
  echo -n "$FUNCNAME A"
  for ((i=0; i<${#args[@]}; i++)); do
    printf "[%s]: %s" "${i}" "${args[$i]}"
  done
  echo

  local m_args=( $(echo "${args[0]}") )
  echo -n "$FUNCNAME B"
  for ((i=0; i<${#m_args[@]}; i++)); do
    printf "[%s]: %s" "${i}" "${m_args[$i]}  "
  done
  echo

  func "${m_args[*]}"
}

main "$@"

和输出...

main A[0]: ick/blick/flick
main B[0]: ick  [1]: blick  [2]: flick
func A[0]: ick/blick/flick
func B[0]: ick  [1]: blick  [2]: flick

如果在main()中声明的IFS仍不在func()的范围内,则该数组将无法在func()B中正确解析。取消注释func()的第一行,您将获得以下输出:

main A[0]: ick/blick/flick
main B[0]: ick  [1]: blick  [2]: flick
func A[0]: ick/blick/flick
func B[0]: ick/blick/flick

如果IFS超出范围,您应该得到什么。

恕我直言,更好的解决方案是在全球/本地级别放弃更改或依赖IFS。而是生成一个新的shell并在其中摆弄IFS。例如,如果要按以下方式在main()中调用func(),则将数组作为带有反斜杠字段分隔符的字符串传递:

func $(IFS='\'; echo "${m_args[*]}")

...对IFS的更改将不会反映在func()中。该数组将作为字符串传递:

ick\blick\flick

...但是在func()内部,除非在func()中进行了局部更改,否则IFS仍将是“ /”(在main()中设置)。

可以从以下链接查看有关隔离IFS更改的更多信息:

如何将bash数组变量转换为以换行符分隔的字符串?

Bash字符串与IFS进行数组

常规shell脚本编程的提示和技巧-请参阅“注意使用子shell ...”


确实很有趣……
iruvar 2014年

“用IFS数组打击字符串” IFS=$'\n' declare -a astr=(...)非常感谢!
Aquarius Power

1

问题的摘录:

IFS=$'\n' a=($str)

被解释为从左到右评估的两个单独的全局变量分配,并且等效于:

IFS=$'\n'; a=($str)

要么

IFS=$'\n'
a=($str)

这解释了为什么IFS修改了global 以及为什么$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]}'"

但是您很快就会注意到,对的修改a也仅限于以下子外壳:

Subshell IFS: :
Subshell: a[0]='value 0' a[1]='value 1'
Parent IFS: $' \t\n'
Parent: a[0]='old' a[1]='values'

接下来,您将很想使用@msw的上一个答案中的解决方案来保存/恢复IFS ,或者尝试使用@helpermethod 建议local IFS内部函数。但是很快,您会发现自己遇到了种种麻烦,尤其是如果您是一位图书馆作者,需要坚决防止行为不当的脚本调用:

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

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

  • 要将变量修改限制为单个命令,内置命令或函数调用,请使用IFS="value" command

    • 要通过分割特定字符来读取多个变量(:以下用作示例),请使用:

      IFS=":" read -r var1 var2 <<< "$str"
    • 要读入数组,请使用(代替array_var=( $str )):

      IFS=":" read -r -a array_var <<< "$str"
  • 将修改变量的影响限制在子外壳中。

    • 要输出以逗号分隔的数组元素:

      (IFS=","; echo "${array[*]}")
    • 要将其捕获到字符串中:

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

0

最直接的解决方案是获取原始副本,$IFS例如在msw的答案中。但是,此解决方案无法区分unset IFSIFS等于空字符串的set,这对于许多应用程序来说很重要。这是捕获这种区别的更通用的解决方案:

# Functions taking care of IFS
set_IFS(){
    if [ -z "${IFS+x}" ]; then
        IFS_ori="__unset__"
    else
        IFS_ori="$IFS"
    fi
    IFS="$1"
}
reset_IFS(){
    if [ "${IFS_ori}" == "__unset__" ]; then
        unset IFS
    else
        IFS="${IFS_ori}"
    fi
}

# Example of use
set_IFS "something_new"
some_program_or_builtin
reset_IFS
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.