Eine universelle Konvertierung/Ersetzung von `while read something` nach `parallel`

Eine universelle Konvertierung/Ersetzung von `while read something` nach `parallel`

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 paralleles nicht installiert ist.

commandoben kann so ziemlich alles sein, nicht auf einen einzigen Befehl beschränkt. a long list of commandskann 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 logund touchbeides 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-treeund dann git log, date, und touchmehrmals in einer Bash-While-Read-Schleife auszuführen, nimmt das folgende Perl-Skript die Ausgabe von git log --name-only HEADund 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 logder Ausgabe werden ignoriert.

Das Skript verwendet die unqqbackslash()Funktion vonZeichenfolge::Escapebei jedem Dateinamen, um die Art und Weise, wie git logDateinamen 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 gitgut genug.

verwandte Informationen