如何在每个命令的基础上正确分配临时Bash变量?

8

在临时、每个命令变量赋值方面,Bash表现得似乎是不可预测的,特别是与IFS相关。

我经常将IFS分配到一个临时值中,与read命令结合使用。我希望使用相同的机制来定制输出,但目前只能使用函数或子shell来包含变量赋值。

$ while IFS=, read -a A; do
>   echo "${A[@]:1:2}"                # control (undesirable)
> done <<< alpha,bravo,charlie
bravo charlie

$ while IFS=, read -a A; do
>   IFS=, echo "${A[*]:1:2}"          # desired solution (failure)
> done <<< alpha,bravo,charlie
bravo charlie

$ perlJoin(){ local IFS="$1"; shift; echo "$*"; }
$ while IFS=, read -a A; do
>   perlJoin , "${A[@]:1:2}"          # function with local variable (success)
> done <<< alpha,bravo,charlie
bravo,charlie

$ while IFS=, read -a A; do
>   (IFS=,; echo "${A[*]:1:2}")       # assignment within subshell (success)
> done <<< alpha,bravo,charlie
bravo,charlie

如果以下代码块中的第二个赋值语句既不影响命令的环境,也不导致错误,则它的目的是什么?
$ foo=bar
$ foo=qux echo $foo
bar
3个回答

9
$ foo=bar
$ foo=qux echo $foo
bar

这是一个常见的Bash陷阱,而https://www.shellcheck.net/可以捕获它。

foo=qux echo $foo
^-- SC2097: This assignment is only seen by the forked process.
             ^-- SC2098: This expansion will not see the mentioned assignment.

问题在于第一个foo=bar设置的是bash变量,而不是环境变量。然后,使用内联foo=qux语法来为echo设置环境变量--但是echo实际上从未查看该变量。相反,$foo被识别为bash变量并替换为bar
因此,回到您的主要问题,您的最终尝试使用子shell基本上已经接近成功了--除了您实际上不需要子shell:
while IFS=, read -a A; do
  IFS=,; echo "${A[*]:1:2}"
done <<< alpha,bravo,charlie

输出:
bravo,charlie

为了完整起见,这里是一个最终的例子,它读取多行并使用不同的输出分隔符来演示不同的IFS赋值未发生冲突:

while IFS=, read -a A; do
  IFS=:; echo "${A[*]:1:2}"
done < <(echo -e 'alpha,bravo,charlie\nfoo,bar,baz')

输出:
bravo:charlie
bar:baz

你的解决方案不会在环境中保留修改后的IFS吗?由于IFSIFS=,echo "${Foo [*]}"中没有扩展,echo难道不能看到修改后的值吗? - vintnes
@vintnes 是的,这会改变脚本的其余部分的 IFS。另一种选择是,例如,printf '%s\n' "$(IFS=,; echo "${A[*]:1:2}"),它仅在命令替换中更改。 - Benjamin W.
我现在明白,在echo读取其参数之前,所有这些扩展都会发生。 - vintnes
1
这并不是关于 shell 变量与环境变量的问题:而只是发生事情的顺序不同而已。 - glenn jackman

5
答案比其他答案所呈现的要简单一些:
$ foo=bar
$ foo=qux echo $foo
bar

我们看到 "bar" 是因为 shell 在设置 foo=qux 之前扩展了 $foo

简单命令扩展 -- 这里有很多内容需要理解,所以请耐心等待...

当执行一个简单的命令时,shell 从左到右执行以下扩展、分配和重定向:
1. 将被解析器标记为“变量分配”(即在命令名称之前)和重定向的单词“保存以备后处理”。
2. 非变量分配或重定向的单词被“扩展”(请参阅 Shell Expansions)。如果扩展后还有任何单词,则第一个单词被视为命令名称,剩余的单词是参数。
3. 执行如上所述的重定向(请参阅重定向)。
4. 在每个变量分配中,“=”后面的文本在分配给变量之前要进行图形展开、参数展开、命令替换、算术展开和引号删除。
如果没有命令名称生成,则变量分配影响当前 shell 环境。否则,这些变量将添加到执行命令的环境中,并且不会影响当前 shell 环境。如果任何赋值尝试向只读变量分配值,则会出现错误,并且命令退出具有非零状态。
如果没有命令名称生成,则执行重定向,但不会影响当前 shell 环境。重定向错误会导致命令退出具有非零状态。
如果在扩展后还有命令名称,则执行如下。否则,命令退出。如果扩展中包含命令替换之一,则该命令的退出状态为执行的最后一个命令替换的退出状态。如果没有命令替换,则该命令以零状态退出。

因此:

  • shell看到foo=qux并将其保存以备后用
  • shell看到$foo并将其扩展为"bar"
  • 然后我们现在有:foo=qux echo bar

