Wie überprüfe ich, ob eine Pipe leer ist und führe einen Befehl für die Daten aus, wenn dies nicht der Fall ist?

42

Ich habe eine Zeile im Bash-Skript gepippt und möchte überprüfen, ob die Pipe Daten enthält, bevor ich sie an ein Programm weitergebe.

Suche habe ich über gefunden, test -t 0aber es funktioniert hier nicht. Gibt immer false zurück. Wie kann man also sicherstellen, dass die Pipe Daten enthält?

Beispiel:

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

Ausgabe: fill

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

Ausgabe: fill

Im Gegensatz zu Standard / Canonical Weg, um zu testen, ob die vorstehende Pipeline Output produziert? Die Eingabe muss erhalten bleiben, damit sie an das Programm übergeben werden kann. Dies verallgemeinert, wie die Ausgabe von einem Prozess zu einem anderen weitergeleitet wird, aber nur ausgeführt wird, wenn der erste eine Ausgabe hat. Das konzentriert sich auf das Senden von E-Mails.

Zetah
quelle

Antworten:

37

Es gibt keine Möglichkeit, den Inhalt einer Pipe mit den allgemein verfügbaren Shell-Dienstprogrammen zu überprüfen, noch gibt es eine Möglichkeit, ein Zeichen in die Pipe einzulesen und es dann zurückzusetzen. Die einzige Möglichkeit zu erkennen, dass eine Pipe Daten enthält, besteht darin, ein Byte zu lesen und dieses Byte dann an sein Ziel zu bringen.

Tun Sie einfach Folgendes: Lesen Sie ein Byte. Wenn Sie ein Dateiende erkennen, tun Sie, was Sie tun möchten, wenn die Eingabe leer ist. Wenn Sie ein Byte lesen, geben Sie an, was Sie tun möchten, wenn die Eingabe nicht leer ist, leiten Sie dieses Byte hinein und leiten Sie den Rest der Daten.

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

Das ifneHilfsprogramm aus Joey Hess 'moreutils führt einen Befehl aus, wenn seine Eingabe nicht leer ist. Es wird normalerweise nicht standardmäßig installiert, sollte jedoch verfügbar sein oder auf den meisten Unix-Varianten einfach aufzubauen sein. Wenn die Eingabe leer ist, ifnewird nichts ausgeführt und der Status 0 zurückgegeben, der nicht von dem Befehl unterschieden werden kann, der erfolgreich ausgeführt wird. Wenn Sie etwas tun möchten, wenn die Eingabe leer ist, müssen Sie dafür sorgen, dass der Befehl keine 0 zurückgibt. Dies kann erfolgen, indem der Erfolgsfall einen erkennbaren Fehlerstatus zurückgibt:

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

test -t 0hat nichts damit zu tun; Es wird geprüft, ob die Standardeingabe ein Terminal ist. Auf die eine oder andere Weise sagt es nichts darüber aus, ob ein Eingang verfügbar ist.

Gilles 'SO - hör auf böse zu sein'
quelle
Auf Systemen mit STREAMS-basierten Pipes (Solaris HP / UX) können Sie mit I_PEEK ioctl einen Blick auf die Pipes werfen, ohne sie zu verbrauchen.
Stéphane Chazelas
@ StéphaneChazelas Leider gibt es keine Möglichkeit, Daten aus einer Pipe / Fifo auf * BSD zu sehen. Es gibt also keine Perspektive, ein portables peek Dienstprogramm zu implementieren, das die tatsächlichen Daten aus einer Pipe zurückgeben könnte, und nicht nur, wie viel davon vorhanden ist. (In 4.4 BSD, 386BSD usw. wurden Pipes als Socket-Paare implementiert , aber das wurde in späteren Versionen von * BSD ausgeblendet - obwohl sie bidirektional gehalten wurden).
Mosvy
Bash hat eine Routine, um die Eingabe zu überprüfen, die über a angezeigt wird read -t 0(t bedeutet in diesem Fall Timeout, wenn Sie sich fragen).
Isaac
11

