web-dev-qa-db-fra.com

Comment détecter si mon script Shell s'exécute dans un tube?

Comment détecter à partir d'un script Shell si sa sortie standard est envoyée à un terminal ou si elle est redirigée vers un autre processus?

Le cas d'espèce: j'aimerais ajouter des codes d'échappement pour coloriser la sortie, mais uniquement lorsqu'il est exécuté de manière interactive, mais pas lorsqu'il est redirigé, comme le fait ls --color.

208
user111422

Dans un pur shell POSIX,

if [ -t 1 ] ; then echo terminal; else echo "not a terminal"; fi

renvoie "terminal", car la sortie est envoyée à votre terminal, alors que 

(if [ -t 1 ] ; then echo terminal; else echo "not a terminal"; fi) | cat

renvoie "pas un terminal", car la sortie de la parenthétique est dirigée vers cat.


L’indicateur -t est décrit dans les pages de manuel comme suit:

-t fd Vrai si le descripteur de fichier fd est ouvert et fait référence à un terminal.

... où fd peut être l'une des affectations de descripteur de fichier habituelles:

0:     stdin  
1:     stdout  
2:     stderr
314
dmckee

Il n'y a pas de moyen infaillible de déterminer si STDIN, STDOUT ou STDERR sont acheminés vers/depuis votre script, principalement à cause de programmes comme ssh.

Les choses qui "normalement" fonctionnent

Par exemple, la solution bash suivante fonctionne correctement dans un shell interactif:

[[ -t 1 ]] && \
    echo 'STDOUT is attached to TTY'

[[ -p /dev/stdout ]] && \
    echo 'STDOUT is attached to a pipe'

[[ ! -t 1 && ! -p /dev/stdout ]] && \
    echo 'STDOUT is attached to a redirection'

Mais ils ne travaillent pas toujours

Toutefois, lors de l'exécution de cette commande en tant que commande non-TTY ssh, STD diffuse toujours ressemble à ce qu'ils soient acheminés. Pour illustrer cela, utilisez STDIN car c'est plus facile:

# CORRECT: Forced-tty mode correctly reports '1', which represents
# no pipe.
ssh -t localhost '[[ -p /dev/stdin ]]; echo ${?}'

# CORRECT: Issuing a piped command in forced-tty mode correctly
# reports '0', which represents a pipe.
ssh -t localhost 'echo hi | [[ -p /dev/stdin ]]; echo ${?}'

# INCORRECT: Non-tty mode reports '0', which represents a pipe,
# even though one isn't specified here.
ssh -T localhost '[[ -p /dev/stdin ]]; echo ${?}'

Pourquoi c'est important

C'est un gros problème, car cela implique qu'il est impossible pour un script bash de dire si une commande non-tty ssh est en cours de piping. Notez que ce comportement malheureux a été introduit lorsque des versions récentes de ssh ont commencé à utiliser des canaux pour les systèmes non-TTY STDIO. Les versions précédentes utilisaient des sockets, qui POURRAIENT être différenciés de bash en utilisant [[ -S ]].

Quand ça compte

Cette limitation pose normalement des problèmes lorsque vous souhaitez écrire un script bash ayant un comportement similaire à un utilitaire compilé, tel que cat. Par exemple, cat autorise le comportement flexible suivant pour la gestion simultanée de plusieurs sources d'entrée et est suffisamment intelligent pour déterminer s'il reçoit une entrée canalisée, que la variable non TTY ou TTY forcée ssh soit utilisée ou non:

ssh -t localhost 'echo piped | cat - <( echo substituted )'
ssh -T localhost 'echo piped | cat - <( echo substituted )'

Vous ne pouvez faire quelque chose comme ça que si vous pouvez déterminer de manière fiable si des tuyaux sont impliqués ou non. Sinon, l'exécution d'une commande qui lit STDIN lorsqu'aucune entrée n'est disponible à partir des canaux ou d'une redirection entraîne le blocage du script et l'attente de l'entrée STDIN.

Autres choses qui ne fonctionnent pas

En essayant de résoudre ce problème, j'ai examiné plusieurs techniques qui échouent, notamment les suivantes:

  • examiner les variables d'environnement SSH
  • utiliser stat sur les descripteurs de fichier/dev/stdin
  • examen du mode interactif via [[ "${-}" =~ 'i' ]]
  • examiner le statut du terminal via tty et tty -s
  • examiner le statut ssh via [[ "$(ps -o comm= -p $PPID)" =~ 'sshd' ]]

Notez que si vous utilisez un système d'exploitation prenant en charge le système de fichiers virtuel /proc, vous aurez peut-être de la chance en suivant les liens symboliques de STDIO pour déterminer si un canal est utilisé ou non. Cependant, /proc n'est pas une solution multiplate-forme compatible POSIX.

Je suis extrêmement intéressé par la résolution de ce problème. Veuillez me faire savoir si vous pensez à une autre technique susceptible de fonctionner, de préférence des solutions POSIX fonctionnant à la fois sous Linux et BSD.

95
Dejay Clayton

La commande test (intégrée dans bash) a une option permettant de vérifier si un descripteur de fichier est un tty.

if [ -t 1 ]; then
    # stdout is a tty
fi

Voir "man test" ou "man bash" et recherchez "-t"

27
Beano

Vous ne mentionnez pas le shell que vous utilisez, mais dans Bash, vous pouvez faire ceci:

#!/bin/bash

if [[ -t 1 ]]; then
    # stdout is a terminal
else
    # stdout is not a terminal
fi
11
Dan Moulding

Sur Solaris, la suggestion de Dejay Clayton fonctionne principalement. Le -p ne répond pas comme souhaité. 

bash_redir_test.sh ressemble à:

[[ -t 1 ]] && \
    echo 'STDOUT is attached to TTY'

[[ -p /dev/stdout ]] && \
    echo 'STDOUT is attached to a pipe'

[[ ! -t 1 && ! -p /dev/stdout ]] && \
    echo 'STDOUT is attached to a redirection'

Sous Linux, cela fonctionne très bien:

:$ ./bash_redir_test.sh
STDOUT is attached to TTY

:$ ./bash_redir_test.sh | xargs echo
STDOUT is attached to a pipe

:$ rm bash_redir_test.log 
:$ ./bash_redir_test.sh >> bash_redir_test.log

:$ tail bash_redir_test.log 
STDOUT is attached to a redirection

Sur Solaris:

:# ./bash_redir_test.sh
STDOUT is attached to TTY

:# ./bash_redir_test.sh | xargs echo
STDOUT is attached to a redirection

:# rm bash_redir_test.log 
bash_redir_test.log: No such file or directory

:# ./bash_redir_test.sh >> bash_redir_test.log
:# tail bash_redir_test.log 
STDOUT is attached to a redirection

:# 
4
sbj3

Le code suivant (testé uniquement dans linux bash 4.4) ne doit pas être considéré comme portable ou recommandé , mais par souci d’exhaustivité, il est:

ls /proc/$$/fdinfo/* >/dev/null 2>&1 || grep -q 'flags: 00$' /proc/$$/fdinfo/0 && echo "pipe detected"

Je ne sais pas pourquoi, mais il semble que le descripteur de fichier "3" soit en quelque sorte créé lorsqu'une fonction bash a un canal STDIN.

J'espère que ça aide,

0
ATorras