for NAME [in WORDS ... ] ; do COMMANDS; done
我想重新定义 for 循环的行为,即我希望现有 BASH 脚本的语法在更改语法含义时保持不变。例如,我想COMMANDS
成为coproc ( COMMANDS )
这样的人:
for i in $(seq 1 5);do sleep $[ 1 + $RANDOM % 5 ]; echo $i;done
成为
for i in $(seq 1 5);do coproc (sleep $[ 1 + $RANDOM % 5 ]; echo $i);done
答案1
TCL可以做到这一点之类的事情。bash
不是 TCL ;-)
另请参阅 Stéphan 的简洁zsh
解决方案(因为zsh
有并发 coprocs):
alias do='do coproc {' done='}; done'
不是那个它惯于工作bash
,但有两个问题:bash
只能正确支持单个 coproc(见下文);你呢应该当您有多个协程时,请命名您的协程,并且bash
当前不对名称应用扩展(即coproc $FOO
是一个错误),它只接受静态名称(解决方法是eval
)。
但是,基于此并假设您并不真正需要coproc
,您可以做的是:
shopt -s expand_aliases # enable aliases in a script
alias do='do (' done=')& done' # "do (" ... body... ")& done"
这会将每个do
主体“提升”到后台子 shell。如果您需要在循环底部进行同步,则 await
可以解决问题,或者对重要的脚本更具选择性:
shopt -s expand_aliases
declare -a _pids
alias do='do (' done=')& _pids+=($!); done'
for i in $(seq 1 5);do sleep $[ 1 + $RANDOM % 5 ]; echo $i;done
wait ${_pids[*]}
它会跟踪_pids
数组中的每个新子 shell。
(使用 do { ... } &
也可以,但会启动一个额外的 shell 进程。)
请注意可能以下内容都不应该出现在生产代码中!
您想要的部分内容可以完成(尽管不优雅,并且没有很强的鲁棒性),但主要问题是:
- 协进程不仅仅是为了并行化,它们是为与另一个进程同步交互而设计的(其中一个效果是 stdin 和 stdout 更改为管道,并且作为子 shell 可能存在其他差异,例如
$$
) - 更重要的是,仅限 bash(最高 4.4)支持单个协进程一次。
- 如果主体在后台运行,您可以打破循环控制并有效地放松
break
(但这不会影响这里的简单示例)exit
如果不对 bash 的内部结构进行重大修改(包括不完整的并发协进程实现),则无法让 bash 做到这一点。命令必须用( )
or显式分组{ }
,这隐含在语法级别结构中,例如for/do/done
.您可以预处理 bash 脚本,但这需要一个强大的解析器(当前解析器大约有 6500 行 C/bison 输入)。
(温柔的读者现在可能希望把目光移开。)
您可以do
使用别名和函数“重新定义”,这可能会遇到很多复杂情况(转义、引用、扩展等):
function _do()
{
( eval "$@" ) &
}
shopt -s expand_aliases # allow alias expansion in a script
alias do="do _do"
# single command only
for i in {1..5}; do sleep $((1+$RANDOM%5)); done
但为了传递多个命令,您必须转义和/或引用:
for i in {1..5}; do "sleep $((1+$RANDOM%5)); echo $i" ; done
在这种情况下,您也可以放弃alias
欺骗,直接调用_do
,或者只是修改循环。
(温柔的读者现在可能希望尖叫着逃离大楼。)
这是使用数组的稍微不易出错的版本,但这需要对代码进行进一步修改:
function _do()
{
local _cmd
printf -v _cmd "%s;" "$@"
( eval "$_cmd" ) &
}
cmd=( 'sleep $((1+$RANDOM%5))' )
cmd+=( 'echo $i' )
for i in {1..5}; do "${cmd[@]}" ; done
(我想现在这位温柔的读者已经昏倒了,或者已经安全,不会受到进一步的伤害。)
最后,只是因为它可以做到,而不是因为它是一个好主意:
function _for() {
local _cmd _line
printf -v _cmd '%s ' "$@"
printf -v _cmd "for %s\n" "$_cmd" # reconstruct "for ..."
read _line # "do"
_cmd+=$'do (\n'
while read _line; do
_cmd+="$_line"; _cmd+=$'\n' # reconstruct loop body
done
_cmd+=$') &\ndone' # finished
unalias for # see note below
eval "$_cmd"
alias for='_for <<-"done"'
}
shopt -s expand_aliases
alias for='_for <<-"done"'
for i in $(seq 1 5)
do
sleep $((1+$RANDOM%5))
echo $i
done
唯一需要的更改是do
/done
单独出现在自己的行中,并且done
必须使用 0 个或多个硬制表符缩进。
上面的作用是:
- 使用别名进行劫持
for
,该别名调用带有 HEREDOC 的函数,该函数将主体吞没到done
. - 该函数
for
根据其参数重建循环,然后从 HEREDOC 读取主体的其余部分 - 重建的主体有“( ... ) &”楔入其中,并
done
附加 - invoke
eval
,撤消for
别名(\
前缀适用于别名命令,但不适用于关键字,unalias
需要恢复)
同样,这也不是非常稳健:循环的重定向done > /dev/null
会导致问题,嵌套会导致问题,需要额外引用才能工作的for
算术也会导致问题。for (( ;; ))
您不能使用相同的技巧来 override do
,除了可能会破坏select/do/done
它,如果它有效的话会更简单,但是do
如果您尝试这样做,您会收到语法错误(带有重定向)。