Как проверить, является ли канал пустым, и запустить команду для данных, если это не так?


42

Я передал строку в скрипте bash и хочу проверить, есть ли в канале данные, прежде чем передавать их в программу.

Поиск, который я нашел, test -t 0но он не работает здесь. Всегда возвращает ложь. Так как быть уверенным, что в трубе есть данные?

Пример:

echo "string" | [ -t 0 ] && echo "empty" || echo "fill"

Выход: fill

echo "string" | tail -n+2 | [ -t 0 ] && echo "empty" || echo "fill"

Выход: fill

В отличие от стандартного / канонического способа проверки того, дает ли предыдущий конвейер выходной сигнал? вход должен быть сохранен, чтобы передать его программе. Это обобщает, Как направить вывод от одного процесса к другому, но выполнить, только если первый имеет вывод? который фокусируется на отправке электронной почты.


Ответы:


37

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

Так что просто сделайте это: прочитайте один байт; если вы обнаружите конец файла, то делайте то, что хотите, когда ввод пуст; если вы действительно читаете байт, тогда выполните форк то, что вы хотите сделать, когда ввод не пустой, направьте в него этот байт и передайте остальные данные.

first_byte=$(dd bs=1 count=1 2>/dev/null | od -t o1 -A n | tr -dc 0-9)
if [ -z "$first_byte" ]; then
  # stuff to do if the input is empty
else
  {
    printf "\\$first_byte"
    cat
  } | {
    # stuff to do if the input is not empty
  }      
fi

ifneУтилита от moreutils Джои Гесса запускает команду , если ее ввод не пуст. Обычно он не устанавливается по умолчанию, но он должен быть доступен или прост в сборке на большинстве вариантов Unix. Если вход пуст, ifneничего не делает и возвращает статус 0, который нельзя отличить от успешно выполненной команды. Если вы хотите что-то сделать, если ввод пуст, вам нужно сделать так, чтобы команда не возвращала 0, что можно сделать, если в успешном случае вернуть различимый статус ошибки:

ifne sh -c 'do_stuff_with_input && exit 255'
case $? in
  0) echo empty;;
  255) echo success;;
  *) echo failure;;
esac

test -t 0не имеет к этому никакого отношения; он проверяет, является ли стандартный ввод терминалом. Это не говорит ничего так или иначе относительно того, доступен ли какой-либо вход.


Я полагаю, что в системах с каналами на основе STREAMS (Solaris HP / UX) вы можете использовать ioctl I_PEEK, чтобы посмотреть, что находится на канале, не потребляя его.
Стефан Шазелас

@ StéphaneChazelas, к сожалению, нет способа получить данные из pipe / fifo на * BSD, поэтому нет перспективы реализовать портативную peek утилиту, которая могла бы возвращать реальные данные из канала, а не только их количество. (в 4.4 BSD каналы 386BSD и т. д. были реализованы в виде пар сокетов , но это было выпотрошено в более поздних версиях * BSD - хотя они оставляли их двунаправленными).
Мосви

В bash есть подпрограмма для проверки ввода через read -t 0( a в этом случае означает тайм-аут, если вам интересно).
Исаак

11

Простым решением является использование ifneкоманды (если ввод не пустой). В некоторых дистрибутивах он не установлен по умолчанию. Это часть пакета moreutilsв большинстве дистрибутивов.

ifne запускает данную команду тогда и только тогда, когда стандартный ввод не пуст

Обратите внимание, что если стандартный ввод не пуст, он передается ifneданной команде


2
Начиная с 2017 года его нет по умолчанию в Mac или Ubuntu.
Шридхар Сарнобат

7

Старый вопрос, но в случае, если кто-то сталкивается с ним, как я: мое решение - читать с таймаутом.

while read -t 5 line; do
    echo "$line"
done

Если stdinпусто, это вернется через 5 секунд. В противном случае он будет читать все входные данные, и вы можете обрабатывать их по мере необходимости.


Хотя мне нравится эта идея, к -tсожалению, она не является частью POSIX: pubs.opengroup.org/onlinepubs/9699919799/utilities/read.html
JepZ

6

проверить, открыт ли файл-дескриптор stdin (0):

[ ! -t 0 ] && echo "stdin has data" || echo "stdin is empty"

Когда вы передаете некоторые данные и хотите проверить, есть ли они, вы все равно проходите FD, так что это тоже не очень хороший тест.
Jakuje

1
[ -t 0 ]проверяет, открыт ли fd 0 для tty , а не закрыт или открыт.
Мосви

@mosvy, не могли бы вы рассказать, как это повлияет на использование этого решения в сценарии? Есть ли случаи, когда это не работает?
JepZ

@JepZ да? ./that_script </dev/null=> "У stdin есть данные". Или ./that_script <&-чтобы действительно был закрыт stdin .
Мосви

5

Вы также можете использовать test -s /dev/stdin(в явной форме).

# test if a pipe is empty or not
echo "string" | 
    (test -s /dev/stdin && echo 'pipe has data' && cat || echo 'pipe is empty')

echo "string" | tail -n+2 | 
    (test -s /dev/stdin && echo 'pipe has data' && cat || echo 'pipe is empty')

: | (test -s /dev/stdin && echo 'pipe has data' && cat || echo 'pipe is empty')

8
Не работает для меня Всегда говорит, что труба пуста.
амфетахин

2
Работает на моем Mac, но не на моей коробке Linux.
пик

3

В Баш:

read -t 0 

