Tengo un montón de scripts que hacen esto:
command | while read something; do
a
long
list
of
commands
done
¿Alguien ha pensado alguna vez en cómo ejecutar todos los comandos que se envían a través de la tubería parallel
? Hayad hocsoluciones a este problema pero estoy buscando algogeneralque requiere sólo cambios mínimos en mis scripts y, si es posible, también permite ejecutarse incluso cuando parallel
no está instalado.
command
Lo anterior puede ser prácticamente todo, sin limitarse a un solo comando. a long list of commands
también puede ser absolutamente diferente.
Por ejemplo, considere esta línea que cambia las fechas de modificación de los archivos extraídos en un repositorio git a la fecha en que se cambiaron por ú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
De forma predeterminada, es terriblemente lento porque git log
ambos touch
son comandos bastante lentos. Pero es sólo un ejemplo y, además, sencillo.
Respuesta1
Usaría una función bash y la llamaría:
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
Úselo parallel -0
si desea dividir en NUL.
Si desea ejecutar lo anterior sin instalar GNU Parallel, puede usar:
parallel --embed > myscript.sh
y luego agregue lo anterior a myscript.sh
.
Respuesta2
Es posible hacer que bash o ksh ejecuten una lista de comandos independientes simultáneamente, de modo que cada secuencia comience un nuevo comando tan pronto como finalice su tarea anterior. Las transmisiones se mantienen ocupadas excepto los comandos finales.
El método básico es iniciar una serie de shells asincrónicos que leen desde el mismo canal: el canal garantiza el almacenamiento en búfer de línea y lecturas atómicas (se puede usar un archivo de comandos con redirección, cat file |
pero no mediante redirección).
Los comandos pueden ser cualquier shell de una sola línea (en la sintaxis correcta para el shell propietario de la secuencia), pero no pueden depender de los resultados de comandos anteriores, ya que la asignación de comandos a las secuencias es arbitraria. Los comandos complejos se configuran mejor como scripts externos para que puedan invocarse como un comando simple con argumentos.
Esta es una prueba de seis trabajos en tres corrientes, que ilustra la superposición de trabajos. (También probé 240 trabajos en 80 secuencias en mi computadora portátil).
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 es el script proxy que proporciona la depuración para esos trabajos.
#! /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 es el script de gestión de transmisiones. Crea los comandos de trabajo para ejecutar los servidores proxy e inicia los shells en 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
Respuesta3
En lugar de ejecutar git ls-tree
y luego git log
, date
y touch
varias veces en un ciclo bash mientras se lee, el siguiente script en Perl toma la salida git log --name-only HEAD
y almacena la marca de tiempo más reciente para cualquier archivo mencionado en un registro de confirmación en un hash llamado %files
. Ignora los nombres de archivos que no existen.
Luego construye un Hash of Arrays ("HoA" - ver man perldsc
) llamado %times
, con las marcas de tiempo como clave hash y los valores son una matriz anónima que contiene los nombres de archivos con esa marca de tiempo. Esta es una optimización para que la función táctil solo deba ejecutarse una vez para cada marca de tiempo en lugar de una vez para cada nombre de archivo.
Se ignoran el ID de confirmación, el mensaje de confirmación, el nombre del autor y las líneas en blanco de git log
la salida.
El script utiliza la unqqbackslash()
función deCadena::Escaparen cada nombre de archivo para manejar correctamente la forma en que git log
se imprimen los nombres de archivo con tabulaciones incrustadas, líneas nuevas, comillas dobles, etc. (es decir, como cadenas entre comillas dobles con códigos/caracteres de escape con barra invertida).
Espero que se ejecute docenas de veces más rápido, al menos, que su ciclo 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} });
};
El guión utiliza elFecha::Analizar, Archivo::Tocar, yCadena::Escaparmódulos perl.
En Debian, apt install libtimedate-perl libfile-touch-perl libstring-escape-perl
. Probablemente otras distribuciones también los tengan empaquetados. De lo contrario, instálelos con cpan
.
Ejemplo de uso, en un repositorio de git con un par de archivos basura ( file
y 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
(muy ligeramente falsificado: edité los mensajes de confirmación para que quede claro cuándo se confirmaron ambos archivos por primera vez, luego se cambió el archivo 2 y se confirmó nuevamente. Aparte de eso, se copió y pegó directamente desde mi terminal).
Este es mi segundo intento: originalmente intenté usar elGit::Crudomódulo pero no pude encontrar la manera de conseguir que me diera una lista desololos nombres de archivos modificados en una confirmación particular. Estoy seguro de que hay una manera, pero ya me di por vencido. Simplemente no conozco lo git
suficientemente bien los aspectos internos.