pythonで簡易コンソールを作る

pythonで簡易コンソールを作る機会があったのでメモしておく。

コンソールを作る場合、以下のように標準入力でループさせて処理させる方法がある。
参考:http://python.g.hatena.ne.jp/mhrs/20060522/p2

#!/usr/bin/env python
# -*- coding:utf-8 -*-
#
# easy_console.py
#

import sys

sys.stdout.write(">> ")
for line in iter(sys.stdin.readline, ""):
    if line.strip() != "":
        sys.stdout.write(line)
    sys.stdout.write(">> ")
sys.stdout.write("\n")

$ python easy_console.py
>>
>> hoge
hoge
>> fuga
fuga
>> ^D

一方で、pythonには標準でコンソールがあり、それをうまく流用できないか考えてみる。
調べてみると、コンソールをエミュレートするcodeというモジュールがあるらしい。
http://www.python.jp/doc/release/library/code.html

codeでは

の2つが定義されている。

例えば、以下の様なコードを書くとコンソールもどきが起動できる。

#!/usr/bin/env python
# -*- coding:utf-8 -*-
#
# code_console.py
#

import code

console = code.InteractiveConsole()
console.interact()

$ python codo_consoele.py
Python 2.7.1 (r271:86832, Jun 25 2011, 05:09:01)
[GCC 4.2.1 (Based on Apple Inc. build 5658) (LLVM build 2335.15.00)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> 1 + 1 * 2
3
>>> for i in range(3):
... print i
...
0
1
2
>>> ^D

デフォルトではpythonインタプリタとして動作する。これは、InteractiveConsoleのpushメソッドでInteractiveInterpretarに入力した文字列を渡しているから。
pushをオーバライドすることで独自処理が実装できる。

#!/usr/bin/env python
# -*- coding:utf-8 -*-
#
# my_console.py
#

import sys
import code

class MyConsole(code.InteractiveConsole):
    def __init__(self, local=None, filename="<console>"):
        code.InteractiveConsole.__init__(self, local, filename)
    
    def push(self, line):
        if line == "nullpo":
            print "ga!!!"

my_console = MyConsole()
sys.ps1 = "(test)>> "
sys.ps2 = "------>> "
my_console.interact("### welcome to my console!!! ###")

$ python my_console.py
### welcome to my console!!! ###
(test)>> hoge
(test)>> nullpo
ga!!!
(test)>> ^D

ところで、InteractiveConsoleをそのまま使うと上下とかでコマンド履歴を呼び出す機能がない。
pythonの標準コンソールでは出来るので、"python -vvv"とかして調べてたら以下のドキュメントを見つけた。
http://www.python.jp/doc/release/library/readline.html

早速拡張してみる。

#!/usr/bin/env python
# -*- coding:utf-8 -*-
#
# my_console.py
#

import sys
import code
import readline
import atexit
import os

class MyConsole(code.InteractiveConsole):
    def __init__(self, local=None, filename="<console>",
                 histfile=os.path.expanduser("~/.console-history")):
        code.InteractiveConsole.__init__(self, local, filename)
        self.init_history(histfile)

    def init_history(self, histfile):
        readline.parse_and_bind("tab: complete")
        if hasattr(readline, "read_history_file"):
            try:
                readline.read_history_file(histfile)
            except IOError:
                pass
            atexit.register(self.save_history, histfile)

    def save_history(self, histfile):
        readline.write_history_file(histfile)
    
    def push(self, line):
        if line == "nullpo":
            print "ga!!!"

my_console = MyConsole()
sys.ps1 = "(test)>> "
sys.ps2 = "------>> "
my_console.interact("### welcome to my console!!! ###")

これで上下での履歴の呼び出しができるようになる。