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" 이 입력될때까지 단순히 라인 하나 하나를 읽어 들인다. 프로그램을 좀더 개선하면 실제로 입력 라인을 파싱하고 그에 따른 명령을 수행할 수 있을것이다.
$ 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첫번째 부분은 첫번째 탭키 입력에 의한 것이다. 자동완성 알고리즘은 모든 후보들을 질의하지만 입력라인에 보여주진 않는다. 그런뒤 두번째 탭키가 눌렸을 때 후보목록들이 재계산되고 비로소 사용자에게 보여지게 된다.
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주의할 점
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() 메소드는 입력버퍼의 내용이 첫번째 단어와 일치하는지 이후의 단어와 일치하는지 결정하기 위해 자동완성의 포지션 정보가 필요하다. 타겟이 첫번째 단어 라면, 옵션 사전의 키값을 후보로 사용한다. 타겟이 이후의 단어라면, 옵션 사전으로부터 후보들을 찾아 사용한다.
$ 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입력 히스토리
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):이뿐만 아니라 개개의 히스토리를 제거하거나 전체 히스토리를 제거할수 있는 기능이 있다.
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() 함수호출이 필요하다.
이전 글 : 컴퓨터 언어는 무의미한가?
다음 글 : 우리의 첫 대량 감염
최신 콘텐츠