Como escrever uma função “compor” de pipelines bash com pipes nomeados

3

Esta página detalha o psuedocode para criar uma função compose que leva n comandos e os executa em um pipeline:

We program a command compose such that

compose cmd1 cmd2 ... cmdn

behaves like the shell command:

cmd1 | cmd2 | ... | cmdn

Estou experimentando pipes nomeados e estou interessado em realmente escrever compose no Bash. Infelizmente eu não obtenho nenhuma saída quando o faço, presumivelmente devido a condições de corrida lendo e gravando nos diferentes canais. Eu fiz um número de iterações, mas continuo recebendo um comportamento confuso. Reduzi esse problema menor:

echo foo |   # stdin
{
  mkfifo p   # create pipe p
  cat > p &  # direct stdin to pipe p
  cat < p    # read pipe p to stdout
  rm p       # remove pipe p
}

Espero que isso produza foo , mas em vez disso não recebo nada. O que estou fazendo errado?

    
por dimo414 21.01.2016 / 02:57

2 respostas

1

O problema com o código de exemplo na pergunta é sutil; para gravar em um pipe nomeado, você precisa & background o comando, senão ele irá bloquear a espera para ser lido. No entanto, quando você faz isso, os " comandos iniciados em segundo plano com & têm sua entrada padrão redirecionada [para] /dev/null . ", significando que /dev/null é o que é canalizado para p , em vez de stdin.

No Bash, a solução alternativa é simples, redirecione o stdin para o processo em segundo plano com 0<&0 . Então o exemplo funcionará corretamente:

$ echo foo | { mkfifo p; cat 0<&0 > p & cat < p; rm p; }
foo

A função compose completa acaba assim:

compose() {
  dir=$(mktemp -d)                 # Create a temp dir to hold the pipes
  cd $dir                          #   to avoid filename conflicts
  i=0                              #
  mkfifo "pipe$i"                  # Create pipe0, the output for command $1
  ($1 0<&0 > "pipe$i" &)           # Start $1, reading stdin and writing to pipe0
  shift                            # Shift off $1 since it's running
    for c in "$@"; do              # Loop over the remaining commands
    ii=$((i+1))                    # 
    mkfifo "pipe$ii"               # Create a pipe i+1, the next command's output
    ($c < "pipe$i" > "pipe$ii" &)  # Start the next command, reading from the
    i=$ii                          #   i'th pipe and writing to the i+1'th
  done                             #
  cat "pipe$i"                     # Output the last pipe, executing the commands
  cd - > /dev/null                 # Change back to the old directory
  rm -rf $dir                      # Remove all the pipes
}
    
por 21.01.2016 / 02:57
1
compose()
    case    $#  in  
    [01])  "$@" ;;          ## if 1 or fewer args just run what we've got
       *)  "$1" | {         ## otherwise pipe output from $1 into a
    shift; compose "$@"     ## self-call until all args are gone
};  esac
    
por 21.01.2016 / 03:07