Як визначити, чи мій скрипт оболонки працює через трубу?


252

Як я можу виявити зсередини скрипт оболонки, якщо його стандартний вихід надсилається до терміналу або якщо він передається в інший процес?

Справжній випадок: я хотів би додати коди евакуації, щоб розфарбувати вихід, але лише тоді, коли вони запускаються в інтерактивному режимі, але не в трубопроводі, як ls --colorце робиться.


2
Ось ще кілька цікавих тестових випадків! <a href=" serverfault.com/questions/156470/… для сценарію, який очікує на stdin</a>

2
@ user940324 Правильним посиланням є serverfault.com/q/156470/197218
Palec,

Відповіді:


385

У чистої оболонці 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 True, якщо дескриптор файлу fd відкритий і посилається на термінал.

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

0:     stdin  
1:     stdout  
2:     stderr

1
@Kelvin Фрагмент сторінки man пропонує, що він повинен бути, але дескриптори файлів не призначені за замовчуванням.
dmckee --- кошеня колишнього модератора

41
Для уточнення, -tпрапор вказаний в POSIX, і тому він повинен працювати для будь-якої сумісної з POSIX оболонки (тобто це не розширення bash). pubs.opengroup.org/onlinepubs/009695399/utilities/test.html
FireFly

Працює і під час запуску скрипту як віддаленої команди ssh. Найкраща відповідь коли-небудь і дуже проста.
linux_newbie

Я погоджуюсь, що після редагування (редакція 5) відповідь чіткіша, ніж у редакції 3, а також фактично правильна (ігноруючи, що "повернення" використовується дуже неофіційно, де "друк" буде точнішим).
Palec

Шукав fishвідповідь на оболонку. Використання testакуратно, але я не можу спробувати приклади в скобках, оскільки це не підтримується. Спробувавши обернути його в аналог begin; ...; end, але це, здавалося, не спрацювало, і просто запустив позитивний блок коду знову. Думав, що мені може знадобитися використовувати, statusале це, схоже, не перевіряє наявність трубопроводів. Я думаю, я по суті хочу перевірити, чи STDOUT попередньої команди / скрипту не встановлений на термінал, завдяки цим уточнюючим відповідям.
Pysis

126

Не існує надійного способу визначити, чи 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 немає способу сказати, чи є нетипізована sshкоманда в трубі чи ні. Зауважте, що ця нещасна поведінка була введена, коли останні версії sshпочали використовувати труби для не TTY STDIO. У попередніх версіях використовувалися сокети, які МОЖЕ бути диференційовані з башти за допомогою використання [[ -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дескрипторів файлів on / dev / stdin
  • вивчення інтерактивного режиму через [[ "${-}" =~ 'i' ]]
  • вивчення статусу tty через ttyтаtty -s
  • вивчення sshстатусу через[[ "$(ps -o comm= -p $PPID)" =~ 'sshd' ]]

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

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


2
Чітка перевірка змінних оточуючих середовищ або імен процесів є дуже ненадійною евристикою. Але ви могли б трохи розширити, чому інші евристики непридатні для цієї мети або в чому полягає їхня проблема? Наприклад, я не бачу різниці у виході statвиклику на / dev / stdin. І чому це працює "${-}"чи tty -sні? Я також заглянув у вихідний код, catале не бачу, яка частина займається магією, яку ви не можете зробити в оболонці POSIX. Чи можете ви розширити це?
Джош

30

Команда test(вбудована bash) має можливість перевірити, чи є дескриптор файлу tty.

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

Дивіться " man test" або " man bash" та шукайте " -t"


3
+1 для "тесту на людину", оскільки / usr / bin / test буде працювати навіть у оболонці, яка не реалізує -t у своєму тесті на вбудований
Ніл Мейхю

4
Як зазначає FireFly у відповіді dmckee, оболонка, яка не реалізує -t, не відповідає POSIX.
scy

Дивіться також вбудований bash's help test(та help helpбільше), а потім info bashдля отримання більш поглибленої інформації. Ці команди чудові, якщо ви коли-небудь закінчите сценарій в режимі офлайн або просто хочете отримати більш широке розуміння.
Джоел Пурра

13

Ви не згадуєте, яку оболонку використовуєте, але в Bash ви можете це зробити:

#!/bin/bash

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

6

Щодо 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

:# 

Цікаво, хотілося б, щоб я мав доступ до Solaris для тестування. Якщо ваш екземпляр Solaris використовує файлову систему "/ proc", існують більш надійні рішення, які передбачають пошук символічних посилань "/ proc" для stdin, stdout та stderr.
Dejay Clayton

1

Наступний код (перевірений лише у Linux bash 4.4) не слід вважати переносним і не рекомендується , але заради повноти тут він є:

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

Я не знаю чому, але здається, що дескриптор файлу "3" якимось чином створюється, коли у функції bash є STDIN.

Сподіваюся, це допоможе,

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.