Как обрабатывать последовательность элементов группами по N штук за раз

Вопрос или проблема

Последовательно один за другим:

for i in $(ls -1) ; do command $i ; done

Все за один раз:

for i in $(ls -1); do \
 ( \
  command $i ; done ; \
 ) & \
done; wait

Как сказать, например, 4 или 8 за раз?

Если переход на zsh — это вариант:

autoload zargs
zargs --eof= -rl1 -P4 -- *(N) '' cmd --

Это выполнит cmd -- file, 4 параллельно для file, находящихся в текущем рабочем каталоге.

По умолчанию разделитель eof — это --, но здесь мы переходим к пустой строке на случай, если в текущем рабочем каталоге есть файл с именем --.

Если ваш cmd принимает параметры, но не принимает -- в качестве разделителя параметров, вы можете заменить cmd -- на cmd, а *(N) на ./*(N), чтобы избежать проблем с именами файлов, начинающимися с - (так как cmd будет выполняться как cmd ./-те-файлы-).

Обратите внимание, что он выполняет их партиями по 4 и не начинает новую партию, пока первая не завершится. Вам может быть предпочтительнее поведение GNU xargs -P, которое всегда пытается запустить до 4 задач параллельно:

xargs -r0 -P4 -l1 -a <(print -rNC1 -- *(N)) cmd --

Это можно сделать и с помощью bash (и если ваша система имеет bash (GNU shell), есть шансы, что в ней будет GNU xargs):

print0() { [ "$#" -eq 0 ] || printf '%s\0' "$@"; }
xargs -r0 -P4 -l1 -a <(
  shopt -u failglob
  shopt -s nullglob
  print0 *) cmd --

С помощью команды parallel из moreutils (но не GNU parallel, хотя GNU parallel будет иметь эквивалентный способ сделать это):

parallel -j4 cmd -- ./*(N) # zsh
(shopt -u failglob; shopt -s nullglob
 exec parallel -j4 cmd -- ./*) # bash

-- здесь обозначает для параллельного конца команды, которую нужно выполнить, это не аргумент, переданный --, следовательно, необходим префикс ./, чтобы защититься от имен файлов, начинающихся с -, как отмечено выше.

Я предполагаю, что ссылка OP на command — это заполнитель для другого двоичного файла (в отличие от вызова встроенной функции bash с именем command).

Создайте несколько файлов:

$ touch a.txt b.txt 'c d e.pdf' g.dat i.txt k.txt 'l m n.pdf' qrst.pdf$'\r'
$ mkdir dir1
$ touch dir1/XXX.txt

$ ls -1F
a.txt
b.txt
'c d e.pdf'
dir1/
g.dat
i.txt
k.txt
'l m n.pdf'
'qrst.pdf'$'\r'

$ tree
.
├── a.txt
├── b.txt
├── c d e.pdf
├── dir1
│   └── XXX.txt
├── g.dat
├── i.txt
├── k.txt
├── l m n.pdf
└── qrst.pdf\015

Простой сценарий для обработки имен файлов:

$ cat my_command
#!/bin/bash

printf "my input:%s:\n" "$@"
sleep 4

Общий подход:

  • использовать find, чтобы сгенерировать список файлов из текущего каталога
  • передать файлы в xargs и позволить xargs обрабатывать запуск N экземпляров my_command одновременно

Один из подходов:

find . -maxdepth 1 -type f -print0 | xargs -0 -r -P3 -n1 my_command

Где:

  • find / -print0 и xargs / -0 используются для правильной обработки имен файлов, которые включают пробелы и другие нечитаемые символы
  • -P3 – запускать не более 3 экземпляров my_command одновременно
  • -n1 – отправлять одно имя файла за раз в my_command

Это выдает:

my input:./b.txt:           # 3 строки вывода, напечатанные быстро
my input:./g.dat:
my input:./l m n.pdf:
                            # 4-секундная задержка
my input:./i.txt:           # 3 строки вывода, напечатанные быстро
my input:./c d e.pdf:
my input:./k.txt:
                            # 4-секундная задержка
my input:./a.txt:           # осталось только 2 имени файла, поэтому только 2 экземпляра my_command вызваны для генерации 2 строк вывода быстро
:y input:./qrst.pdf         # обратите внимание, что последний ':' отображается в начале строки; это указывает на то, что завершающий '\r' имени файла был сохранен
                            # 4-секундная задержка

Другой вариант — поместить все файлы в массив и затем брать последовательные срезы массива:

#!/bin/bash

## Нет ошибки, если шаблон не совпадает ни с чем
shopt -u failglob
## Вернуть null, если шаблон не совпадает вместо
## возвращения самого шаблона как строки
shopt -s nullglob

## установить целевой каталог; по умолчанию текущий, если аргумент не указан
target=${1:-.}
## сохранить содержимое $target в массиве $files
files=( "$target"/* )

## Перебирать массив группами по 4
i=0
while [ $i -lt  ${#files[@]} ]; do
  end=$((i+4))
  subset=("${files[@]:$i:4}")
  echo "subset $i - $end: ${subset[@]}"
  ((i+=4))
done

Этот сценарий будет обрабатывать группы по 4 (обратите внимание, что он также будет обрабатывать каталоги, а не только файлы) в любом каталоге, который вы укажете. Если вы запустите его без аргументов, он пройдет через текущий каталог. Например:

$ tree
.
├── file1
├── file10
├── file11
├── file12
├── file2
├── file3
├── file4
├── file5
├── file6
├── file7
├── file8
└── file9

1 каталог, 12 файлов

$ foo.sh
subset 0 - 4: ./file1 ./file10 ./file11 ./file12
subset 4 - 8: ./file2 ./file3 ./file4 ./file5
subset 8 - 12: ./file6 ./file7 ./file8 ./file9

Ответ или решение

Для обработки последовательности элементов группами по N одновременно, существует несколько подходов в Bash и Zsh. Ниже приведены основные методы реализации:

1. Использование xargs

Если ваша задача состоит в том, чтобы выполнять команды над файлами в текущем каталоге с ограничением на количество одновременно запущенных процессов, вы можете использовать xargs:

find . -maxdepth 1 -type f -print0 | xargs -0 -r -P4 -n1 my_command

В этом примере:

  • find . -maxdepth 1 -type f -print0 — находит все файлы в текущем каталоге и выводит их с разделением нулевым символом, чтобы правильно обрабатывать имена файлов с пробелами и специальными символами.
  • xargs -0 — принимает входные данные, разделенные нулем.
  • -r — предотвращает запуск команды, если входные данные отсутствуют.
  • -P4 — указывает xargs запускать максимум 4 процесса одновременно.
  • -n1 — передает один файл за раз в my_command.

2. Использование GNU parallel

Если у вас установлен parallel (часть пакета moreutils), это тоже отличный способ обработки файлов группами:

parallel -j4 my_command -- ./* 

где:

  • -j4 — указывает на запуск до 4 процессов одновременно.
  • my_command -- — это ваша команда, которая будет применяться к каждому файлу в текущем каталоге.

3. Применение массивов и срезов

Вы можете создать массив файлов и обрабатывать их по частям:

#!/bin/bash

shopt -u failglob
shopt -s nullglob

target=${1:-.}
files=( "$target"/* )

for (( i=0; i<${#files[@]}; i+=4 )); do
  subset=("${files[@]:$i:4}")
  echo "Processing subset: ${subset[@]}"
  # Выполняем команду для файлов в subset
  for file in "${subset[@]}"; do
    my_command "$file" &
  done
  wait  # ждем завершения всех процессов в текущем наборе
done

В этом примере:

  • Мы сохраняем все файлы из указанного каталога в массив files.
  • Затем обрабатываем их по 4 файла за раз, создавая подмассивы и запуская команду параллельно, после чего останавливаем выполнение, дожидаясь завершения всех запущенных процессов.

4. Использование zargs в Zsh

Если у вас Zsh, можно использовать zargs для более утонченной обработки:

autoload zargs
zargs --eof= -rl1 -P4 -- *(N) '' cmd --

Это позволяет запускать cmd для каждого файла до 4 процессов одновременно.

Заключение

Все предложенные методы позволяют эффективно обрабатывать файлы группами, соблюдая ограничения на количество одновременно выполняемых процессов. Выбор метода зависит от ваших потребностей и доступных инструментов. К примеру, использование xargs и GNU parallel более универсально и подходит для Bash, в то время как zargs может использовать специфические возможности Zsh.

Оцените материал
Добавить комментарий

Капча загружается...