Как определить, работает ли моя оболочка script через канал?

Как я могу обнаружить из оболочки script, если ее стандартный вывод предназначен для терминала или если он подключен к другому процессу? (В данном случае я хотел бы добавить escape-коды для раскраски вывода, но только при интерактивном запуске, но не в том случае, когда используется канал, аналогично тому, что делает ls --color.)

Ответ 1

В чистой оболочке 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 описывается в man-страницах как

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

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

0:     stdin  
1:     stdout  
2:     stderr

Ответ 2

Нет надежного способа определить, передаются ли STDIN, STDOUT или STDERR в/из вашего script, в основном из-за таких программ, как 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 script указать, передается ли команда non-tty ssh или нет. Обратите внимание, что это неудачное поведение было введено, когда последние версии ssh начали использовать трубы для не-TTY STDIO. В предыдущих версиях использовались сокеты, которые МОГУТ быть дифференцированы из bash с помощью [[ -S ]].

Когда это имеет значение

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

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

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

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

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

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

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

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

Ответ 3

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

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

См. "man test" или "man bash" и выполните поиск "-t"

Ответ 4

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

#!/bin/bash

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

Ответ 5

В Solaris предложение от Dejay Clayton работает в основном. -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

В 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

:#