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

时间:2019-04-29 23:35:50

标签: bash scope ifs

对于临时的,按命令分配的变量,尤其是使用IFS时,Bash似乎表现出不可预测的行为。

我经常将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

3 个答案:

答案 0 :(得分: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

所以回到您的主要问题,基本上您已经在最后尝试使用subshel​​l了,只是您实际上并不需要subshel​​l:

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

答案 1 :(得分:2)

答案比其他答案要简单一些。

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

我们看到“条形”,因为外壳在设置$foo之前扩展了foo=qux

Simple Command Expansion-这里有很多事情要做,所以请忍受...

  

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

     
      
  1. 解析器标记为变量赋值(在命令名称之前的变量)和重定向的单词将保存以供以后处理
  2.   
  3. 不是变量分配或重定向的单词将被扩展(请参见Shell Expansions)。如果扩展后还剩下任何单词,则将第一个单词作为命令的名称,其余的单词作为参数。
  4.   
  5. 重定向如上所述进行(请参阅重定向)。
  6.   
  7. 在分配给变量之前,每个变量分配中“ =”之后的文本都会进行波浪线扩展,参数扩展,命令替换,算术扩展和引号删除。
  8.   
     

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

     

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

     

如果扩展后还剩下命令名称,执行将继续,如下所述。否则,命令退出。如果其中一个扩展包含命令替换,则命令的退出状态为上次执行的命令替换的退出状态。如果没有命令替换,则命令以零状态退出。

所以:

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

一旦您真正了解了bash做事的顺序,那么许多谜团就会消失。

答案 2 :(得分:1)

简短的回答:更改IFS的影响是复杂且难以理解的,最好避免使用,除非有一些定义明确的习惯用法(IFS=, read ...是我认为可以的习惯用法之一)。 / p>

长答案:为了了解从IFS的更改中看到的结果,您需要记住几件事:

  • 使用IFS=something作为命令的前缀会更改IFS 仅针对该命令的执行。特别是,它不会影响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}",并且由于外壳程序IFS的定义以空格开头,因此将A的元素与它们之间的空间。因此,这等效于运行IFS=, echo "alpha bravo"

另一方面,IFS=,; echo "${A[*]:1:2}"更改了外壳程序IFS的定义,因此它确实影响了外壳程序将元素放在一起的方式,因此等效于IFS=, echo "alpha,bravo"。不幸的是,从那时起,它还会影响其他所有内容,因此您要么必须将其隔离到子外壳中,要么之后再将其设置为正常。

仅出于完整性考虑,以下是其他两个无效的版本:

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

在这种情况下,[@]告诉外壳程序将数组的每个元素视为一个单独的参数,因此将其留给echo进行合并,而忽略IFS和总是使用空格。

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

那呢:

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

在这种情况下,[*]告诉外壳程序将所有元素与它们之间的IFS的第一个字符混在一起,得到bravo,charlie。但是它不是用双引号引起来的,因此shell会立即在“,”上将其重新拆分,再次将其重新拆分为单独的参数(然后echo总是将它们与空格连接在一起)。

如果您想更改IFS的shell定义而不必将其隔离到子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,它只是强制外壳恢复其默认行为,这与以常规方式定义的IFS差不多。

...但是,如果在较大的上下文中更改了IFS,并且您不想弄乱它,则需要先保存然后重新设置。如果已正常更改,它将起作用:

saveIFS=$IFS
...
IFS=$saveIFS

...但是,如果有人认为使用unset IFS是个好主意,则会将其定义为空白,从而产生奇怪的结果。因此,您可以使用此方法或unset方法,但不能同时使用两者。如果您想针对unset冲突使它变得更强大,则可以在bash中使用类似的方法:

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

...或者出于便携性考虑,请省略$' '并使用文字空格+ tab +换行符:

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

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