
Я хочу подсчитать количество строк в трубе, а затем продолжить трубу в зависимости от результата.
Я пытался
x=$(printf 'faa\nbor\nbaz\n' \
| tee /dev/stderr | wc -l) 2>&1 \
| if [[ $x -ge 2 ]]; then
grep a
else
grep b
fi
Но он вообще не фильтрует (ни для "a", ни для "b"). Это было довольно неожиданно, так как по крайней мере эти работают так, как и ожидалось:
printf 'faa\nbor\nbaz\n' | if true; then grep a; else grep b; fi
printf 'faa\nbor\nbaz\n' | if false; then grep a; else grep b; fi
Похоже, я не могу перенаправить stderr изнутри подстановки команд, так как это тоже не работает (в bash). Он печатает все три строки:
x=$(printf 'faa\nbor\nbaz\n' | tee /dev/stderr | wc -l) 2>&1 | grep a
В zsh выводится только две строки.
Но в обеих оболочках переменная x не устанавливается после конвейера и даже во второй половине конвейера.
Что я могу сделать, чтобы посчитать строки в конвейере и действовать в зависимости от этого числа? Я хотел бы избежать временных файлов.
решение1
Этот комментарийправда:
Каждая часть конвейера запускается независимо от других частей того же конвейера. Это означает, что
$x
не может быть доступен в середине конвейера, если он установлен на одном из других этапов.
Это не значит, что вы ничего не можете сделать. Конвейер может считаться основным каналом данных, но процессы могут общаться, используя побочные каналы: файлы, именованные fifo или что-то еще (хотя иногда нужно быть особенно осторожным и не допускать их блокировки).
Вы хотите подсчитать количество строк и условно обработать весь поток данных позже. Это означает, что вам нужно добраться до конца потока и только потом передать весь поток. Поэтому вам нужно как-то сохранить весь поток. Временный файл выглядит как разумный подход. Вы должны разделить свой конвейер как минимум на две части. Первая часть должна сохранять данные в файле; затем строки должны быть подсчитаны (я думаю, эта задача может относиться к первой части); затем последняя часть должна получить число, прочитать файл, чтобы получить данные с самого начала, и действовать соответствующим образом.
Если вы действительно хотите избежать временных файлов, то некоторая часть вашего конвейера должна действовать как-то так sponge
. Чтобы избежать побочных каналов, число строк должно быть передано как самая первая строка вывода, а оставшаяся часть конвейера должна понимать этот протокол.
Рассмотрим следующую команду:
sed '$ {=; H; g; p;}; H; d'
Он накапливает строки в пространстве удержания. Если есть хотя бы одна строка, то после получения последней строки sed
печатает количество строк, за которыми следует пустая строка и фактический ввод.
Пустая строка не нужна, но она появляется "естественно" из этого простого кода. Вместо того, чтобы пытаться избежать ее в sed
, я бы просто разобрался с ней позже в конвейере (например, с помощью sed '2 d'
).
Пример использования:
#!/bin/sh
sed '$ {=; H; g; p;}; H; d' | sed '2 d' | {
if ! IFS= read -r nlines; then
echo "0 lines. Nothing to do." >&2
else
echo "$nlines lines. Processing accordingly." >&2
if [ "$nlines" -ge 2 ]; then
grep a
else
grep b
fi
fi
}
Примечания:
IFS= read -r
является излишеством, поскольку первая строка четко определена и содержит одно число (или оно не существует).- Я использовал
/bin/sh
. Код также будет работать в Bash. Вы не можете предполагать,
sed
что сможете хранить произвольный объем данных.Спецификация POSIXговорит:Пространства шаблона и хранения должны быть способны вмещать не менее 8192 байтов каждое.
Так что, возможно, предел составляет всего 8192 байта. С другой стороны, я могу легко представить временный файл, содержащий 1 ТБ данных. Возможно, не стоит избегать временных файлов любой ценой.
В заголовке говорится «подсчитайте количество строк», но ваш пример пытается решить, является ли число 2 или больше (N или больше в общем случае). Эти задачи не эквивалентны. После 2-й (N-й) строки ввода вы знаете ответ на последнюю задачу, четные строки будут появляться бесконечно. Приведенный выше код не может обрабатывать неопределенный ввод. Давайте исправим это в некоторой степени.
sed '
7~1 {p; d}
6 {H; g; i \
6+
p; d}
$ {=; H; g; p}
6! {H; d}
'
Эта команда ведет себя как предыдущее решение, за исключением того, что когда она доходит до 6-й строки, она предполагает (выводит) количество строк 6+
. Затем печатаются уже просмотренные строки, а следующие строки (если таковые имеются) печатаются, как только они появляются ( cat
поведение, подобное -).
Пример использования:
#!/bin/sh
threshold=6
sed "
$((threshold+1))~1 {p; d}
$threshold {H; g; i \
$threshold+
p; d}
$ {=; H; g; p}
${threshold}! {H; d}
" | sed '2 d' | {
if ! IFS= read -r nlines; then
echo "0 lines. Nothing to do." >&2
else
echo "$nlines lines. Processing accordingly." >&2
if [ "$nlines" = "$threshold+" ]; then
grep a
else
grep b
fi
fi
}
Примечания:
- Исправлено "в некоторой степени", поскольку ограничение
sed
(каким бы оно ни было в вашем случае) все еще применяется. Но теперьsed
обрабатывает не более$threshold
количества строк; если$threshold
достаточно мало, то все должно быть в порядке. - В примере кода выполняется только тестирование,
$threshold+
но протокол позволяет различать строки 0, 1, 2, …, порог-минус-один и порог-или-более.
Я не очень разбираюсь в sed
. Если мой sed
код можно упростить, пожалуйста, оставьте мне подсказку в комментарии.
решение2
На основе обсуждения и кода sed от Камиля я нашел следующее решение awk:
awk -v th="$threshold" '
function print_lines() { for (i in lines) print lines[i] }
NR < th { lines[NR] = $0 }
NR > th { print }
NR == th { print th; print_lines(); print }
END { if (NR < th) { print NR; print_lines(); } }' \
| if read nlines; then
if [ "$nlines" -eq "$threshold" ]; then
grep a
else
grep b
fi
fi