web-dev-qa-db-ja.com

シェルスクリプトがパイプを介して実行されているかどうかを検出する方法は?

標準出力が端末に送信されている場合、または別のプロセスにパイプされている場合、シェルスクリプト内からどのように検出できますか?

適切な例:エスケープコードを追加して出力を色付けしたいのですが、ls --colorのように、インタラクティブに実行するときだけで、パイプでは実行しません。

232
user111422

純粋な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にパイプされるため、「not a terminal」を返します。


-tフラグは、manページで次のように説明されています

-t fdファイル記述子fdが開いており、端末を参照している場合は真です。

...ここで、fdは、通常のファイル記述子の割り当てのいずれかです。

0:     stdin  
1:     stdout  
2:     stderr
347
dmckee

主にsshなどのプログラムが原因で、STDIN、STDOUT、またはSTDERRがスクリプトとの間でパイプされているかどうかを判断するfoolproofの方法はありません。

「通常」機能するもの

たとえば、次の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ストリームalwaysはパイプされているように見えます。これを実証するために、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のパイプの使用を開始したときに導入されたことに注意してください。以前のバージョンではソケットを使用していましたが、[[ -S ]]を使用してbash内と区別できます。

重要なとき

通常、この制限は、catなどのコンパイルされたユーティリティに似た動作を持つbashスクリプトを作成するときに問題を引き起こします。たとえば、catは、さまざまな入力ソースを同時に処理する際に次の柔軟な動作を可能にし、非TTYまたは強制TTY sshが使用されているかどうかに関係なく、パイプ入力を受信して​​いるかどうかを判断するのに十分スマートです:

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

パイプが関与しているかどうかを確実に判断できる場合にのみ、そのようなことができます。そうしないと、パイプまたはリダイレクトからの入力がないときにSTDINを読み取るコマンドを実行すると、スクリプトがハングしてSTDIN入力を待機します。

動作しないその他のもの

この問題を解決するために、問題を解決できないいくつかの手法を検討しました。

  • sSH環境変数を調べる
  • / dev/stdinファイル記述子でstatを使用
  • [[ "${-}" =~ 'i' ]]を介してインタラクティブモードを調べる
  • ttyおよびtty -sを介してttyステータスを調べる
  • [[ "$(ps -o comm= -p $PPID)" =~ 'sshd' ]]を介してsshステータスを調べる

/proc仮想ファイルシステムをサポートするOSを使用している場合は、STDIOのシンボリックリンクをたどってパイプが使用されているかどうかを判断することができます。ただし、/procはクロスプラットフォームのPOSIX互換ソリューションではありません。

この問題を解決するのは非常に興味深いので、動作する可能性のある他の手法、できればLinuxとBSDの両方で動作するPOSIXベースのソリューションを考えている場合はお知らせください。

113
Dejay Clayton

コマンドtest(ビルトインbash)には、ファイル記述子がttyであるかどうかを確認するオプションがあります。

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

man test」または「man bash」を参照して、「-t」を検索してください

27
Beano

使用しているシェルについては言及しませんが、Bashではこれを行うことができます。

#!/bin/bash

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

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

:# 
4
sbj3

次のコード(linux bash 4.4でのみテスト済み)移植性も推奨もありませんですが、完全を期すために以下に示します。

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

理由はわかりませんが、bash関数にSTDINがパイプされると、ファイル記述子「3」が何らかの形で作成されるようです。

それが役に立てば幸い、

1
ATorras