Вопрос или проблема
У меня есть очень сложный набор команд:
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
выполняется в своём собственном контексте.
Применение
-
Использование Awk для обработки данных: Вместо использования
tee
иgrep
, мы можем использоватьawk
для обработки данных за один проход и сохранения результатов в переменных.read var1 var2 < <(command | ... | ... | awk ' /[^3]$/ { c1++; next }; /[^35]$/ { c2++; next }; END { print c1, c2 }')
Здесь
awk
сначала обрабатывает входные данные, подсчитывая строки, заканчивающиеся разными символами, и затем выводит результаты, которые могут быть прочитаны в переменныеvar1
иvar2
. -
Использование временных файлов в 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
, что позволяет избежать создания временных файлов и поддерживать производительность за счёт единовременной обработки данных.