Обнаруживает, есть ли на входе данные (без чтения чего-либо). Затем вы можете прочитать ввод (если он доступен во время выполнения чтения):

if     read -t 0
then   read -r input
       echo "got input: $input"
else   echo "No data to read"
fi

Примечание: поймите, что это зависит от времени. Это обнаруживает, если вход уже имеет данные только во время выполнения read -t.

Например, с

{ sleep 0.1; echo "abc"; } | read -t 0; echo "$?"

выход 1(ошибка чтения, т. е. пустой вход). Эхо записывает некоторые данные, но не очень быстро запускается и записывает свой первый байт, поэтому read -t 0сообщит, что его ввод пуст, поскольку программа еще ничего не записала.


github.com/bminor/bash/blob/… вот источник того, как bash обнаруживает, что что-то есть в дескрипторе файла.
Павел Патрин

Спасибо @PavelPatrin
Исаак

1
@PavelPatrin Это не работает . Как ясно видно из вашей ссылки, bashбудет либо a, select()либо an ioctl(FIONREAD), либо ни один из них, но не оба, как это должно работать для него. read -t0сломан. Не используйте его
mosvy

Ооо, сегодня я пытаюсь понять, что с ним не так два часа! Спасибо, @mosvy!
Павел Патрин

3

Один простой способ проверить, доступны ли данные для чтения в Unix, с помощью FIONREADioctl.

Я не могу представить себе, чтобы какая-либо стандартная утилита делала именно это, поэтому вот тривиальная программа, которая делает это (лучше, чем ifneиз moreutils IMHO ;-)).

fionread [ prog args ... ]

Если на stdin нет данных, он выйдет со статусом 1. Если есть данные, он запустится prog. Если нет prog, он выйдет со статусом 0.

Вы можете удалить pollзвонок, если вас интересуют только данные, доступные сразу . Это должно работать с большинством типов fds, а не только с каналами.

fionread.c

#include <unistd.h>
#include <poll.h>
#include <sys/ioctl.h>
#ifdef __sun
#include <sys/filio.h>
#endif
#include <err.h>

int main(int ac, char **av){
        int r; struct pollfd pd = { 0, POLLIN };
        if(poll(&pd, 1, -1) < 0) err(1, "poll");
        if(ioctl(0, FIONREAD, &r)) err(1, "ioctl(FIONREAD)");
        if(!r) return 1;
        if(++av, --ac < 1) return 0;
        execvp(*av, av);
        err(1, "execvp %s", *av);
}

Эта программа действительно работает? Вам не нужно ждать POLLHUPсобытия, чтобы обработать пустой случай? Это работает, если есть несколько описаний файлов на другом конце канала?
Жиль "ТАК - прекрати быть злым"

Да, это работает. POLLHUP возвращается только опросом, вы должны использовать POLLIN для ожидания POLLHUP. Не имеет значения, сколько открытых ручек на любом конце трубы.
Мосви

См. Unix.stackexchange.com/search?q=FIONREAD+user%3A22565, чтобы узнать, как запустить FIONREAD из perl (более распространенный, чем компиляторы)
Стефан Шазелас

Подпрограмма, которая использует FIONREAD (или HAVE_SELECT), реализована в bash здесь .
Исаак

2

Если вам нравятся короткие и загадочные строки:

$ echo "string" | grep . && echo "fill" || echo "empty"
string
fill
$ echo "string" | tail -n+2 | grep . && echo "fill" || echo "empty"
empty

Я использовал примеры из оригинального вопроса. Если вы не хотите использовать данные по каналу-q параметр с помощью grep


0

Кажется, это разумная реализация ifne в bash, если вы хорошо читаете всю первую строку

ifne () {
        read line || return 1
        (echo "$line"; cat) | eval "$@"
}


echo hi | ifne xargs echo hi =
cat /dev/null | ifne xargs echo should not echo

5
readтакже вернет false, если ввод не пустой, но не содержит символа новой строки, readвыполняет некоторую обработку своего ввода и может прочитать более одной строки, если вы не вызовете его как IFS= read -r line. echoне может быть использован для произвольных данных.
Стефан Шазелас

0

Это работает для меня, используя read -rt 0

пример из исходного вопроса, без данных:

echo "string" | tail -n+2 | if read -rt 0 ; then echo has data ; else echo no data ; fi

нет, это не работает попробуйте с { sleep .1; echo yes; } | { read -rt0 || echo NO; cat; }(ложно отрицательный) и true | { sleep .1; read -rt0 && echo YES; }(ложно положительный). На самом деле, в Bash readбудет обмануть даже FDS , открытый в записи-только режим: { read -rt0 && echo YES; cat; } 0>/tmp/foo. Единственное, что он, кажется, делает, это select(2)на этом FD.
Мосви

... и selectвернет fd как "готовый", если a read(2)на нем не заблокируется, независимо от того, вернется ли он EOFили возникнет ошибка. Вывод: read -t0это сломано в bash. Не используйте это.
Мосви

@mosvy Вы сообщили об этом с bashbug?
Исаак

@mosvy The { sleep .1; echo yes; } | { read -rt0 || echo NO; cat; }не является ложноотрицательным, потому что (во время чтения) нет ввода. Позже (сон 0,1) , что вход в наличии (для кошки).
Исаак

@mosvy Почему параметр r влияет на обнаружение ?: echo "" | { read -t0 && echo YES; }печатает ДА, но echo "" | { read -rt0 && echo YES; }не печатает .
Исаак
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.