У меня есть куча скриптов, которые делают это:
command | while read something; do
a
long
list
of
commands
done
Кто-нибудь когда-нибудь задумывался о том, как запустить все команды, проходящие через конвейер, используя parallel
?для этого случаярешения этой проблемы, но я ищу что-тообщийкоторый требует лишь минимальных изменений в моих скриптах и, если возможно, позволяет работать даже если parallel
он не установлен.
command
выше может быть практически все, не ограничиваясь одной командой. a long list of commands
может быть и совершенно другим.
Например, рассмотрим этот однострочный код, который изменяет даты изменения извлеченных файлов в репозитории git на дату их последнего изменения:
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
По умолчанию это ужасно медленно, потому что git log
и touch
обе команды довольно медленные. Но это всего лишь один пример, и притом простой.
решение1
Я бы использовал функцию bash и вызвал бы ее:
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
Используйте parallel -0
, если хотите разделить по NUL.
Если вы хотите запустить вышеизложенное без установки GNU Parallel, вы можете использовать:
parallel --embed > myscript.sh
а затем добавьте вышеизложенное к myscript.sh
.
решение2
Можно заставить bash или ksh одновременно запускать список независимых команд, так что каждый поток начинает новую команду, как только его предыдущая задача завершается. Потоки остаются занятыми, за исключением команд хвостовой части.
Основной метод заключается в запуске нескольких асинхронных оболочек, которые все считывают данные из одного и того же канала: канал гарантирует буферизацию строк и атомарное чтение (файл команд может использоваться с cat file |
перенаправлением, но не посредством него).
Команды могут быть любыми однострочными командами оболочки (в правильном синтаксисе для оболочки, владеющей потоком), но не могут полагаться на результаты предыдущих команд, поскольку распределение команд по потокам является произвольным. Сложные команды лучше всего настраивать как внешние скрипты, чтобы их можно было вызывать как простую команду с аргументами.
Это тестовый запуск шести заданий в трех потоках, иллюстрирующий перекрытие заданий. (Я также провел стресс-тест 240 заданий в 80 потоках на своем ноутбуке.)
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
Это прокси-скрипт, который обеспечивает отладку этих заданий.
#! /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}"
Это скрипт управления потоком. Он создает команды задания для запуска прокси и инициирует фоновые оболочки.
#! /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
решение3
Вместо того, чтобы запускать git ls-tree
и затем git log
, date
, и touch
несколько раз в цикле bash while read, следующий скрипт perl берет вывод git log --name-only HEAD
и сохраняет самую последнюю временную метку для любого файла, упомянутого в журнале коммитов, в хэше с именем %files
. Он игнорирует имена файлов, которые не существуют.
Затем он создает хэш массивов («HoA» — см. man perldsc
), называемый %times
, с временными метками в качестве ключа хэша и значениями, представляющими собой анонимный массив, содержащий имена файлов с этой временной меткой. Это оптимизация, так что сенсорную функцию нужно запускать только один раз для каждой временной метки, а не один раз для каждого имени файла.
Идентификатор коммита, сообщение о коммите, имя автора и пустые строки из git log
выходных данных игнорируются.
Скрипт использует unqqbackslash()
функцию изСтрока::Escapeдля каждого имени файла для правильной обработки способа git log
печати имен файлов со встроенными символами табуляции, новой строки, двойными кавычками и т. д. (т. е. как строк в двойных кавычках с экранированными кодами/символами обратной косой черты).
Я ожидаю, что он будет работать как минимум в десятки раз быстрее, чем ваш 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} });
};
Скрипт используетДата::Анализ, Файл::Touch, иСтрока::Escapeмодули perl.
В Debian, apt install libtimedate-perl libfile-touch-perl libstring-escape-perl
. Другие дистрибутивы, вероятно, также имеют их в пакетах. В противном случае установите их с помощью cpan
.
Пример использования в репозитории git с парой ненужных файлов ( file
, и 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
(совсем немного подделано — я отредактировал сообщения о коммите, чтобы было понятно, когда оба файла были впервые зафиксированы, а затем file2 был изменён и снова зафиксирован. В остальном это просто копипаст из моего терминала).
Это моя вторая попытка: изначально я пытался использоватьGit::Сыроймодуль, но не могу понять, как заставить его выдать мне списоктолькоимена файлов, измененные в определенном коммите. Я уверен, что есть способ, но я отказался от этого. Я просто недостаточно хорошо знаю внутренности git
.