如何重新定义 bash 内置循环表达式行为?

如何重新定义 bash 内置循环表达式行为?
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如果您尝试这样做,您会收到语法错误(带有重定向)。

相关内容