Pythonで外部コマンドのstdinを制御してみた

シェルの外部コマンドの中には、コマンドの途中で端末からの入力を促すコマンド(su とか)があるが、 この入力内容をPython等から制御できると色々と便利そうだ。。。

今思えば、Pexpect ( http://de.sourceforge.jp/projects/sfnet_pexpect/ ) を使えば簡単だったのだが、PexpectはSSH等でしか使えないと思っていたので、Python標準モジュールで悪戦苦闘してしまった。。(というか最初はbashスクリプトで書けないかを調べていた。今思えば無謀だった。。)

ちなみに、su - を行った後、id コマンドを使い、ログアウトするスクリプトを Pexpectで書くとこんな感じになる。

from pexpect  import *
c=spawn('su',['-'])
c.expect (':')
c.sendline('xxx')
c.expect ('#')
c.sendline('id')
c.sendline('exit')
print c.read()

xxx のところはパスワードを入力する。結果は次のようになる。

$ python ddd.py 
 id
uid=0(root) gid=0(root) groups=0(root)
# exit
logout

さて、結論から進むと、特に難しいことは無いのだが、確認を始めた当初、" suを打つttyを調べておいて bash -c "sleep 5; echo password > $(tty) " & みたいな文を組み合わせたスクリプト実行できるのでは"、と考えてしまい長時間ハマった。

結論だけいうと、この方法は上手くいかないらしい。(少なくとも筆者が試した限りでは) どうも、tty 端末はフォアグラウンドにあるプロセスからの書込みと、バックグラウンドからの書込みを区別するらしく、実際にsuを打った端末からの書込み以外はsuのコマンドに伝わらないらしいのだ。。。詳しい理由は結局よく分からなかったのだが、、、

この後、bashスクリプトでの実施を諦め、長時間Pythonの標準モジュールと戯れた後、ようやく上手くいったスクリプトは次のようになる。 うまくいってよかったが、次回以降はPexpectを使おう。。

import fcntl, termios, os,threading,time
st='xxx\n'   <=== xxxに、パスワードを記述
def aaa():
 time.sleep(5)
 for a in st:
  fcntl.ioctl(0,termios.TIOCSTI, a)
a= threading.Thread(target=aaa)
a.start()
os.system('su - -c "id" ')

結果はこんな感じである。

$ python ccc.py 
パスワード: 
uid=0(root) gid=0(root) groups=0(root)

スクリプトの方は色々と大変なことになっているが、ttyデバイスのioctl のうち、 TIOCSTI ( 端末の偽装 (Faking Input) 詳しくはmanを参照。 http://www.linux.or.jp/JM/html/LDP_man-pages/man4/tty_ioctl.4.html )
を使って、無理やりフォアグラウンドのプロセスからの書込みのように見せかけて追記を行うのがミソとなる。

su - を実施した後に並行して書込みを行う必要があったため、途中Thread を使っている。
また、su - は書込みとして1回(パスワード)しか取らないため、比較的対応しやすいのだが、複数回の書込みを取るような外部コマンドに対して(上手く動かせるかどうかわからないが、 yast など??)、 sleep 値を変えながら順次書込みを行うことでおそらく制御できそうだ。

尚、対象のioctlについて色々試してみたのだが、最後の引数は1文字ずつしか指定できないらしく、for文で順次パスワードを与える必要がある。


ttyが色々と難しいことは認識していたが、まさかちょっとしたことでioctlを使うハメになるとは思わなかった。今後はうっかり深みにはまらないよう気をつけよう。。