【问题标题】:How does one properly assign temporary Bash variables on a per-command basis?如何在每个命令的基础上正确分配临时 Bash 变量?
【发布时间】:2019-04-29 23:35:50
【问题描述】:

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

我经常将IFSread 命令一起分配给一个临时值。我想使用相同的机制来定制输出,但目前使用函数或子外壳来包含变量赋值。

$ 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

如果后面block中的第二个赋值不影响命令的环境,也不产生错误,那它是干什么用的呢?

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

【问题讨论】:

    标签: bash scope ifs


    【解决方案1】:
    $ 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 是的,这会更改脚本其余部分的 IFS。例如,另一种选择是 printf '%s\n' "$(IFS=,; echo "${A[*]:1:2}"),它只在命令替换中更改它。
    • 我现在明白所有这些扩展都发生在 echo 读取其参数之前。
    • 这并不是关于 shell var 与 env var:这只是事情发生的顺序。
    【解决方案2】:

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

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

    我们看到“bar”是因为外壳扩展了$foo 之前设置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 做事的顺序,很多谜团就烟消云散了。

    【讨论】:

    • 是的,我到了那里。 foo=qux eval 'echo $foo' 返回qux
    【解决方案3】:

    简答:更改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 一如既往地用空格将它们连接起来)。

    如果您想更改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,它只是强制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
    

    总而言之,对于粗心的人来说,这是一大堆充满陷阱的烂摊子。我建议尽可能避免它。

    【讨论】:

    • readecho 没有什么特别之处。这些只是内置命令。不同之处在于 echo 命令中有一个 $var,它首先被展开。
    猜你喜欢
    • 2021-06-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-03-06
    • 1970-01-01
    • 1970-01-01
    • 2012-07-16
    • 1970-01-01
    相关资源
    最近更新 更多