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
.
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
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
.
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'
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 ${?}'
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 ]]
.
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.
En essayant de résoudre ce problème, j'ai examiné plusieurs techniques qui échouent, notamment les suivantes:
stat
sur les descripteurs de fichier/dev/stdin[[ "${-}" =~ 'i' ]]
tty
et tty -s
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.
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
"
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
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
:#
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,