vimexpect.vim (4946B)
1 " vimexpect.vim is a small object-oriented library that simplifies the task of 2 " scripting communication with jobs or any interactive program. The name 3 " `expect` comes from the famous tcl extension that has the same purpose. 4 " 5 " This library is built upon two simple concepts: Parsers and States. 6 " 7 " A State represents a program state and associates a set of regular 8 " expressions(to parse program output) with method names(to deal with parsed 9 " output). States are created with the vimexpect#State(patterns) function. 10 " 11 " A Parser manages data received from the program. It also manages State 12 " objects by storing them into a stack, where the top of the stack is the 13 " current State. Parsers are created with the vimexpect#Parser(initial_state, 14 " target) function 15 " 16 " The State methods are defined by the user, and are always called with `self` 17 " set as the Parser target. Advanced control flow is achieved by changing the 18 " current state with the `push`/`pop`/`switch` parser methods. 19 " 20 " An example of this library in action can be found in Neovim source 21 " code(contrib/neovim_gdb subdirectory) 22 23 let s:State = {} 24 25 26 " Create a new State instance with a list where each item is a [regexp, name] 27 " pair. A method named `name` must be defined in the created instance. 28 function s:State.create(patterns) 29 let this = copy(self) 30 let this._patterns = a:patterns 31 return this 32 endfunction 33 34 35 let s:Parser = {} 36 let s:Parser.LINE_BUFFER_MAX_LEN = 100 37 38 39 " Create a new Parser instance with the initial state and a target. The target 40 " is a dictionary that will be the `self` of every State method call associated 41 " with the parser, and may contain options normally passed to 42 " `jobstart`(on_stdout/on_stderr will be overridden). Returns the target so it 43 " can be called directly as the second argument of `jobstart`: 44 " 45 " call jobstart(prog_argv, vimexpect#Parser(initial_state, {'pty': 1})) 46 function s:Parser.create(initial_state, target) 47 let parser = copy(self) 48 let parser._line_buffer = [] 49 let parser._stack = [a:initial_state] 50 let parser._target = a:target 51 let parser._target.on_stdout = function('s:JobOutput') 52 let parser._target.on_stderr = function('s:JobOutput') 53 let parser._target._parser = parser 54 return parser._target 55 endfunction 56 57 58 " Push a state to the state stack 59 function s:Parser.push(state) 60 call add(self._stack, a:state) 61 endfunction 62 63 64 " Pop a state from the state stack. Fails if there's only one state remaining. 65 function s:Parser.pop() 66 if len(self._stack) == 1 67 throw 'vimexpect:emptystack:State stack cannot be empty' 68 endif 69 return remove(self._stack, -1) 70 endfunction 71 72 73 " Replace the state currently in the top of the stack. 74 function s:Parser.switch(state) 75 let old_state = self._stack[-1] 76 let self._stack[-1] = a:state 77 return old_state 78 endfunction 79 80 81 " Append a list of lines to the parser line buffer and try to match it the 82 " current state. This will shift old lines if the buffer crosses its 83 " limit(defined by the LINE_BUFFER_MAX_LEN field). During normal operation, 84 " this function is called by the job handler provided by this module, but it 85 " may be called directly by the user for other purposes(testing for example) 86 function s:Parser.feed(lines) 87 if empty(a:lines) 88 return 89 endif 90 let lines = a:lines 91 let linebuf = self._line_buffer 92 if lines[0] != "\n" && !empty(linebuf) 93 " continue the previous line 94 let linebuf[-1] .= lines[0] 95 call remove(lines, 0) 96 endif 97 " append the newly received lines to the line buffer 98 let linebuf += lines 99 " keep trying to match handlers while the line isnt empty 100 while !empty(linebuf) 101 let match_idx = self.parse(linebuf) 102 if match_idx == -1 103 break 104 endif 105 let linebuf = linebuf[match_idx + 1 : ] 106 endwhile 107 " shift excess lines from the buffer 108 while len(linebuf) > self.LINE_BUFFER_MAX_LEN 109 call remove(linebuf, 0) 110 endwhile 111 let self._line_buffer = linebuf 112 endfunction 113 114 115 " Try to match a list of lines with the current state and call the handler if 116 " the match succeeds. Return the index in `lines` of the first match. 117 function s:Parser.parse(lines) 118 let lines = a:lines 119 if empty(lines) 120 return -1 121 endif 122 let state = self.state() 123 " search for a match using the list of patterns 124 for [pattern, handler] in state._patterns 125 let matches = matchlist(lines, pattern) 126 if empty(matches) 127 continue 128 endif 129 let match_idx = match(lines, pattern) 130 call call(state[handler], matches[1:], self._target) 131 return match_idx 132 endfor 133 endfunction 134 135 136 " Return the current state 137 function s:Parser.state() 138 return self._stack[-1] 139 endfunction 140 141 142 " Job handler that simply forwards lines to the parser. 143 function! s:JobOutput(_id, lines, _event) dict 144 call self._parser.feed(a:lines) 145 endfunction 146 147 function vimexpect#Parser(initial_state, target) 148 return s:Parser.create(a:initial_state, a:target) 149 endfunction 150 151 152 function vimexpect#State(patterns) 153 return s:State.create(a:patterns) 154 endfunction