リダイレクションやパイプと、端末の入力を同時に行う方法

お題

リダイレクションやパイプからデータを読み込んだ上で端末の入力を読み込む、
そんなプログラムを書こうとしてはまったので整理。

イメージ:

$ echo hoge foo bar | ./to_upper_case_and_xxx.py
HOGE FOO BAR
input:  <- 端末からなにか入力

この場合、次のように書きたくなります。

import sys

# 標準入力をすべて読み込む
for line in sys.stdin:
    sys.stdout.write(line.upper())

# 端末の入力を読み込む
result = raw_input('input: ')
print '>> ' + result

しかし、EOFErrorが発生してうまくいきません。

$ echo hoge | ./raw_input_test.py 
...
EOFError: EOF when reading a line

raw_input()が内部でsys.stdinを参照しているので、読みきってると動かないわけですね。
それに読み込み元がリダイレクションやパイプになっているため、このままではインタラクティブな操作はできません。

ではどうするか

実行例 標準入力
通常 ./program.py /dev/tty
パイプ echo hoge | ./program.py echoの出力
リダイレクション ./program.py < path/to/file /path/to/file

通常、標準入力は/dev/ttyになっており、端末の入力はこの特殊なファイルから読み込んでいます。
これが、パイプやリダイレクションを利用した場合、別プログラムの出力やファイルに差し替わります。

/dev/ttyをオープンすれば端末からの入力を受け取れるので、sys.stdinに割り当ててみましょう。

修正版

というわけで、次のように変更します。

import os
import sys

# 標準入力をすべて読み込む
for line in sys.stdin:
    sys.stdout.write(line.upper())

# TTY (/dev/tty) をオープンして、標準入力に割り当てる
sys.stdin = file('/dev/tty')

# TTYを割り当てたので、端末の入力を読み込める
result = raw_input('input: ')
print '>> ' + result

今度はちゃんと動作しますね。

もちろん、raw_input()を使わずに直接TTYから読み込んでもOKです。

tty = file(os.ctermid())
sys.stdout.write('input: ')
result = tty.readline()
print '>> ' + result

[python][unix] リダイレクションやパイプの有無を識別する方法

リダイレクションやパイプを使用している場合は読み込み、使用してない場合はヘルプを表示するなど、
切り替えたい場合は次のように書くのがいいのかな。

import os
import sys

if os.isatty(os.sys.stdin.fileno()):
    print_usage()
    sys.exit(1)

# 標準入力をすべて読み込む
for line in sys.stdin:
    sys.stdout.write(line)