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はできるそのようなものbashTCLではありません;-)

同時のcoprocのため、Stéphanのきれいなzsh解決策も参照してください。zsh

alias do='do coproc {' done='}; done'

それではなくに慣れる動作しますbashが、2つの問題があります。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各プリンシパルを背景サブシェルに「上昇」させる。ループの一番下で同期が必要な場合、a はwaitトリックを実行するか、重要なスクリプトに対してより選択的に処理します。

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配列の新しい各サブシェルを追跡します。

(を使用して do { ... } &も機能しますが、追加のシェルプロセスが開始されます。)


参考にしてくださいおそらく、次のいずれもプロダクションコードには表示されません。

必要なものの一部を実行できますが(エレガントではなく強力ではありませんが)、主な問題は次のとおりです。

  • 共同プロセス並列化だけでなく、他のプロセスとの同期相互作用のために設計されています。 (1つの効果は、stdinとstdoutがパイプに変わり、サブシェルとして異なる違いがある可能性があることです。$$
  • さらに、bashのみ(最大4.4)単一の共同プロセスサポート一度。
  • 体がバックグラウンドで実行されている場合は、ループ制御を中断して効果的に休憩を取ることができますbreak(しかし、これはここの簡単な例には影響しません)。exit

同時コプロセスの不完全な実装を含むbashの内部を大幅に変更せずにbashにこれを実行させる方法はありません。コマンドは、構文レベルの構造に暗黙的( )または明示的にグループ化する必要があります。 bashスクリプトを前処理できますが、これには強力なパーサーが必要です(現在のパーサーには約6500行のC / bison入力があります)。{ }for/do/done

(純粋な読者は今目を向けたいと思うかもしれません。)

エイリアスと関数を使用して「オーバーライド」できます。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ゼロ以上のハードタブにインデントする必要があることです。

上記は次のとおりです。

  • ハイジャッキングはfor本文をdone
  • 関数はfor引数に基づいてループを書き換えてから、HEREDOCから本文の残りの部分を読み込みます。
  • 再構成されたボディには「(…)&」が挿入されて追加されdoneました。
  • 呼び出し、エイリアスをevalキャンセルするには(プレフィックスはエイリアスに適用されます。)for\注文する、キーワードの場合はunalias復元が必要です)

繰り返しますが、これはあまり強力ではありません。ループのリダイレクトによってdone > /dev/null問題が発生する可能性があり、ネストによって問題が発生する可能性があり、追加の参照が必要な操作によってfor問題が発生する可能性があります。for (( ;; ))上書きするために同じトリックを使用することはできませんdo。ただし、うまくいくselect/do/doneと簡単になりますが、doそうしようとすると構文エラー(リダイレクトを含む)が発生します。

おすすめ記事