readline – GNU readline ライブラリのインタフェース

目的:コマンドプロンプトでユーザと対話的にやり取りするための GNU readline ライブラリのインタフェースを提供する
利用できるバージョン:1.4 以上

readline モジュールは、簡単に対話的なコマンドラインプログラムを拡張するために使用できます。主にコマンドラインのテキスト補完、または “タブ補完” に使用されます。

Note

readline はコンソールコンテンツを対話的にやり取りするので、デバッグメッセージを表示すると readline が普通に動作しているのと比べて、サンプルコードで何が起こっているかが分かり難くなります。次のサンプルでは、デバッグ情報を別ファイルへ書き込むのに logging を使用します。ログ出力はサンプルごとに表示します。

設定

readline ライブラリを有効にする設定方法は、設定ファイルか parse_and_bind() 関数を使用するかの2通りあります。設定オプションは、補完処理を実行するキーバインド、編集モード(vi や emacs)、他にも多くのオプションがあります。詳細は GNU readline ライブラリドキュメント を参照してください。

タブ補完を有効にする最も簡単な方法は、 parse_and_bind() を呼び出すことです。その他のオプションも同時に設定できます。このサンプルは、編集制御をデフォルトの “emacs” ではなく “vi” モードに変更します。カレントの入力行を編集するには、 ESC を押してから j, k, l, h といった通常の vi ナビゲーションキーを使用します。

import readline

readline.parse_and_bind('tab: complete')
readline.parse_and_bind('set editing-mode vi')

while True:
    line = raw_input('Prompt ("stop" to quit): ')
    if line == 'stop':
        break
    print 'ENTERED: "%s"' % line

同じ設定は、ライブラリが1回の呼び出しでファイルを読み込んで設定できます。もし myreadline.rc が、

# Turn on tab completion
tab: complete

# Use vi editing mode instead of emacs
set editing-mode vi

を含む場合、 read_init_file() で読み込みます。

import readline

readline.read_init_file('myreadline.rc')

while True:
    line = raw_input('Prompt ("stop" to quit): ')
    if line == 'stop':
        break
    print 'ENTERED: "%s"' % line

テキストを補完する

コマンドライン補完を構築する方法の1つとして、あるプログラムの実行可能な組み込みのコマンドセットを調べられます。ユーザーがその命令を入力するとタブ補完を使用します。

import readline
import logging

LOG_FILENAME = '/tmp/completer.log'
logging.basicConfig(filename=LOG_FILENAME,
                    level=logging.DEBUG,
                    )

class SimpleCompleter(object):
    
    def __init__(self, options):
        self.options = sorted(options)
        return

    def complete(self, text, state):
        response = None
        if state == 0:
            # このテキストは初めてなのでマッチリストを作成する
            if text:
                self.matches = [s 
                                for s in self.options
                                if s and s.startswith(text)]
                logging.debug('%s matches: %s', repr(text), self.matches)
            else:
                self.matches = self.options[:]
                logging.debug('(empty input) matches: %s', self.matches)
        
        # たくさんある場合、マッチリストから state 番目の値を返す
        try:
            response = self.matches[state]
        except IndexError:
            response = None
        logging.debug('complete(%s, %s) => %s', 
                      repr(text), state, repr(response))
        return response

def input_loop():
    line = ''
    while line != 'stop':
        line = raw_input('Prompt ("stop" to quit): ')
        print 'Dispatch %s' % line

# 補完関数を登録する
readline.set_completer(SimpleCompleter(['start', 'stop', 'list', 'print']).complete)

# 補完に tab キーを使用する
readline.parse_and_bind('tab: complete')

# ユーザへテキストを表示する
input_loop()

input_loop() 関数は、入力された値が "stop" になるまで単純に一行読み込みます。より洗練されたプログラムは、実際に入力行を解析してコマンドを実行します。

SimpleCompleter クラスは、自動補完の候補となる “options” のリストを保持します。インスタンスの complete() メソッドは、補完ソースとして readline に登録されるように設計されています。その引数は、補完する “text” 文字列と、同じテキストで何回 complete() が呼び出されたかを示す “state” の値です。この関数は呼び出される毎に “state” を追加します。”state” の値の候補がある場合は文字列を、候補がない場合は None を返します。この complete() の実装は、”state” が 0 のときの matches を調べてから、その後の処理でマッチする全ての候補を返します。

