Comment vérifier si un tube est vide et exécuter une commande sur les données si ce n'est pas le cas?

42

J'ai pipé une ligne dans le script bash et je veux vérifier si le tuyau contient des données avant de l'envoyer à un programme.

En cherchant j'ai trouvé environ test -t 0mais ça ne marche pas ici. Retourne toujours faux. Alors, comment être sûr que le tuyau a des données?

Exemple:

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

Sortie: fill

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

Sortie: fill

Contrairement à la manière standard / canonique de tester si le pipeline précédent produit une sortie? l'entrée doit être préservée pour la transmettre au programme. Ceci généralise Comment diriger la sortie d'un processus vers un autre mais ne s'exécuter que si le premier a une sortie? qui se concentre sur l'envoi d'email.

Zetah
la source
Très similaire: faites un pipe conditionnel à un retour non vide (sur super utilisateur )
G-Man dit 'Réintégrez Monica' le

Réponses:

37

Il n’ya aucun moyen de jeter un coup d’œil sur le contenu d’un tube à l’aide des utilitaires shell courants, ni de lire un caractère sur le tube, puis de le remettre en place. Le seul moyen de savoir qu'un canal contient des données est de lire un octet, puis de le renvoyer à sa destination.

Alors faites juste ça: lisez un octet; si vous détectez une fin de fichier, faites ce que vous voulez quand l'entrée est vide; si vous lisez un octet, indiquez ce que vous voulez faire lorsque l'entrée n'est pas vide, transmettez-lui cet octet et transmettez le reste des données.

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

L' ifneutilitaire de moreutils de Joey Hess exécute une commande si son entrée n'est pas vide. Il n'est généralement pas installé par défaut, mais il devrait être disponible ou facile à utiliser avec la plupart des variantes d'Unix. Si l'entrée est vide, ifnene fait rien et renvoie le statut 0, qui ne peut pas être distingué de l'exécution de la commande. Si vous voulez faire quelque chose si l'entrée est vide, vous devez vous assurer que la commande ne renvoie pas 0, ce qui peut être fait en renvoyant un état d'erreur distinct au cas de réussite:

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

test -t 0n'a rien à voir avec cela; il teste si l'entrée standard est un terminal. Il ne dit rien d'une manière ou d'une autre quant à savoir si une entrée est disponible.

Gilles, arrête de faire le mal
la source
Sur les systèmes dotés de tuyaux basés sur STREAMS (Solaris HP / UX), je pense que vous pouvez utiliser l'ioctl I_PEEK pour visualiser le contenu d'un tuyau sans le consommer.
Stéphane Chazelas
@ StéphaneChazelas Malheureusement, il n'y a aucun moyen de consulter les données d'un pipe / fifo sur * BSD, il n'y a donc aucune perspective pour implémenter un utilitaire portable peek qui pourrait renvoyer les données réelles d'un pipe, pas seulement la quantité qu'il contient. (Dans 4.4, BSD, 386BSD, etc., les tuyaux étaient implémentés sous forme de paires de sockets , mais cela a été vidé dans les versions ultérieures de * BSD - bien qu'ils les aient gardés bidirectionnels).
Mosvy
bash a une routine pour vérifier les entrées exposées via un read -t 0(t dans ce cas-ci signifie timeout, si vous vous le demandez).
Isaac
11

Une solution simple consiste à utiliser la ifnecommande (si l'entrée n'est pas vide). Dans certaines distributions, il n'est pas installé par défaut. C'est une partie du paquet moreutilsdans la plupart des distributions.

ifne exécute une commande donnée si et seulement si l'entrée standard n'est pas vide

Notez que si l'entrée standard n'est pas vide, elle est transmise ifneà la commande donnée.

Nick Wirth
la source
2
À partir de 2017, ce n'est pas là par défaut pour Mac ou Ubuntu.
Sridhar Sarnobat
7

Ancienne question, mais au cas où quelqu'un l'aurait trouvée comme moi: ma solution est de lire avec un délai d'attente.

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

Si stdinest vide, cela reviendra après 5 secondes. Sinon, toutes les entrées sont lues et vous pouvez les traiter au besoin.

Ladd
la source
Bien que cette idée me-t
plaise
6

vérifie si le descripteur de fichier de stdin (0) est ouvert ou fermé:

[ ! -t 0 ] && echo "stdin has data" || echo "stdin is empty"
Mviereck
la source
Lorsque vous transmettez des données et que vous voulez vérifier s’il en existe, vous passez quand même le FD, ce n’est donc pas un bon test.
Jakuje
1
[ -t 0 ]vérifie si le fd 0 est ouvert sur un terminal , pas s'il est fermé ou ouvert.
Mosvy
@mosvy, pourriez-vous préciser en quoi cela affecterait l'utilisation de cette solution dans un script? Y a-t-il des cas où cela ne fonctionne pas?
JepZ
@ JPZ hein? ./that_script </dev/null=> "stdin a data". Ou ./that_script <&-d'avoir le stdin vraiment fermé .
Mosvy
5

Vous pouvez également utiliser test -s /dev/stdin(dans un sous-shell explicite).

