메뉴 바로가기 검색 및 카테고리 바로가기 본문 바로가기

한빛출판네트워크

한빛랩스 - 지식에 가능성을 머지하다 / 강의 콘텐츠 무료로 수강하시고 피드백을 남겨주세요. ▶︎

IT/모바일

PyMOTW: readline

한빛미디어

|

2009-01-09

|

by HANBIT

11,801

제공 : 한빛 네트워크
저자 : Doug Hellmann
역자 : 유일호
원문 : PyMOTW: readline

readline – GNU readline 라이브러리와의 결합
  • 목적 : 명령 프롬프트상에서 사용자와의 상호작용을 위해 GNU readline 라이브러리를 사용한다.
  • Python 버전 : 1.4 버전 이상
Readline 은 명령라인 프로그램들을 사용하기 쉽도록 만들기 위해 프로그램과 사용자간의 대화능력을 향상시키기 위해 사용할 수 있는 모듈이다. 이것은 주로 명령어 자동완성에 사용되며, 이것을 이른바 “탭자동완성” 이라고 한다.

알아야 할 사항

Readline 이 콘솔과 상호작용하며 동작하는 이유로, 디버그 메시지를 화면에 출력하는 것은 샘플코드와 readline 사이에서 무슨일이 일어나고 있는지 분간하기 힘들게 만든다. 그래서 아래의 예제들은 디버그 정보를 파일에 따로 출력하도록 logging 모듈을 사용한다. 로그는 해당 예제와 함께 보여 줄 것이다.

설정하기

Readline 라이브러리를 설정하는 데에는 2가지 방법이 있는데, 설정파일을 사용하거나 parse_and_bind() 함수를 사용하는 것이다. 설정 옵션들은 자동완성을 위한 키바인딩, 편집 모드(vi 또는 emacs), 그리고 많은 옵션들을 포함 할 수 있다. 자세한것은 GNU readline library documentation 을 살펴보기 바란다.

탭자동완성을 구현하기 위한 가장 쉬운 방법은 parse_and_bind() 함수를 사용하는것이다. 이 함수는 여러 옵션들을 동시에 설정할 수가 있다. 다음 예제는 기본 편집 컨트롤을 기본모드인 “emacs” 대신에 “vi”로 바꾸는 것을 보여준다. 라인을 편집하기 위해서는 ESC 키를 누른 뒤 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
동일한 설정을 파일에 저장하고 한번에 불러올 수도 있다. myreadline.rc 로 저장한다면 아래와 같이 할수 잇을것이다:
# 탭자동완성을 사용한다.

tab: complete

# emacs 대신 vi 편집 모드를 사용한다.

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
텍스트 자동완성

명령라인 자동완성을 어떻게 구현하는지 보여주는 다음 예제에서, 우리는 프로그램에 내장된 실행 가능한 명령어 세트들이 내부에 있어서 해당 명령어를 입력할 때 탭자동완성을 사용할 수 있음을 알 수가 있다.
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:

            # 처음에는 text 가 이부분을 거치게 되고, 일치하는 리스트를 만들게 된다.
            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)


# 자동완성을 위해 탭키 사용
readline.parse_and_bind("tab: complete")


# 사용자 입력 프롬프트
input_loop()
input_loop() 함수는 입력값으로 "stop" 이 입력될때까지 단순히 라인 하나 하나를 읽어 들인다. 프로그램을 좀더 개선하면 실제로 입력 라인을 파싱하고 그에 따른 명령을 수행할 수 있을것이다.

SimpleCompleter 클래스는 자동완성 후보들의 리스트를 가진다. complete()메소드는 자동완성의 주체로서 readline 에 등록하기 위해 디자인 되었다. 해당 메소드의 인자로 “text” 는 완성할 문자열을, “state” 값은 해당 “text” 로 함수가 얼마나 호출되었는지 판별하기 위해 사용된다. 함수는 매번 state 의 증가와 함께 반복적으로 호출된다. 이 함수는 일치하는 후보의 state 값이 있을 경우 해당 문자열을 반환하거나 더 이상의 후보가 없을 경우 None을 반환해야만 한다. complete() 의 구현체는 state 가 0일 때 매칭되는 세트를 찾은뒤에 한번에 하나씩 일치된 모든 후보들을 리턴한다.

프로그램을 실행하면 결과는 다음과 같을 것이다.:
$ python readline_completer.py

Prompt ("stop" to quit):
탭키를 두번 누르면, 옵션의 목록이 출력될 것이다.
$ python readline_completer.py

Prompt ("stop" to quit):

list   print  start  stop