実行すると、最初の出力は次のようになります。

$ python readline_completer.py
Prompt ("stop" to quit):

TAB を2回押すと、オプションのリストが表示されます。

$ python readline_completer.py
Prompt ("stop" to quit):
list   print  start  stop
Prompt ("stop" to quit):

ログファイルには、 complete() が state 値をもつ2つの別々のシーケンスで呼び出されたことが出力されます。

$ tail -f /tmp/completer.log
DEBUG:root:(empty input) matches: ['list', 'print', 'start', 'stop']
DEBUG:root:complete('', 0) => 'list'
DEBUG:root:complete('', 1) => 'print'
DEBUG:root:complete('', 2) => 'start'
DEBUG:root:complete('', 3) => 'stop'
DEBUG:root:complete('', 4) => None
DEBUG:root:(empty input) matches: ['list', 'print', 'start', 'stop']
DEBUG:root:complete('', 0) => 'list'
DEBUG:root:complete('', 1) => 'print'
DEBUG:root:complete('', 2) => 'start'
DEBUG:root:complete('', 3) => 'stop'
DEBUG:root:complete('', 4) => None

1回目の TAB キー入力で最初のシーケンスが出力されます。この補完アルゴリズムは、全ての候補を問い合わせますが、空の入力行は展開しません。それから2回目の TAB で、候補のリストが再計算されてユーザへ表示されます。

もし次に “l” を入力して TAB を再入力すると、画面は次のようになります。

Prompt ("stop" to quit): list

ログには complete() へ別の引数が出力されます。

DEBUG:root:'l' matches: ['list']
DEBUG:root:complete('l', 0) => 'list'
DEBUG:root:complete('l', 1) => None

RETURN キーを押すと、 raw_input() で入力した値を返して while ループ処理が繰り返されます。

Dispatch list
Prompt ("stop" to quit):

s” で始まるコマンドの補完候補が2つあります。”s” を入力した後で TAB キーを押すと、 “start” と “stop” が候補として見つかりますが、画面上では “t” のみを追加してテキスト補完が完了します。

ログファイルには次のように出力されます。

DEBUG:root:'s' matches: ['start', 'stop']
DEBUG:root:complete('s', 0) => 'start'
DEBUG:root:complete('s', 1) => 'stop'
DEBUG:root:complete('s', 2) => None

そして、画面は次のように出力されます。

Prompt ("stop" to quit): st

Warning

補完関数が例外を発生させる場合、 readline は補完対象がなかったと仮定してその例外を無視します。

補完バッファへアクセスする

前節の補完アルゴリズムは単純なもので、関数に渡されたテキスト引数を調べるのみですが、readline 内部の状態を特に使用しません。入力バッファのテキストを扱うために readline の関数も使用できます。

import readline
import logging

LOG_FILENAME = '/tmp/completer.log'
logging.basicConfig(filename=LOG_FILENAME,
                    level=logging.DEBUG,
                    )

class BufferAwareCompleter(object):
    
    def __init__(self, options):
        self.options = options
        self.current_candidates = []
        return

    def complete(self, text, state):
        response = None
        if state == 0:
            # このテキストは初めてなのでマッチリストを作成する
            
            origline = readline.get_line_buffer()
            begin = readline.get_begidx()
            end = readline.get_endidx()
            being_completed = origline[begin:end]
            words = origline.split()

            logging.debug('origline=%s', repr(origline))
            logging.debug('begin=%s', begin)
            logging.debug('end=%s', end)
            logging.debug('being_completed=%s', being_completed)
            logging.debug('words=%s', words)
            
            if not words:
                self.current_candidates = sorted(self.options.keys())
            else:
                try:
                    if begin == 0:
                        # 最初の単語
                        candidates = self.options.keys()
                    else:
                        # 後続の単語
                        first = words[0]
                        candidates = self.options[first]
                    
                    if being_completed:
                        # 補完される入力部をもつオプションにマッチする
                        self.current_candidates = [ w for w in candidates
                                                    if w.startswith(being_completed) ]
                    else:
                        # 空の文字列がマッチしたので全ての候補を使用する
                        self.current_candidates = candidates

                    logging.debug('candidates=%s', self.current_candidates)
                    
                except (KeyError, IndexError), err:
                    logging.error('completion error: %s', err)
                    self.current_candidates = []
        
        try:
            response = self.current_candidates[state]
        except IndexError:
            response = None
        logging.debug('complete(%s, %s) => %s', repr(text), state, response)
        return response
            

