やってみる

アウトプットすべく己を導くためのブログ。その試行錯誤すらたれ流す。

Pythonのstdinは値がないときに読むと永久待機してしまう

 sys.stdinを読んで値がなければブロッキングされて処理が進まない。バッチ処理できないためコマンドが作れない。

成果物

やりたいこと

echo -e "A\n\tB" | ./StdIn.py

 このときStdIn.pyは文字列A\n\tBを内部変数に取得したい。ただし、もし標準入力が与えられなかったときは、変数値を""またはNoneにしたい。

./StdIn.py

 StdIn.pyの内部変数stdinに値を代入する。

StdIn.py

stdin = 標準入力取得処理
assert(stdin == "A\n\tB")   # 標準入力あり
assert(stdin == "")         # 標準入力なし

 上記を実現できるような標準入力取得処理を求めている。だが、見つけられなかった……。

問題

 標準入力がないとき、バッチ処理が停止してしまう。

 sys.stdin.read()は標準入力がないとき、EOFが送られるまで永久に待機してしまう。つまり、標準入力がないとき、バッチ処理ができない!

期待値

 標準入力があろうが無かろうが、正しく標準入力と起動引数を取得した上でバッチ処理したい。

 たとえば、以下のようなコマンドパターンに対応できるものをPythonで書きたい。停止せずに。

./StdIn.py
./StdIn.py --some value
echo -e "A\n\tB" | ./StdIn.py
echo -e "A\n\tB" | ./StdIn.py --some value

やってみたがダメだったこと

失敗1

 標準入力があるときは成功する。

echo -e "A\n\tB" | python3 -c "import sys;[print(i.rstrip('\n')) for i in sys.stdin]" 
A
    B

 では、標準入力がない場合は?

python3 -c "import sys;[print(i.rstrip('\n')) for i in sys.stdin]" 
(永久に待機してしまう!)

 これを.pyファイルに書いて、shellで実行したとき、標準入力がなくても待機せず実行したい。

失敗2

StdIn.py

import sys
stdin = sys.stdin.read()
print(stdin)

 標準入力ありのときは成功する。

echo -e "A\n\tB" | ./StdIn.py
A
    B

 だが、標準入力がないときは永久に待機してしまう……。

$ ./StdIn.py
(永久に待機してしまう!)

 EOFが送られると例外発生して終了する。EOFは端末にてCtrl+C,Ctrl+D,Ctrl+Zなど環境に応じたキー入力で送信する。キーはOSや端末で違うと思われる。

 このEOFをPythonコード内で書き込んでやれば終了できる? だが以下の通り失敗した。

失敗3

 EOFを送るにはclose(), flush()する必要があるなどの情報があった。

import sys
sys.stdin.flush()
sys.stdin.close()
stdin = sys.stdin.read()
import sys
stdin = sys.stdin.read()
sys.stdin.flush()
sys.stdin.close()

 だが、いずれも標準入力がないときは待機してしまう……。

失敗4

 終了はするが、そもそもsys.stdinからは読んでいない。

 今回の問題の原因はPythonの実装にあるらしい。上記URLによるとPythonC言語実装において、stdinの読込で停止するよう実装されているのだとか。それを回避するのは以下コード。

shell.py

import sys
while True:
    s = raw_input("Enter command: ")
    print "You entered: {}".format(s)
    sys.stdout.flush()

client_O_NONBLOCK.py

#!/usr/bin/env python
# -*- coding: utf-8 -*-
from subprocess import Popen, PIPE
from time import sleep
from fcntl import fcntl, F_GETFL, F_SETFL
from os import O_NONBLOCK, read

# run the shell as a subprocess:
p = Popen(['python', 'shell.py'], stdin = PIPE, stdout = PIPE, stderr = PIPE, shell = False)
# set the O_NONBLOCK flag of p.stdout file descriptor:
flags = fcntl(p.stdout, F_GETFL) # get current p.stdout flags
fcntl(p.stdout, F_SETFL, flags | O_NONBLOCK)
# issue command:
p.stdin.write('command\n')
# let the shell output the result:
sleep(0.1)
# get the output
while True:
    try:
        print read(p.stdout.fileno(), 1024),
    except OSError:
        # the os throws an exception if there is no data
        print '[No more data]'
        break

 実行してみる。

chmod +x client_O_NONBLOCK.py
./client_O_NONBLOCK.py

 結果は以下。

Enter command: You entered: command
[No more data]

 ちゃんと終了した。

 しかし、端末の標準入力から値を得ているわけではない。以下の部分から入力値を得ている。

client_O_NONBLOCK.py

p.stdin.write('command\n')

 これでは意味がない。

 やりたいことは以下だったのだが、標準入力を読み取るコードをかいていないため無視される……。

echo -e "A\n\tB" | ./client_O_NONBLOCK.py
Enter command: You entered: command
[No more data]

 つまり、入力値をPythonコードで書かねばならない……。Pythonコードのファイルを編集せねばならない……。入力が固定などという応用の効かないコマンドなんぞ使い物にならん……。

 だからといって、標準入力を読み取るコードを書いてしまうと永久待機してしまう……。

client_O_NONBLOCK.py

#p.stdin.write('command\n')
import sys
p.stdin.write(sys.stdin.read())
(永久待機……)

 この問題をどうにかしたいのが本題なのに……。どうあっても解決不能らしい。

 複数行の読込はできた。だが、末尾に改行がなければ最終行が読み込まれない……。

client_O_NONBLOCK.py

#p.stdin.write('command\n')
p.stdin.write('A\n\tB\n')
Enter command: You entered: A
Enter command: You entered:     B
[No more data]

 なお、このサイトにあるスレッド版を使っても同様だった。キモはp.stdin.write('command\n')sys.stdin.read()を使わずにそれをシミュレートしている点である。sys.stdinを読み取るとき、どうしてもブロッキングが発生するのだから仕方ない。

 この方法では、端末に入力されたstdinを取得できない。

 つまりこの方法には以下の問題がある。

  • bashecho -e "A\n\tB" | ./client_O_NONBLOCK.pyでの標準入力は取得できない
    • 標準入力はPythonコードp.stdin.write('A\n\tB\n')で書かねばならない
  • 入力値の末尾に\n必須
    • さもなくばその行が読み取られない

Pythonにおけるstdinの未解決問題

  • 標準入力がないとき永久待機してしまう

 標準入力を読む方法は以下のようにいくつかある。だが、それらすべてにおいて同じと思われる。

  • sys.stdin.read()
  • sys.stdin.readline()
  • input_raw()

 また、Python3でも同様にsys.stdin.read()ブロッキングが発生するのを確認した。

所感

 なんとかならんの?

対象環境

$ uname -a
Linux raspberrypi 4.19.75-v7l+ #1270 SMP Tue Sep 24 18:51:41 BST 2019 armv7l GNU/Linux