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

问题描述 投票:3回答:3

对于临时的每命令变量赋值,Bash似乎表现得不可预测,特别是与IFS

我经常将IFSread命令一起分配给临时值。我想使用相同的机制来定制输出,但目前使用函数或子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
bash scope ifs
3个回答
3
投票
$ 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

2
投票

答案比其他答案更简单:

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

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

Simple Command Expansion - 这里有很多东西可以通过,所以忍受我...

执行简单命令时,shell将从左到右执行以下扩展,分配和重定向。

  1. 解析器标记为变量赋值(命令名称前面的那些)和重定向的单词将保存以供以后处理。
  2. 不是变量赋值或重定向的单词被扩展(参见Shell Expansions)。如果在扩展后仍有任何单词,则第一个单词将被视为命令的名称,其余单词为参数。
  3. 如上所述执行重定向(请参阅重定向)。
  4. 每个变量赋值中'='之后的文本在分配给变量之前经历波浪扩展,参数扩展,命令替换,算术扩展和引用删除。

如果未生成命令名称,则变量赋值会影响当前的shell环境。否则,变量将添加到已执行命令的环境中,并且不会影响当前的shell环境。如果任何分配尝试将值分配给只读变量,则会发生错误,并且命令以非零状态退出。

如果没有命令名称结果,则执行重定向,但不影响当前的shell环境。重定向错误导致命令以非零状态退出。

如果在扩展后留下命令名称,则执行如下所述。否则,该命令退出。如果其中一个扩展包含命令替换,则该命令的退出状态是执行的最后一个命令替换的退出状态。如果没有命令替换,则命令以状态为零退出。

所以:

  • shell看到foo=qux并保存以供日后使用
  • shell看到$foo并将其扩展为“bar”
  • 那么我们现在有:foo=qux echo bar

一旦你真正理解了bash做事的顺序,很多谜团就会消失。


1
投票

简短的回答:改变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中使用这样的东西:

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

...或者为了便携性,请不要使用$' '并使用文字空格+制表符+换行符:

saveIFS=${IFS:- 
}                # Again, there's an invisible space and tab at the end of the first line

总而言之,对于那些粗心大意的人来说,这里充满了陷阱。我建议尽可能避免使用它。

© www.soinside.com 2019 - 2024. All rights reserved.