Eine einfache Lösung ist die Verwendung eines ifneBefehls (wenn die Eingabe nicht leer ist). In einigen Distributionen wird es nicht standardmäßig installiert. Es ist Teil des Pakets moreutilsin den meisten Distributionen.

ifne Führt einen bestimmten Befehl genau dann aus, wenn die Standardeingabe nicht leer ist

Beachten Sie, dass die Standardeingabe, wenn sie nicht leer ist, ifnean den angegebenen Befehl übergeben wird

Nick Wirth
quelle
2
Ab 2017 ist es in Mac oder Ubuntu nicht standardmäßig vorhanden.
Sridhar Sarnobat
7

Alte Frage, aber für den Fall, dass jemand darauf stößt wie ich: Meine Lösung ist es, mit einer Auszeit zu lesen.

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

Wenn stdinleer, kehrt dies nach 5 Sekunden zurück. Andernfalls werden alle Eingaben gelesen und können nach Bedarf verarbeitet werden.

Ladd
quelle
Obwohl mir die Idee gefällt, -tist sie leider nicht Teil von POSIX: pubs.opengroup.org/onlinepubs/9699919799/utilities/read.html
JepZ
6

überprüfe ob der Dateideskriptor von stdin (0) offen oder geschlossen ist:

[ ! -t 0 ] && echo "stdin has data" || echo "stdin is empty"
mviereck
quelle
Wenn Sie einige Daten übergeben und prüfen möchten, ob es welche gibt, übergeben Sie trotzdem die FD, sodass dies ebenfalls kein guter Test ist.
Jakuje
1
[ -t 0 ]prüft, ob fd 0 bis zu einem tty geöffnet ist , nicht, ob es geschlossen oder offen ist.
Mosvy
@mosvy Könnten Sie bitte erläutern, wie sich dies auf die Verwendung dieser Lösung in einem Skript auswirken würde? Gibt es Fälle, in denen es nicht funktioniert?
JepZ
@JepZ huh? ./that_script </dev/null=> "stdin has data". Oder ./that_script <&-um die stdin wirklich geschlossen zu haben .
Mosvy
5

Sie können auch test -s /dev/stdin(in einer expliziten Subshell) verwenden.

# 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')
trent55
quelle
8
Funktioniert bei mir nicht. Sagt immer, dass die Pipe leer ist.
Amphetamachine
2
Funktioniert auf meinem Mac, aber nicht auf meiner Linux-Box.
Peak
3

In der Bash:

read -t 0 

Erkennt, ob eine Eingabe Daten enthält (ohne etwas zu lesen). Dann können Sie die Eingabe lesen (wenn die Eingabe zum Zeitpunkt der Ausführung des Lesevorgangs verfügbar ist):

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

Hinweis: Bitte beachten Sie, dass dies vom Timing abhängt. Dies erkennt, ob die Eingabe bereits Daten enthält, und zwar nur zu dem Zeitpunkt, zu dem sie ausgeführt wird read -t.

Zum Beispiel mit

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

die Ausgabe ist 1(Lesefehler, dh: leere Eingabe). Das Echo schreibt einige Daten, ist jedoch nicht sehr schnell zu starten und schreibt sein erstes Byte. Daher read -t 0wird gemeldet, dass seine Eingabe leer ist, da das Programm noch nichts geschrieben hat.

Isaac
quelle
github.com/bminor/bash/blob/… - hier ist eine Quelle, wie bash erkennt, dass sich etwas im Dateideskriptor befindet.
Pavel Patrin
Thanks @PavelPatrin
Isaac
1
@PavelPatrin Das geht nicht . Wie aus Ihrem Link klar hervorgeht, bashwird entweder a select()oder a ioctl(FIONREAD)oder keiner von beiden ausgeführt, aber nicht beide, so wie es sollte, damit es funktioniert. read -t0ist kaputt. Verwenden Sie es nicht
mosvy
Oooh, heute versuche ich zwei Stunden lang zu verstehen, was daran falsch ist! Vielen Dank, @mosvy!
Pavel Patrin
3

Eine einfache Möglichkeit, um zu überprüfen, ob Daten zum Lesen in Unix verfügbar sind, ist FIONREADioctl.

