Uma conversão/substituição universal `enquanto lê algo` para `paralela`

Uma conversão/substituição universal `enquanto lê algo` para `paralela`

Eu tenho vários scripts que fazem isso:

command | while read something; do
    a
    long
    list
    of
    commands
done

Alguém já pensou em como executar todos os comandos alimentados pelo pipe usando parallel? HáAd hocsoluções para esse problema, mas estou procurando algoem geralque requer apenas alterações mínimas em meus scripts e, se possível, também permite ser executado mesmo quando parallelnão está instalado.

commandacima pode ser praticamente tudo, não limitado a um único comando. a long list of commandstambém pode ser absolutamente diferente.

Por exemplo, considere este liner que altera as datas de modificação dos arquivos com check-out em um repositório git para a data em que foram alterados pela última vez:

git ls-tree -r --name-only HEAD | 
while read filename; do
   unixtime=$(git log -1 --format="%at" -- "${filename}");
   touchtime=$(date -d @$unixtime +'%Y%m%d%H%M.%S');
   touch -t ${touchtime} "${filename}";
done

Por padrão, é terrivelmente lento porque git logambos touchsão comandos bastante lentos. Mas é apenas um exemplo e simples.

Responder1

Eu usaria uma função bash e chamaria isso:

myfunc() {
   filename="$1"
   unixtime=$(git log -1 --format="%at" -- "${filename}");
   touchtime=$(date -d @$unixtime +'%Y%m%d%H%M.%S');
   touch -t ${touchtime} "${filename}";
}
export -f myfunc

git ls-tree -r --name-only HEAD | parallel myfunc

Use parallel -0se quiser dividir em NUL.

Se você deseja executar o acima sem instalar o GNU Parallel, você pode usar:

parallel --embed > myscript.sh

e, em seguida, anexe o acima a myscript.sh.

Responder2

É possível fazer com que o bash ou o ksh executem uma lista de comandos independentes simultaneamente, de modo que cada fluxo inicie um novo comando assim que sua tarefa anterior terminar. Os fluxos são mantidos ocupados, exceto para os comandos finais.

O método básico é lançar uma série de shells assíncronos que leem do mesmo canal: o canal garante buffer de linha e leituras atômicas (um arquivo de comandos pode ser usado com, cat file |mas não por redirecionamento).

Os comandos podem ser qualquer shell de uma linha (na sintaxe correta para o shell que possui o fluxo), mas não podem confiar nos resultados de comandos anteriores, pois a alocação de comandos aos fluxos é arbitrária. Comandos complexos são melhor configurados como scripts externos para que possam ser invocados como um comando simples com argumentos.

Este é um teste de seis trabalhos em três fluxos, ilustrando a sobreposição de trabalhos. (Também testei 240 trabalhos em 80 fluxos no meu laptop.)

Time now 23:53:47.328735254
Sleep until 00 seconds to make debug easier.
Starting 3 Streams
23:54:00.040   Shell   1 Job   1 Go    sleep 5
23:54:00.237   Shell   2 Job   2 Go    sleep 13
23:54:00.440   Shell   3 Job   3 Go    sleep 14
Started all Streams
23:54:05.048   Shell   1 Job   1   End sleep 5
23:54:05.059   Shell   1 Job   4 Go    sleep 3
23:54:08.069   Shell   1 Job   4   End sleep 3
23:54:08.080   Shell   1 Job   5 Go    sleep 13
23:54:13.245   Shell   2 Job   2   End sleep 13
23:54:13.255   Shell   2 Job   6 Go    sleep 3
23:54:14.449   Shell   3 Job   3   End sleep 14
23:54:16.264   Shell   2 Job   6   End sleep 3
23:54:21.089   Shell   1 Job   5   End sleep 13
All Streams Ended

Este é o script proxy que fornece a depuração para esses trabalhos.

#! /bin/bash

#.. jobProxy.
#.. arg 1: Job number.
#.. arg 2: Sleep time.
#.. idStream: Exported into the Stream's shell.

    fmt='%.12s   Shell %3d Job %3d %s sleep %s\n'
    printf "${fmt}" $( date '+%T.%N' ) "${idStream}" "${1}" "Go   " "${2}"
    sleep "${2}"
    printf "${fmt}" $( date '+%T.%N' ) "${idStream}" "${1}" "  End" "${2}"

Este é o script de gerenciamento de fluxo. Ele cria os comandos de trabalho para executar os proxies e inicia os shells em segundo plano.

#! /bin/bash

makeJobs () {

    typeset nJobs="${1}"

    typeset Awk='
BEGIN { srand( Seed % 10000000); fmt = "./jobProxy %s %3d\n"; }
{ printf (fmt, $1, 2 + int (14 * rand())); }
'
    seq 1 "${nJobs}" | awk -v Seed=$( date "+%N$$" ) "${Awk}"
}

