Как определить, работает ли мой сценарий оболочки через канал?

Как определить из сценария оболочки, отправляется ли его стандартный вывод на терминал или он передается другому процессу?

Показательный пример: я хотел бы добавить escape-коды для раскрашивания вывода, но только при интерактивном запуске, а не при передаче по конвейеру, аналогично тому, что ls --color делает.

6 ответов

Решение

В чистой оболочке POSIX,

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

возвращает "терминал", потому что вывод отправляется на ваш терминал, тогда как

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

возвращает "не терминал", потому что вывод скобок передается cat,


-t флаг описывается на страницах руководства как

-t fd Истинно, если дескриптор файла fd открыт и ссылается на терминал.

... где fd может быть одним из обычных назначений дескриптора файла:

0:     stdin  
1:     stdout  
2:     stderr

Не существует надежного способа определить, передаются ли STDIN, STDOUT или STDERR в / из вашего сценария, в основном из-за таких программ, как ssh,

Вещи, которые "нормально" работают

Например, следующее решение bash правильно работает в интерактивной оболочке:

[[ -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'

Но они не всегда работают

Однако при выполнении этой команды как не TTY ssh Команда STD-потоков всегда выглядит так, как будто они передаются по каналу. Чтобы продемонстрировать это, используйте STDIN, потому что это проще:

# 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 ${?}'

Почему это важно

Это довольно большое дело, потому что это означает, что сценарий bash не может определить, является ли не-tty ssh Команда передается по каналу или нет. Обратите внимание, что это неудачное поведение было введено, когда последние версии ssh начал использовать трубы для не-TTY STDIO. В предыдущих версиях использовались сокеты, которые МОГУТ дифференцироваться изнутри bash с помощью [[ -S ]],

Когда это важно

Это ограничение обычно вызывает проблемы, когда вы хотите написать bash-скрипт, который ведет себя подобно скомпилированной утилите, такой как cat, Например, cat допускает следующее гибкое поведение при одновременной обработке различных источников ввода и достаточно умен, чтобы определить, получает ли он ввод по каналу независимо от того, является ли он не TTY или принудительным TTY ssh используется:

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

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

Другие вещи, которые не работают

Пытаясь решить эту проблему, я рассмотрел несколько методов, которые не смогли решить проблему, в том числе:

  • изучение переменных среды SSH
  • с помощью stat в /dev/stdin файловых дескрипторах
  • изучение интерактивного режима через [[ "${-}" =~ 'i' ]]
  • проверка статуса tty через tty а также tty -s
  • изучения ssh статус через [[ "$(ps -o comm= -p $PPID)" =~ 'sshd' ]]

Обратите внимание, что если вы используете ОС, которая поддерживает /proc Виртуальная файловая система, вам может повезти, пройдя по символическим ссылкам для STDIO, чтобы определить, используется ли канал или нет. Тем не мение, /proc не является кроссплатформенным, POSIX-совместимым решением.

Я чрезвычайно заинтересован в решении этой проблемы, поэтому, пожалуйста, дайте мне знать, если вы думаете о какой-либо другой технике, которая могла бы работать, предпочтительно решениях на основе POSIX, которые работают как на Linux, так и на BSD.

Команда test (встроенный в bash), имеет возможность проверить, является ли дескриптор файла tty.

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

Увидеть "man test" или же "man bash"и искать"-t"

Вы не упоминаете, какую оболочку вы используете, но в Bash вы можете сделать это:

#!/bin/bash

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

На Солярисе предложение от Деджея Клейтона работает в основном. -P не отвечает как требуется.

bash_redir_test.sh выглядит так:

[[ -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'

В Linux это прекрасно работает:

:$ ./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

На Солярисе:

:# ./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

:# 

Следующий код (протестированный только в linux bash 4.4) не следует рассматривать как переносимый и не рекомендуемый, но для полноты здесь он таков:

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

Я не знаю почему, но кажется, что файловый дескриптор "3" каким-то образом создается, когда функция bash имеет канал STDIN.

Надеюсь, поможет,

Другие вопросы по тегам