Ich kann mir kein Standard-Dienstprogramm vorstellen, das genau das tut. ifneDeshalb hier ein triviales Programm, das dies tut (besser als das von moreutils IMHO ;-)).

fionread [ prog args ... ]

Wenn für stdin keine Daten verfügbar sind, wird es mit Status 1 beendet. Wenn Daten vorhanden sind, wird es ausgeführt prog. Wenn nein progangegeben wird, wird es mit dem Status 0 beendet.

Sie können den pollAnruf entfernen , wenn Sie nur an sofort verfügbaren Daten interessiert sind. Dies sollte mit den meisten Arten von FDS funktionieren, nicht nur mit Pipes.

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);
}
Mosvy
quelle
Funktioniert dieses Programm tatsächlich? Müssen Sie nicht auch auf ein POLLHUPEreignis warten , um den leeren Fall zu bearbeiten? Funktioniert das, wenn sich am anderen Ende der Pipe mehrere Dateibeschreibungen befinden?
Gilles 'SO- hör auf böse zu sein'
Ja es funktioniert. POLLHUP wird nur per Umfrage zurückgegeben. Verwenden Sie POLLIN, um auf ein POLLHUP zu warten. Es spielt keine Rolle, wie viele offene Griffe sich an einem Ende der Leitung befinden.
Mosvy
Unter unix.stackexchange.com/search?q=FIONREAD+user%3A22565 erfahren Sie, wie Sie FIONREAD über Perl ausführen (häufiger als Compiler)
Stéphane Chazelas
Die Routine, die FIONREAD (oder HAVE_SELECT) verwendet, ist hier in bash implementiert .
Isaac
2

Wenn Sie kurze und kryptische Einzeiler mögen:

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

Ich habe die Beispiele aus der ursprünglichen Frage verwendet. Wenn Sie die weitergeleiteten Daten nicht möchten, verwenden Sie die -qOption grep

Yashma
quelle
0

Dies scheint eine vernünftige ifne-Implementierung in bash zu sein, wenn Sie mit dem Lesen der gesamten ersten Zeile einverstanden sind

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


echo hi | ifne xargs echo hi =
cat /dev/null | ifne xargs echo should not echo
Eric Woodruff
quelle
5
readGibt auch false zurück, wenn die Eingabe nicht leer ist, aber kein Zeilenumbruchzeichen enthält, readverarbeitet die Eingabe und liest möglicherweise mehr als eine Zeile, sofern Sie sie nicht als aufrufen IFS= read -r line. echoKann nicht für beliebige Daten verwendet werden.
Stéphane Chazelas
0

Das funktioniert bei mir mit read -rt 0

Beispiel aus der ursprünglichen Frage, ohne Daten:

echo "string" | tail -n+2 | if read -rt 0 ; then echo has data ; else echo no data ; fi
user333755
quelle
nein das geht nicht versuchen Sie es mit { sleep .1; echo yes; } | { read -rt0 || echo NO; cat; }(falsch negativ) und true | { sleep .1; read -rt0 && echo YES; }(falsch positiv). In der Tat, bash readwird sogar von fds eröffnet täuschen Nur - Schreib- Modus: { read -rt0 && echo YES; cat; } 0>/tmp/foo. Das einzige, was es zu tun scheint, ist ein select(2)auf diesem fd.
Mosvy
... und selectgibt ein fd als "ready" zurück, wenn ein read(2)darauf nicht blockiert würde, egal ob es zurückkommt EOFoder ein Fehler vorliegt. Fazit: read -t0ist gebrochen in bash. Benutze es nicht.
Mosvy
@mosvy Hast du es mit bashbug gemeldet?
Isaac
@mosvy Das { sleep .1; echo yes; } | { read -rt0 || echo NO; cat; }ist kein falsches Negativ, da (zum Zeitpunkt der Ausführung des Lesevorgangs ) keine Eingabe erfolgt. Später (sleep 0,1) , die Eingabe ist verfügbar (für die Katze).
Isaac
@mosvy Warum wirkt sich die Option r auf die Erkennung aus ?: Gibt echo "" | { read -t0 && echo YES; }JA aus, echo "" | { read -rt0 && echo YES; }aber nicht.
Isaac