Передайте вывод из подстановки процесса в переменную.

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

У меня есть очень сложный набор команд:

command | ... | ... | tee >(grep -c '[^3]$') >(grep -c '[^35]$') 1>/dev/null

Я не хочу иметь временный файл для сохранения вывода, так как он довольно большой. Я попробовал сделать >(grep -c '[^3]$' | read variable) и >(grep -c '[^3]$' | read variable2), но, думаю, это не работает из-за вызова под-оболочки подстановки процесса.

Что я могу сделать, чтобы перенаправлять вывод непосредственно в несколько переменных? Это вообще возможно?

Сейчас у меня есть такое обходное решение:

var=$(command ... | ... | ... | tee >(grep -c '[^3]$') >(grep -c '[^35]$') 1>/dev/null)
var1=$(tail -1 <<< $var)
var2=$(head -1 <<< $var)

но, на мой взгляд, это неуклюже и не выглядит хорошо. Я знаю, что могу сделать grep в другой файл, но мне это тоже не нравится.

Заранее спасибо.

Используйте awk вместо двойного использования tee для подстановок процессов grep. Например:

command | ... | ... | awk '
     /[^3]$/  { c1++; next };
     /[^35]$/ { c2++; next };
     END { print c1, c2 > counts.txt }'

read var1 var2 < counts.txt
rm counts.txt

Если вы не хотите использовать временный файл, вы можете сделать это так:

read var1 var2 < <(command | ... | ... | awk '
         /[^3]$/  { c1++; next };
         /[^35]$/ { c2++; next };
         END { print c1, c2 }')

Во-первых, вы отправляете стандартный вывод в вашей цепочке команд в /dev/null, поэтому ваше присваивание, вероятно, будет присваивать пустое значение. Уберите это. Похоже, вы пытаетесь захватить только первую и последнюю строку вывода; это может быть сложно сделать в одной строке, но awk должен справиться с этим:

var="$(command ... | [...] | tee [...] | awk 'NR==1{print $0} END { print $0}')"

Однако трубы – это простые существа. У них один вход и один выход. Вы не можете (легко) использовать трубы для отправки данных в несколько мест. Ближе всего, что вы сможете достичь, это использование именованных каналов, которые по сути являются временными файлами, которые вы пытаетесь избежать (технически это не так; очереди FIFO отличаются от файлов, но я упрощаю).

Я знаю, что этот вопрос помечен как bash, однако, я покажу, как можно использовать zsh, чтобы приблизиться к тому, что вы хотите.

Таким образом, то, что вы хотите, выглядит примерно так:

printf '%s\n' 1a 2a 1b 2b \
| tee >(grep 1|read -d '' ones) >(grep 2|read -d '' twos)

# проблема: `ones` и `twos` теряются, так как они установлены в под-процессе

С помощью zsh мы можем сделать это:

printf '%s\n' 1a 2a 1b 2b \
| () { <$1 grep 1|read -d '' ones
       <$1 grep 1|read -d '' twos
     } =(cat)

declare ones twos
# ones=$'1a\n1b'
# twos=$'1a\n1b'

В выше приведенном примере мы используем:

  • что последний этап в трубе выполняется в текущем процессе, так что переменные могут быть установлены
  • подстановку процесса =( .. ), которая подобна <( .. ), но может быть прочитана несколько раз (расширяется до имени временного файла, а не трубы, как в случае с <(..), поэтому с =( ..), поддерживается lseek(2); и у каждого процесса своя собственная позиция поиска, поэтому два процесса grep будут читать независимо)
  • анонимную функцию, которая гарантирует, что временный файл =(cat) существует в течение нужного времени (анонимные функции выполняются сразу; () { echo $1 } arg выводит “arg“)

Вы сказали, что не хотите использовать временный файл, однако именно это zsh создаст “под капотом” с приведенным выше кодом. По крайней мере, он создается и удаляется в zsh, т.е. его управление не нагружает программиста.

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

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

Теория

В UNIX-подобных системах одной из сильных сторон является возможность перенаправления и объединения потоков данных, чтобы выполнять сложные команды. Одним из эффективных инструментов для этого является процессозамещение (process substitution). Однако, использование процессозамещения в bash может иметь ограничения, например, в невозможности непосредственного присвоения результата переменным, так как такие команды работают в отдельных подпроцессах. Это означает, что любые изменения переменных внутри подпроцесса не будут видны в родительской оболочке.

Пример

Рассмотрим предложенную команду:

command | ... | ... | tee >(grep -c '[^3]$') >(grep -c '[^35]$') 1>/dev/null

Здесь tee направляет вывод в два параллельных потока через process substitution. Результат каждой команды grep подсчитывает строки, соответствующие определённым условиям. Однако из-за особенностей подпроцессов bash результат grep не может быть напрямую сохранён в переменную, так как каждое grep выполняется в своём собственном контексте.

Применение

  1. Использование Awk для обработки данных: Вместо использования tee и grep, мы можем использовать awk для обработки данных за один проход и сохранения результатов в переменных.

    read var1 var2 < <(command | ... | ... | awk '
        /[^3]$/  { c1++; next };
        /[^35]$/ { c2++; next };
        END { print c1, c2 }')

    Здесь awk сначала обрабатывает входные данные, подсчитывая строки, заканчивающиеся разными символами, и затем выводит результаты, которые могут быть прочитаны в переменные var1 и var2.

  2. Использование временных файлов в zsh: Для более сложных задач, можно использовать zsh, который поддерживает более гибкие механизмы процессозамещения с использованием временных файлов. Пример ниже показывает, как можно использовать zsh для аналогичных задач.

    printf '%s\n' 1a 2a 1b 2b \
    | tee >(grep 1 | read -d '' ones) >(grep 2 | read -d '' twos)

    В zsh можно создать временные файлы через =(...), которые позволят сохранить промежуточные результаты и прочитать их через функцию, работающую в основном процессе.

Заключение

Проблема присвоения значений переменным из параллельных потоков в bash решается через более сложные методы, такие как использование awk для единовременной обработки или переход на zsh с его расширенными возможностями процессозамещения. Если же требуется строгое избегание временных файлов, оптимальным подходом остаётся использование встроенных возможностей обработки данных, таких как awk, что позволяет избежать создания временных файлов и поддерживать производительность за счёт единовременной обработки данных.

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

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