def input_loop():
    line = ''
    while line != 'stop':
        line = raw_input('Prompt ("stop" to quit): ')
        print 'Dispatch %s' % line

# 補完関数を登録する
readline.set_completer(BufferAwareCompleter(
    {'list':['files', 'directories'],
     'print':['byname', 'bysize'],
     'stop':[],
    }).complete)

# 補完に tab キーを使用する
readline.parse_and_bind('tab: complete')

# ユーザへテキストを表示する
input_loop()

このサンプルでは、サブオプションをもつコマンドが補完されます。 complete() メソッドは、最初の単語、または後続の単語の一部かどうかを判断するために入力バッファ内の補完位置を探す必要があります。その対象が最初の単語なら、 options ディクショナリのキーが候補として使用されます。もし最初の単語はない場合、その後でその単語が options ディクショナリから候補を見つけるのに使用されます。

2つのサブコマンドをもつ3つのトップレベルのコマンドがあります。

  • list
    • files
    • directories
  • print
    • byname
    • bysize
  • stop

前節同様に同じシーケンスで、TAB キーを押すと3つのトップレベルのコマンドが表示されます。

$ python readline_buffer.py
Prompt ("stop" to quit):
list   print  stop
Prompt ("stop" to quit):

ログです。

DEBUG:root:origline=''
DEBUG:root:begin=0
DEBUG:root:end=0
DEBUG:root:being_completed=
DEBUG:root:words=[]
DEBUG:root:complete('', 0) => list
DEBUG:root:complete('', 1) => print
DEBUG:root:complete('', 2) => stop
DEBUG:root:complete('', 3) => None
DEBUG:root:origline=''
DEBUG:root:begin=0
DEBUG:root:end=0
DEBUG:root:being_completed=
DEBUG:root:words=[]
DEBUG:root:complete('', 0) => list
DEBUG:root:complete('', 1) => print
DEBUG:root:complete('', 2) => stop
DEBUG:root:complete('', 3) => None

最初の単語が "list " (単語の後ろにスペース) なら、その補完候補が違います。

Prompt ("stop" to quit): list
directories  files

ログには、テキスト being_completed は全行では ありません が、その一部は後にあります。

DEBUG:root:origline='list '
DEBUG:root:begin=5
DEBUG:root:end=5
DEBUG:root:being_completed=
DEBUG:root:words=['list']
DEBUG:root:candidates=['files', 'directories']
DEBUG:root:complete('', 0) => files
DEBUG:root:complete('', 1) => directories
DEBUG:root:complete('', 2) => None
DEBUG:root:origline='list '
DEBUG:root:begin=5
DEBUG:root:end=5
DEBUG:root:being_completed=
DEBUG:root:words=['list']
DEBUG:root:candidates=['files', 'directories']
DEBUG:root:complete('', 0) => files
DEBUG:root:complete('', 1) => directories
DEBUG:root:complete('', 2) => None

入力履歴

readline は自動的に入力履歴を記録します。履歴を扱う2つの関数セットがあります。カレントセッションの履歴は get_current_history_length()get_history_item() でアクセスできます。それと同じ履歴は write_history_file()read_history_file() で、後からも再読み込みできるファイルに保存できます。デフォルトでは、全ての履歴がファイルに保存されますが、ファイルの最大長は set_history_length() でセットできます。 -1 の長さを指定すると無制限になります。

import readline
import logging
import os

LOG_FILENAME = '/tmp/completer.log'
HISTORY_FILENAME = '/tmp/completer.hist'

logging.basicConfig(filename=LOG_FILENAME,
                    level=logging.DEBUG,
                    )

def get_history_items():
    return [ readline.get_history_item(i)
             for i in xrange(1, readline.get_current_history_length() + 1)
             ]

