Ich habe jede Menge Skripte, die Folgendes tun:
command | while read something; do
a
long
list
of
commands
done
Hat jemand schon einmal darüber nachgedacht, wie man alle durch die Pipe geleiteten Befehle mit ausführen kann parallel
? Es gibtad hocLösungen für dieses Problem, aber ich suche etwasallgemeinDies erfordert nur minimale Änderungen an meinen Skripten und ermöglicht, wenn möglich, auch die Ausführung, wenn parallel
es nicht installiert ist.
command
oben kann so ziemlich alles sein, nicht auf einen einzigen Befehl beschränkt. a long list of commands
kann auch völlig anders sein.
Betrachten Sie beispielsweise diesen Einzeiler, der die Änderungsdaten ausgecheckter Dateien in einem Git-Repository auf das Datum ihrer letzten Änderung ändert:
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
Standardmäßig ist es furchtbar langsam, da git log
und touch
beides ziemlich langsame Befehle sind. Aber es ist nur ein Beispiel und noch dazu ein einfaches.
Antwort1
Ich würde eine Bash-Funktion verwenden und diese aufrufen:
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
Verwenden Sie es parallel -0
, wenn Sie bei NUL teilen möchten.
Wenn Sie das oben genannte ausführen möchten, ohne GNU Parallel zu installieren, können Sie Folgendes verwenden:
parallel --embed > myscript.sh
und hängen Sie dann das Obige an an myscript.sh
.
Antwort2
Es ist möglich, entweder bash oder ksh dazu zu bringen, eine Liste unabhängiger Befehle gleichzeitig auszuführen, sodass jeder Stream einen neuen Befehl startet, sobald die vorherige Aufgabe beendet ist. Die Streams sind mit Ausnahme der Befehle am Ende beschäftigt.
Die grundlegende Methode besteht darin, eine Reihe asynchroner Shells zu starten, die alle aus derselben Pipe lesen: Die Pipe garantiert Zeilenpufferung und atomare Lesevorgänge (eine Datei mit Befehlen kann mit, cat file |
aber nicht durch Umleitung verwendet werden).
Befehle können beliebige Shell-Einzeiler sein (in der richtigen Syntax für die Shell, die den Stream besitzt), können sich aber nicht auf die Ergebnisse vorheriger Befehle verlassen, da die Zuweisung von Befehlen zu Streams willkürlich ist. Komplexe Befehle werden am besten als externe Skripte eingerichtet, sodass sie als einfacher Befehl mit Argumenten aufgerufen werden können.
Dies ist ein Testlauf mit sechs Jobs in drei Streams, der die Überlappung der Jobs veranschaulicht. (Ich habe auch auf meinem Laptop einen Stresstest mit 240 Jobs in 80 Streams durchgeführt.)
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
Dies ist das Proxy-Skript, das den Debug für diese Jobs bereitstellt.
#! /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}"
Dies ist das Stream-Management-Skript. Es erstellt die Jobbefehle zum Ausführen der Proxys und initiiert die Hintergrund-Shells.
#! /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
Antwort3
Anstatt git ls-tree
und dann git log
, date
, und touch
mehrmals in einer Bash-While-Read-Schleife auszuführen, nimmt das folgende Perl-Skript die Ausgabe von git log --name-only HEAD
und speichert den aktuellsten Zeitstempel für alle in einem Commit-Protokoll erwähnten Dateien in einem Hash namens %files
. Es ignoriert Dateinamen, die nicht existieren.
Anschließend wird ein Hash von Arrays („HoA“ – siehe man perldsc
) mit dem Namen erstellt %times
, wobei die Zeitstempel als Hash-Schlüssel dienen und die Werte ein anonymes Array sind, das die Dateinamen mit diesem Zeitstempel enthält. Dies ist eine Optimierung, sodass die Touch-Funktion nur einmal für jeden Zeitstempel und nicht einmal für jeden Dateinamen ausgeführt werden muss.
Die Commit-ID, die Commit-Nachricht, der Autorenname und Leerzeilen aus git log
der Ausgabe werden ignoriert.
Das Skript verwendet die unqqbackslash()
Funktion vonZeichenfolge::Escapebei jedem Dateinamen, um die Art und Weise, wie git log
Dateinamen mit eingebetteten Tabulatoren, Zeilenumbrüchen, Anführungszeichen usw. gedruckt werden (d. h. als Zeichenfolgen in doppelten Anführungszeichen mit durch Backslash geschützten Codes/Zeichen), richtig zu handhaben.
Ich gehe davon aus, dass es mindestens Dutzende Male schneller laufen sollte als Ihre Bash-Schleife.
#!/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} });
};
Das Skript verwendet dieDatum::Parse, Datei::Touch, UndZeichenfolge::EscapePerl-Module.
Unter Debian apt install libtimedate-perl libfile-touch-perl libstring-escape-perl
. Andere Distributionen haben sie wahrscheinlich auch im Paket. Andernfalls installieren Sie sie mit cpan
.
Beispielverwendung in einem Git-Repository mit einigen Junk-Dateien ( file
, und 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
(leicht gefälscht – ich habe die Commit-Nachrichten bearbeitet, um deutlich zu machen, wann beide Dateien zuerst committet wurden und dann Datei 2 geändert und erneut committet wurde. Ansonsten wurde es direkt von meinem Terminal kopiert und eingefügt).
Dies ist mein zweiter Versuch: Ich habe ursprünglich versucht, dieGit::RawModul, aber ich konnte nicht herausfinden, wie ich es bekomme, mir eine Liste zu geben vonnurdie Dateinamen, die in einem bestimmten Commit geändert wurden. Ich bin sicher, dass es einen Weg gibt, aber ich habe es aufgegeben. Ich kenne die internen Vorgänge einfach nicht git
gut genug.