# 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
la source
8
Ca ne marche pas pour moi Dit toujours que le tuyau est vide.
amphetamachine
2
Fonctionne sur mon Mac, mais pas sur ma machine Linux.
pic
3

En bash:

read -t 0 

Détecte si une entrée contient des données (sans rien lire). Ensuite, vous pouvez lire l'entrée (si l'entrée est disponible au moment de l'exécution de la lecture):

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

Remarque: Comprenez que cela dépend du moment choisi. Ceci détecte si l'entrée contient déjà des données uniquement au moment de l' read -texécution.

Par exemple, avec

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

la sortie est 1(échec de lecture, c.-à-d. entrée vide). L'écho écrit des données mais n'est pas très rapide à démarrer et écrit son premier octet, ainsi, read -t 0signalera que son entrée est vide, puisque le programme n'a encore rien écrit.

Isaac
la source
github.com/bminor/bash/blob/… - voici la source indiquant comment bash détecte que quelque chose se trouve dans le descripteur de fichier.
Pavel Patrin le
Merci @PavelPatrin
Isaac
1
@PavelPatrin Cela ne fonctionne pas . Comme vous pouvez le voir clairement sur votre lien, bashfera l'un select()ou l' ioctl(FIONREAD)autre, mais pas les deux, comme il se doit pour que cela fonctionne. read -t0est cassé. Ne pas utiliser
mosvy
Oooh, aujourd'hui, j'essaie de comprendre ce qui ne va pas pendant deux heures! Merci, @mosvy!
Pavel Patrin le
3

Avec FIONREADioctl, vous pouvez facilement vérifier si des données sont lisibles sous Unix .

Je ne peux pas penser à un utilitaire standard faisant exactement cela, alors voici un programme trivial le faisant (meilleur que celui ifnede moreutils IMHO ;-)).

fionread [ prog args ... ]

S'il n'y a pas de données disponibles sur stdin, il sortira avec le statut 1. S'il y a des données, elles seront exécutées prog. Si non prog, il sortira avec le statut 0.

Vous pouvez supprimer l' pollappel si vous n'êtes intéressé que par les données immédiatement disponibles. Cela devrait fonctionner avec la plupart des types de fds, pas seulement les 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
la source
Ce programme fonctionne-t-il réellement? N'avez-vous pas besoin d'attendre un POLLHUPévénement pour gérer le cas vide? Cela fonctionne-t-il s'il y a plusieurs descriptions de fichiers à l'autre bout du canal?
Gilles, arrête de faire le mal.
Oui cela fonctionne. POLLHUP n’est retourné que par sondage; vous devez utiliser POLLIN pour attendre un POLLHUP. Peu importe le nombre de poignées ouvertes à chaque extrémité du tuyau.
Mosvy
Voir unix.stackexchange.com/search?q=FIONREAD+user%3A22565 pour savoir comment exécuter FIONREAD à partir de perl (plus communément disponible que les compilateurs)
Stéphane Chazelas
La routine qui utilise FIONREAD (ou HAVE_SELECT) est implémentée dans bash ici .
Isaac
2

Si vous aimez les one-liners cryptés et courts:

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

J'ai utilisé les exemples de la question initiale. Si vous ne voulez pas utiliser les données canalisées -qavec grep

Yashma
la source
0

Cela semble être une implémentation raisonnable de ifne dans bash si vous êtes d'accord pour lire toute la première ligne.

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
la source
5
readretournera également false si l'entrée n'est pas vide, mais ne contient aucun caractère de nouvelle ligne, readeffectue certains traitements sur son entrée et peut lire plusieurs lignes à moins que vous ne l'appeliez comme IFS= read -r line. echone peut pas être utilisé pour des données arbitraires.
Stéphane Chazelas
0

Cela fonctionne pour moi en utilisant read -rt 0

exemple de question originale, sans données:

echo "string" | tail -n+2 | if read -rt 0 ; then echo has data ; else echo no data ; fi
utilisateur333755
la source
non, ça ne marche pas. essayez avec { sleep .1; echo yes; } | { read -rt0 || echo NO; cat; }(faux négatif) et true | { sleep .1; read -rt0 && echo YES; }(faux positif). En fait, ce bash readsera dupé même par fds ouvert en écriture seule le mode: { read -rt0 && echo YES; cat; } 0>/tmp/foo. La seule chose qu'il semble faire est un select(2)fd.
Mosvy
... et selectretournera un fd en tant que "prêt" si un read(2)sur il ne bloquerait pas, peu importe s'il retournera EOFou une erreur. Conclusion: read -t0est cassé dans bash. Ne l'utilisez pas.
Mosvy
@mosvy Avez-vous signalé avec bashbug?
Isaac
@mosvy The { sleep .1; echo yes; } | { read -rt0 || echo NO; cat; }n'est pas un faux négatif car (au moment de l'exécution de la lecture), il n'y a pas d'entrée. Plus tard (sommeil .1), cette entrée est disponible (pour le chat).
Isaac
@mosvy Pourquoi l' option r affecte- t-elle la détection?: echo "" | { read -t0 && echo YES; }imprime OUI mais echo "" | { read -rt0 && echo YES; }ne le fait pas.
Isaac