Como redefinir o comportamento da expressão de laço interno bash?

0
for NAME [in WORDS ... ] ; do COMMANDS; done

Gostaria de redefinir o comportamento de um loop for, ou seja, quero que a sintaxe dos meus scripts BASH permaneça a mesma enquanto altero o significado da gramática. Por exemplo, eu gostaria que COMMANDS se tornasse coproc ( COMMANDS ) de tal forma que:

for i in $(seq 1 5);do sleep $[ 1 + $RANDOM % 5 ]; echo $i;done

TRANSFORMA-SE

for i in $(seq 1 5);do coproc (sleep $[ 1 + $RANDOM % 5 ]; echo $i);done
    
por Derek Callaway 21.01.2018 / 16:44

1 resposta

1

O TCL pode fazer esse tipo de coisa . bash não é TCL; -)

Veja a solução zsh pura de Stéphan também (já que zsh tem coprocs simultâneos):

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

Não é que não funcione em bash , mas há dois problemas: bash apenas suporta adequadamente um único coproc (veja abaixo); e você deve nomear seus coprocs quando tiver mais de um, e bash atualmente não aplica expansão ao nome (ou seja, coproc $FOO é um erro), ele aceita apenas um nome estático (solução alternativa é eval ).

Mas, com base nisso e presumindo que você não precisa exatamente de coproc , o que você pode fazer é:

shopt -s expand_aliases         # enable aliases in a script
alias do='do (' done=')& done'  # "do ("  ... body... ")& done"

e isso "promoverá" cada do body para uma sub-base em segundo plano. Se você precisar sincronizar na parte inferior do loop, um wait fará o truque ou será mais seletivo para scripts não triviais:

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[*]}

Isso acompanha cada nova subshell na matriz _pids .

(O uso de do { ... } & também funcionaria, mas inicia um processo extra de shell.)

Por favor note provavelmente nenhum dos seguintes deve aparecer no código de produção!

Parte do que você quer pode ser feito (embora de forma deselegante e sem grande robustez), mas os principais problemas são:

  • Os coprocessos não são apenas para paralelização, eles são projetados para interação síncrona com outro processo (um efeito sendo stdin e stdout são alterados para pipes, e como um subshell, pode haver outras diferenças, por exemplo, $$ )
  • mais importante, bash (até 4.4) apenas suporta um único coprocess de cada vez .
  • você pode quebrar o controle de loop e efetivamente perder break e exit se o corpo for executado em segundo plano (mas isso não afeta os exemplos simples aqui)

Você não pode obter o bash para fazer isso sem modificações não triviais em seus componentes internos (incluindo a implementação incompleta de co-processos simultâneos). Os comandos devem ser agrupados explicitamente com ( ) ou { } , isso está implícito em construções de nível gramatical como for/do/done . Você poderia pré-processar seus scripts bash, mas isso exigiria um analisador (o atual é ~ 6500 linhas de entrada C / bison) para ser robusto.

(O gentil leitor pode querer desviar o olhar agora.)

Você pode "redefinir" do com um alias e uma função, potencialmente sujeitos a muitas complicações (escape, citações, expansão, etc.):

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

Mas, para passar vários comandos, você deve escapar e / ou citar:

for i in {1..5}; do "sleep $((1+$RANDOM%5)); echo $i" ; done

Nesse caso, você também pode dispensar o truque alias e chamar o _do diretamente ou apenas alterar os loops de qualquer maneira.

(O gentil leitor pode agora querer sair correndo do prédio.)

Aqui está uma versão ligeiramente menos propensa a erros usando uma matriz, mas isso requer modificações adicionais em seu código:

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

(Eu assumo agora que o gentil leitor desmaiou ou está protegido contra outros danos.)

Por fim, só porque isso pode ser feito, e não porque seja uma boa ideia :

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

A única alteração necessária é que do / done apareçam sozinhos em suas próprias linhas, e done deve ser indentado com 0 ou mais divisórias sólidas.

O que isso faz é:

  • hijack for com um alias que chama uma função com um HEREDOC que engole o corpo até done .
  • essa função reconstrói o loop for de seus argumentos e depois lê o resto do corpo a partir do HEREDOC
  • o corpo reconstruído tem "(...) &" inserido nele e done anexado
  • invoque eval , tendo desfeito o for alias (um prefixo \ funciona para comandos com alias , mas não para palavras-chave, é necessário unalias e restauração)

Mais uma vez, isso não é muito robusto: um redirecionamento do loop como done > /dev/null causará problemas, assim como aninhado for , assim como a aritmética for (( ;; )) que precisa de cotação extra para funcionar. Você não pode usar o mesmo truque para sobrescrever do , além de provavelmente quebrar select/do/done , seria mais simples se funcionasse, mas você obteria um erro de sintaxe ( do com um redirecionamento) se tentasse isso.

    
por 06.02.2018 / 20:35

Tags