一旦你真正理解了bash执行操作的顺序,很多神秘之处就会消失。


2
是的,我到了那里。foo=qux eval 'echo $foo'返回qux - vintnes

2
简短回答:更改IFS的影响是复杂的,难以理解,除了一些定义良好的习惯用法(例如IFS=,read ...),最好避免使用。

长回答:要理解更改IFS的结果,需要牢记以下几点:

  • IFS=something作为命令前缀使用仅更改该命令执行时的IFS值。特别地,它不会影响shell解析要传递给该命令的参数的方式;这由shell的IFS值控制,而不是用于该命令执行的值。

  • 某些命令会注意到它们执行时使用的IFS值(例如read),但其他命令则不会(例如echo)。

基于上述内容,IFS=, read -a A的作用是将其输入按“,”拆分:

$ IFS=, read -a A <<<"alpha,bravo,charlie"
$ declare -p A
declare -a A='([0]="alpha" [1]="bravo" [2]="charlie")'

但是echo并不关心这些; 它总是在传递参数时添加空格,因此在其前缀中使用IFS=something没有任何效果:

最初的回答:

$ echo alpha bravo
alpha bravo
$ IFS=, echo alpha bravo
alpha bravo

因此,当您使用IFS=,echo“${A [*]:1:2}”时,它相当于只是echo“${A [*]:1:2}”,并且由于shell对IFS的定义以空格开头,它将A的元素放在一起,并使用它们之间的空格。因此,它相当于运行IFS=,echo“alpha bravo”
另一方面,IFS=,echo“${A [*]:1:2}”更改了shell对IFS的定义,因此它会影响shell如何将元素组合在一起,因此它的输出等效于IFS=,echo“alpha,bravo” 。不幸的是,它还会影响从那时起的其他所有内容,因此您必须将其隔离到子shell中或在随后将其设置回正常状态。
只是为了完整起见,这里有一些其他无法工作的版本:
$ IFS=,; echo "${A[@]:1:2}"
bravo charlie

在这种情况下,[@]告诉shell将数组的每个元素视为单独的参数,因此它留给echo来合并它们,并且它忽略IFS并始终使用空格。"最初的回答"
$ IFS=,; echo "${A[@]:1:2}"
bravo charlie

那么这样怎么样:

那么这样怎么样:

$ IFS=,; echo ${A[*]:1:2}
bravo charlie

最初的回答:在这种情况下,[*]告诉shell使用IFS的第一个字符将所有元素合并在一起,得到bravo,charlie。但它没有放在双引号中,所以shell会立即按“,”重新分割它,再次将其拆分为单独的参数(然后echo像往常一样用空格连接它们)。
如果您想更改shell对IFS的定义而不必将其隔离到子shell中,则有几种选项可以更改它,并在之后重新设置它。在bash中,您可以像这样将其设置回正常值:
$ IFS=,
$ while read -a A; do    # Note: IFS change not needed here; it's already changed
> echo "${A[*]:1:2}"
> done <<<alpha,bravo,charlie
bravo,charlie
$ IFS=$' \t\n'

然而,$'...' 的语法并不适用于所有shell;如果需要可移植性,最好使用文本字符:

最初的回答

IFS=' 
'        # You can't see it, but there's a literal space and tab after the first '

有些人喜欢使用unset IFS,这只是强制 shell 还原其默认行为,与以正常方式定义 IFS 几乎相同。

但如果在某个更大的上下文中更改了 IFS,而您不想搞砸它,那么您需要保存它,然后将其设置回来。如果以正常方式更改了它,则可以使用以下方法:

最初的回答

saveIFS=$IFS
...
IFS=$saveIFS

...但如果有人认为使用unset IFS是个好主意,那么这将把它定义为空白,导致奇怪的结果。因此,您可以使用这种方法或unset方法,但不能同时使用两种方法。如果您想使其对抗unset冲突更加稳健,您可以在bash中使用类似以下内容的方法:

......但是,如果有人认为使用unset IFS是个好主意,那么这会将其定义为空白,导致出现奇怪的结果。因此,您可以使用这种方法或unset方法,但不能同时使用两种方法。如果您想让它能够抵御unset冲突,您可以在bash中使用以下代码:

saveIFS=${IFS:-$' \t\n'}

最初的回答:为了可移植性,请省略$' '并使用字面上的空格+制表符+换行符:
saveIFS=${IFS:- 
}                # Again, there's an invisible space and tab at the end of the first line

总的来说,这是一个充满陷阱的混乱场所。我建议尽可能避免它。"最初的回答"

readecho 没有什么特别的。它们只是内置命令而已。不同之处在于 echo 命令中有一个 $var,它会首先被扩展。 - glenn jackman

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接