runStreams () {

    typeset n nStreams="${1}"

    echo "Starting ${nStreams} Streams"
    for (( n = 1; n <= nStreams; ++n )); do
        idStream="${n}" bash -s &
        sleep 0.20
    done
    echo "Started all Streams"

    wait
    echo "All Streams Ended"
}

## Script Body Starts Here.

    date '+Time now %T.%N'
    echo 'Sleep until 00 seconds to make debug easier.'
    sleep $( date '+%S.%N' | awk '{ print 60 - $1; }' )

    makeJobs 6 | runStreams 3

Responder3

Em vez de executar git ls-treeand then git log, datee touchvárias vezes em um bash while read loop, o script perl a seguir obtém a saída git log --name-only HEADe armazena o carimbo de data/hora mais recente de qualquer arquivo mencionado em um log de commit em um hash chamado %files. Ignora nomes de arquivos que não existem.

Em seguida, ele cria um Hash de Arrays ("HoA" - veja man perldsc) chamado %times, com os carimbos de data e hora como a chave de hash e os valores sendo uma matriz anônima contendo os nomes de arquivos com esse carimbo de data e hora. Esta é uma otimização para que a função touch precise ser executada apenas uma vez para cada carimbo de data/hora, em vez de uma vez para cada nome de arquivo.

O ID do commit, a mensagem do commit, o nome do autor e as linhas em branco da git logsaída de são ignorados.

O script usa a unqqbackslash()função deString::Escapeem cada nome de arquivo para lidar corretamente com a maneira como git logimprime nomes de arquivos com tabulações incorporadas, novas linhas, aspas duplas, etc. (ou seja, como strings entre aspas duplas com códigos/caracteres de escape de barra invertida).

Espero que ele seja executado dezenas de vezes mais rápido, pelo menos, que o seu loop bash.

#!/usr/bin/perl

use strict;
use Date::Parse;
use File::Touch;
use String::Escape qw(unqqbackslash);

my %files = ();
my %times = ();
my $t;

while (<>) {
  chomp;
  next if (m/^$|^\s+|^Author: |^commit /);

  if (s/^Date:\s+//) {
    $t = str2time($_);

  } else {
    my $f = unqqbackslash($_);
    next unless -e $f;   # don't create file if it doesn't exist

    if (!defined($files{$f}) || $files{$f} < $t) {
      $files{$f} = $t;
    }

  };
};

# build %files HoA with timestamps containing the
# files modified at that time.
foreach my $f (sort keys %files) {
  push @{ $times{$files{$f}} }, $f;
}

# now touch the files
foreach my $t (keys %times) {
  my $tch = File::Touch->new(mtime_only => 1, time => $t);
  $tch->touch(@{ $times{$t} });
};

O script usa oData::Analisar, Arquivo::Toque, eString::Escapemódulos perl.

No Debian, apt install libtimedate-perl libfile-touch-perl libstring-escape-perl. Outras distros provavelmente também os têm empacotados. Caso contrário, instale-os com cpan.

Exemplo de uso, em um repositório git com alguns arquivos inúteis ( file, e file2):

$ git log --date=format:'%Y-%m-%d %H:%M:%S' --pretty='%H  %ad %s' file*
d10c313abb71876cfa8ad420b10f166543ba1402  2021-06-16 14:49:24 updated file2
61799d2c956db37bf56b228da28038841c5cd07d  2021-06-16 13:38:58 added file1
                                                              & file2

$ touch file*
$ ls -l file*
-rw-r--r-- 1 cas cas  5 Jun 16 19:23 file1
-rw-r--r-- 1 cas cas 29 Jun 16 19:23 file2

$ git  log  --name-only HEAD file*  | ./process-git-log.pl 
$ ls -l file*
-rw-r--r-- 1 cas cas  5 Jun 16 13:38 file1
-rw-r--r-- 1 cas cas 29 Jun 16 14:49 file2

(muito ligeiramente falsificado - editei as mensagens de commit para deixar claro quando os dois arquivos foram confirmados pela primeira vez, depois o arquivo2 foi alterado e confirmado novamente. Fora isso, ele é copiado e colado diretamente do meu terminal).


Esta é minha segunda tentativa: originalmente tentei usar oGit::Crumódulo, mas não consegui descobrir como fazer com que ele me desse uma lista deapenasos nomes de arquivos modificados em um commit específico. Tenho certeza de que há uma maneira, mas desisti disso. Eu simplesmente não conheço gitbem os detalhes internos.

informação relacionada