如何在每个命令的基础上正确分配临时 Bash 变量?
How does one properly assign temporary Bash variables on a per-command basis?
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=""; 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
$ 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
.
所以回到你的主要问题,你基本上已经完成了使用 subshell 的最后一次尝试——除了你实际上不需要 subshell:
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
的效果复杂且难以理解,最好避免,除了一些定义明确的成语(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
没注意;它总是把 spaces 放在它传递的参数之间,所以使用 IFS=something
作为它的前缀根本没有效果:
$ echo alpha bravo
alpha bravo
$ IFS=, echo alpha bravo
alpha bravo
因此,当您使用 IFS=, echo "${A[*]:1:2}"
时,它等同于 echo "${A[*]:1:2}"
,并且由于 shell 对 IFS
的定义以 space 开头,它将 A
的元素与它们之间的 space 放在一起。所以它相当于 运行 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
并且始终使用 spaces.
$ IFS=,; echo "${A[@]:1:2}"
bravo charlie
那么这个怎么样:
$ IFS=,; echo ${A[*]:1:2}
bravo charlie
在这种情况下,[*]
告诉 shell 将所有元素与它们之间的 IFS
的第一个字符混合在一起,得到 bravo,charlie
。但它没有用双引号引起来,所以 shell 立即将其重新拆分为“,”,再次将其拆分回单独的参数(然后 echo
将它们与 space 连接为总是)。
如果您想更改 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'}
...或者为了便携性,不要使用 $' '
并使用文字 space+tab+newline:
saveIFS=${IFS:-
} # Again, there's an invisible space and tab at the end of the first line
总而言之,对于粗心的人来说,这是一个充满陷阱的混乱局面。我建议尽可能避免使用它。
答案比其他答案要简单一些:
$ foo=bar
$ foo=qux echo $foo
bar
我们看到“栏”是因为 shell 在 设置 foo=qux
之前扩展了 $foo
Simple Command Expansion -- 这里有很多事情要解决,所以请耐心等待...
When a simple command is executed, the shell performs the following expansions, assignments, and redirections, from left to right.
- The words that the parser has marked as variable assignments (those preceding the command name) and redirections are saved for later processing.
- The words that are not variable assignments or redirections are expanded (see Shell Expansions). If any words remain after expansion, the first word is taken to be the name of the command and the remaining words are the arguments.
- Redirections are performed as described above (see Redirections).
- The text after the ‘=’ in each variable assignment undergoes tilde expansion, parameter expansion, command substitution, arithmetic expansion, and quote removal before being assigned to the variable.
If no command name results, the variable assignments affect the current shell environment. Otherwise, the variables are added to the environment of the executed command and do not affect the current shell environment. If any of the assignments attempts to assign a value to a readonly variable, an error occurs, and the command exits with a non-zero status.
If no command name results, redirections are performed, but do not affect the current shell environment. A redirection error causes the command to exit with a non-zero status.
If there is a command name left after expansion, execution proceeds as described below. Otherwise, the command exits. If one of the expansions contained a command substitution, the exit status of the command is the exit status of the last command substitution performed. If there were no command substitutions, the command exits with a status of zero.
所以:
- shell 看到
foo=qux
并保存以备后用
- shell 看到
$foo
并将其扩展为“bar”
- 那么我们现在有:
foo=qux echo bar
一旦你真正理解了 bash 做事的顺序,很多谜团就会消失。
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=""; 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
$ 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
.
所以回到你的主要问题,你基本上已经完成了使用 subshell 的最后一次尝试——除了你实际上不需要 subshell:
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
的效果复杂且难以理解,最好避免,除了一些定义明确的成语(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
没注意;它总是把 spaces 放在它传递的参数之间,所以使用 IFS=something
作为它的前缀根本没有效果:
$ echo alpha bravo
alpha bravo
$ IFS=, echo alpha bravo
alpha bravo
因此,当您使用 IFS=, echo "${A[*]:1:2}"
时,它等同于 echo "${A[*]:1:2}"
,并且由于 shell 对 IFS
的定义以 space 开头,它将 A
的元素与它们之间的 space 放在一起。所以它相当于 运行 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
并且始终使用 spaces.
$ IFS=,; echo "${A[@]:1:2}"
bravo charlie
那么这个怎么样:
$ IFS=,; echo ${A[*]:1:2}
bravo charlie
在这种情况下,[*]
告诉 shell 将所有元素与它们之间的 IFS
的第一个字符混合在一起,得到 bravo,charlie
。但它没有用双引号引起来,所以 shell 立即将其重新拆分为“,”,再次将其拆分回单独的参数(然后 echo
将它们与 space 连接为总是)。
如果您想更改 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'}
...或者为了便携性,不要使用 $' '
并使用文字 space+tab+newline:
saveIFS=${IFS:-
} # Again, there's an invisible space and tab at the end of the first line
总而言之,对于粗心的人来说,这是一个充满陷阱的混乱局面。我建议尽可能避免使用它。
答案比其他答案要简单一些:
$ foo=bar
$ foo=qux echo $foo
bar
我们看到“栏”是因为 shell 在 设置 foo=qux
$foo
Simple Command Expansion -- 这里有很多事情要解决,所以请耐心等待...
When a simple command is executed, the shell performs the following expansions, assignments, and redirections, from left to right.
- The words that the parser has marked as variable assignments (those preceding the command name) and redirections are saved for later processing.
- The words that are not variable assignments or redirections are expanded (see Shell Expansions). If any words remain after expansion, the first word is taken to be the name of the command and the remaining words are the arguments.
- Redirections are performed as described above (see Redirections).
- The text after the ‘=’ in each variable assignment undergoes tilde expansion, parameter expansion, command substitution, arithmetic expansion, and quote removal before being assigned to the variable.
If no command name results, the variable assignments affect the current shell environment. Otherwise, the variables are added to the environment of the executed command and do not affect the current shell environment. If any of the assignments attempts to assign a value to a readonly variable, an error occurs, and the command exits with a non-zero status.
If no command name results, redirections are performed, but do not affect the current shell environment. A redirection error causes the command to exit with a non-zero status.
If there is a command name left after expansion, execution proceeds as described below. Otherwise, the command exits. If one of the expansions contained a command substitution, the exit status of the command is the exit status of the last command substitution performed. If there were no command substitutions, the command exits with a status of zero.
所以:
- shell 看到
foo=qux
并保存以备后用 - shell 看到
$foo
并将其扩展为“bar” - 那么我们现在有:
foo=qux echo bar
一旦你真正理解了 bash 做事的顺序,很多谜团就会消失。