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 parallel
não está instalado.
command
acima pode ser praticamente tudo, não limitado a um único comando. a long list of commands
també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 log
ambos touch
sã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 -0
se 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-tree
and then git log
, date
e touch
várias vezes em um bash while read loop, o script perl a seguir obtém a saída git log --name-only HEAD
e 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 log
saí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 log
imprime 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 git
bem os detalhes internos.