class HistoryCompleter(object):
    
    def __init__(self):
        self.matches = []
        return

    def complete(self, text, state):
        response = None
        if state == 0:
            history_values = get_history_items()
            logging.debug('history: %s', history_values)
            if text:
                self.matches = sorted(h 
                                      for h in history_values 
                                      if h and h.startswith(text))
            else:
                self.matches = []
            logging.debug('matches: %s', self.matches)
        try:
            response = self.matches[state]
        except IndexError:
            response = None
        logging.debug('complete(%s, %s) => %s', 
                      repr(text), state, repr(response))
        return response

def input_loop():
    if os.path.exists(HISTORY_FILENAME):
        readline.read_history_file(HISTORY_FILENAME)
    print 'Max history file length:', readline.get_history_length()
    print 'Startup history:', get_history_items()
    try:
        while True:
            line = raw_input('Prompt ("stop" to quit): ')
            if line == 'stop':
                break
            if line:
                print 'Adding "%s" to the history' % line
    finally:
        print 'Final history:', get_history_items()
        readline.write_history_file(HISTORY_FILENAME)

# 補完関数を登録する
readline.set_completer(HistoryCompleter().complete)

# 補完に tab キーを使用する
readline.parse_and_bind('tab: complete')

# ユーザへテキストを表示する
input_loop()

HistoryCompleter は全ての入力を覚えていて、その後で入力を補完するときにそういった値を使用します。

$ python readline_history.py
Max history file length: -1
Startup history: []
Prompt ("stop" to quit): foo
Adding "foo" to the history
Prompt ("stop" to quit): bar
Adding "bar" to the history
Prompt ("stop" to quit): blah
Adding "blah" to the history
Prompt ("stop" to quit): b
bar   blah
Prompt ("stop" to quit): b
Prompt ("stop" to quit): stop
Final history: ['foo', 'bar', 'blah', 'stop']

b” に続けて TAB 2回を入力するとき、ログは次のように出力されます。

DEBUG:root:history: ['foo', 'bar', 'blah']
DEBUG:root:matches: ['bar', 'blah']
DEBUG:root:complete('b', 0) => 'bar'
DEBUG:root:complete('b', 1) => 'blah'
DEBUG:root:complete('b', 2) => None
DEBUG:root:history: ['foo', 'bar', 'blah']
DEBUG:root:matches: ['bar', 'blah']
DEBUG:root:complete('b', 0) => 'bar'
DEBUG:root:complete('b', 1) => 'blah'
DEBUG:root:complete('b', 2) => None

このスクリプトが2回目に実行されるとき、そのファイルから全ての履歴が読み込まれます。

$ python readline_history.py
Max history file length: -1
Startup history: ['foo', 'bar', 'blah', 'stop']
Prompt ("stop" to quit):

同様に個々の履歴を削除したり、履歴を完全に消去する関数もあります。

フック

対話的シーケンスの一部としてのアクションをトリガーするのに利用できるフックがあります。 startup フックは、プロンプトを表示する直前に呼び出されます。 pre-input フックは、プロンプトを実行した後でユーザーからのテキスト入力の前に実行されます。

import readline

def startup_hook():
    readline.insert_text('from startup_hook')

def pre_input_hook():
    readline.insert_text(' from pre_input_hook')
    readline.redisplay()

readline.set_startup_hook(startup_hook)
readline.set_pre_input_hook(pre_input_hook)
readline.parse_and_bind('tab: complete')

while True:
    line = raw_input('Prompt ("stop" to quit): ')
    if line == 'stop':
        break
    print 'ENTERED: "%s"' % line

どちらのフックも入力バッファを変更するために insert_text() を使用するため潜在的に便利な位置です。

$ python readline_hooks.py
Prompt ("stop" to quit): from startup_hook from pre_input_hook

もしこのバッファが pre-input フック内部で変更されるなら、画面を更新するために redisplay() を呼び出す必要があります。

See also

readline
本モジュールの標準ライブラリドキュメント
GNU readline
GNU readline ライブラリのドキュメント
readline init file format
初期化と設定ファイルフォーマット
effbot: The readline module
Effbot による readline モジュールのガイド
pyreadline
pyreadline は iPython で使用されていて readline に置き換わる Python ベースのライブラリとして開発されている
cmd
cmd モジュールは、コマンドラインインタフェースでタブ補完を実装するために readline を広範囲で使用します。この記事のサンプルは cmd から編集しました。
rlcompleter
rlcompleter は Python インタープリタと対話的にタブ補完を追加するために readline を使用します。
Bookmark and Share