Prompt ("stop" to quit):
로그 파일은 complete() 가 두번 연속된 형태로 state 값들과 함께 호출되었음을 보여준다.
$ 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
첫번째 부분은 첫번째 탭키 입력에 의한 것이다. 자동완성 알고리즘은 모든 후보들을 질의하지만 입력라인에 보여주진 않는다. 그런뒤 두번째 탭키가 눌렸을 때 후보목록들이 재계산되고 비로소 사용자에게 보여지게 된다.

“l” 를 입력한다음 탭을 누르게 되면, 화면은 다음과 같이 표시된다.:
Prompt ("stop" to quit): list
그리고 로그 파일에는 다른 인자가 넘어간 complete() 가 찍힌다.
DEBUG:root:"l" matches: ["list"]

DEBUG:root:complete("l", 0) => "list"

DEBUG:root:complete("l", 1) => None
엔터키를 누르면 raw_input() 함수가 값을 리턴하고, 다시 while 루프 사이클을 돌게 된다.
Dispatch list

Prompt ("stop" to quit):
“s”로 시작하는 명령은 두가지로 자동완성될 수 있는 가능성을 가진다. “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
주의할 점

만약에 자동완성 함수에서 예외상황이 발생하면, 그것은 그냥 무시되고, readline 은 어떤것도 일치하는 자동완성후보가 없음으로 간주한다.

자동완성 버퍼 사용하기

앞서 본 자동완성 알고리즘은 간단하다. 왜냐하면 함수에 넘겨진 text 인자만을 찾기 때문이다. 이것 역시 입력버퍼의 text 를 다룰수 있는 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:

            # 처음에는 text 가 이부분을 거치게 되고, 일치하는 리스트를 만들게 된다.

            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)


# 자동완성을 위해 탭키를 사용
readline.parse_and_bind("tab: complete")


# 사용자 입력 프롬프트
input_loop()
이 예제에서는 명령어의 하위옵션들이 자동완성된다. complete() 메소드는 입력버퍼의 내용이 첫번째 단어와 일치하는지 이후의 단어와 일치하는지 결정하기 위해 자동완성의 포지션 정보가 필요하다. 타겟이 첫번째 단어 라면, 옵션 사전의 키값을 후보로 사용한다. 타겟이 이후의 단어라면, 옵션 사전으로부터 후보들을 찾아 사용한다.

여기에 3개의 상위레벨 명령어와 그중 2개는 하위레벨 명령어를 가지는 목록이 있다.:
  • list
    • files
    • directories
  • print
    • byname
    • bysize
  • stop
앞서 했던 동작을 따라해보자. 탭을 두번 누르면 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
로그는 텍스트 전체가 아닌 이후 부분에 대해 자동완성되었음을 보여준다.
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)


# 자동완성을 위해 탭키를 사용
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”를 입력한뒤 탭키를 두번 쳤을 때 나오는 로그를 보여준다.
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
스크립트를 다시한번 실행하면, 모든 히스토리를 파일로부터 읽어오게 된다.
$ python readline_history.py

Max history file length: -1

Startup history: ["foo", "bar", "blah", "stop"]

Prompt ("stop" to quit):
이뿐만 아니라 개개의 히스토리를 제거하거나 전체 히스토리를 제거할수 있는 기능이 있다.

훅(Hooks)

몇몇 상호작용 과정에서 액션을 취하기 위해 사용할수 있는 몇가지 훅(hooks)이 존재한다. 스타트업훅은 프로폼트를 출력하기 전에 바로 작동되며, 입력이전훅은 프롬프트 이후에 작동되며 이 시점은 사용자로부터 text를 읽어들이기 이전이다.
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
입력이전훅의 내부에서 버퍼가 수정되면, 화면을 업데이트하기 위해 redisplay() 함수호출이 필요하다.

참고

readline
- Readline 모듈의 표준 라이브러리 문서.
GNU readline
- GNU readline 라이브러리의 문서.
readline init file format
- 초기화 및 설정 파일 포맷.
effbot: The readline module
- Effbot의 readline 모듈 가이드
cmd
- cmd 모듈은 명령인터페이스에서 탭자동완성을 구현하기 위해 readline 을 광범위하게 사용한다. 여기서 소개한 몇몇 예제들은 cmd 의 코드를 참고 하였다.
rlcompleter
- rlcompleter 는 대화형 파이썬 인터프리터에 탭자동완성을 추가하기위해 readline 을 사용한다.


역자 유일호님은 현재 어느 중소기업의 소프트웨어 엔지니어이며, 잡다한 스킬덕에 여러 가지 개발일을 맡아서 하고 있다. 최근에는 더욱 심해진 게으름과 싸우며 힘들어 하고 있다.
TAG :
댓글 입력
자료실

최근 본 상품0