vimexpect.vim 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  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. let s:State = {}
  23. " Create a new State instance with a list where each item is a [regexp, name]
  24. " pair. A method named `name` must be defined in the created instance.
  25. function s:State.create(patterns)
  26. let this = copy(self)
  27. let this._patterns = a:patterns
  28. return this
  29. endfunction
  30. let s:Parser = {}
  31. let s:Parser.LINE_BUFFER_MAX_LEN = 100
  32. " Create a new Parser instance with the initial state and a target. The target
  33. " is a dictionary that will be the `self` of every State method call associated
  34. " with the parser, and may contain options normally passed to
  35. " `jobstart`(on_stdout/on_stderr will be overridden). Returns the target so it
  36. " can be called directly as the second argument of `jobstart`:
  37. "
  38. " call jobstart(prog_argv, vimexpect#Parser(initial_state, {'pty': 1}))
  39. function s:Parser.create(initial_state, target)
  40. let parser = copy(self)
  41. let parser._line_buffer = []
  42. let parser._stack = [a:initial_state]
  43. let parser._target = a:target
  44. let parser._target.on_stdout = function('s:JobOutput')
  45. let parser._target.on_stderr = function('s:JobOutput')
  46. let parser._target._parser = parser
  47. return parser._target
  48. endfunction
  49. " Push a state to the state stack
  50. function s:Parser.push(state)
  51. call add(self._stack, a:state)
  52. endfunction
  53. " Pop a state from the state stack. Fails if there's only one state remaining.
  54. function s:Parser.pop()
  55. if len(self._stack) == 1
  56. throw 'vimexpect:emptystack:State stack cannot be empty'
  57. endif
  58. return remove(self._stack, -1)
  59. endfunction
  60. " Replace the state currently in the top of the stack.
  61. function s:Parser.switch(state)
  62. let old_state = self._stack[-1]
  63. let self._stack[-1] = a:state
  64. return old_state
  65. endfunction
  66. " Append a list of lines to the parser line buffer and try to match it the
  67. " current state. This will shift old lines if the buffer crosses its
  68. " limit(defined by the LINE_BUFFER_MAX_LEN field). During normal operation,
  69. " this function is called by the job handler provided by this module, but it
  70. " may be called directly by the user for other purposes(testing for example)
  71. function s:Parser.feed(lines)
  72. if empty(a:lines)
  73. return
  74. endif
  75. let lines = a:lines
  76. let linebuf = self._line_buffer
  77. if lines[0] != "\n" && !empty(linebuf)
  78. " continue the previous line
  79. let linebuf[-1] .= lines[0]
  80. call remove(lines, 0)
  81. endif
  82. " append the newly received lines to the line buffer
  83. let linebuf += lines
  84. " keep trying to match handlers while the line isnt empty
  85. while !empty(linebuf)
  86. let match_idx = self.parse(linebuf)
  87. if match_idx == -1
  88. break
  89. endif
  90. let linebuf = linebuf[match_idx + 1 : ]
  91. endwhile
  92. " shift excess lines from the buffer
  93. while len(linebuf) > self.LINE_BUFFER_MAX_LEN
  94. call remove(linebuf, 0)
  95. endwhile
  96. let self._line_buffer = linebuf
  97. endfunction
  98. " Try to match a list of lines with the current state and call the handler if
  99. " the match succeeds. Return the index in `lines` of the first match.
  100. function s:Parser.parse(lines)
  101. let lines = a:lines
  102. if empty(lines)
  103. return -1
  104. endif
  105. let state = self.state()
  106. " search for a match using the list of patterns
  107. for [pattern, handler] in state._patterns
  108. let matches = matchlist(lines, pattern)
  109. if empty(matches)
  110. continue
  111. endif
  112. let match_idx = match(lines, pattern)
  113. call call(state[handler], matches[1:], self._target)
  114. return match_idx
  115. endfor
  116. endfunction
  117. " Return the current state
  118. function s:Parser.state()
  119. return self._stack[-1]
  120. endfunction
  121. " Job handler that simply forwards lines to the parser.
  122. function! s:JobOutput(_id, lines, _event) dict
  123. call self._parser.feed(a:lines)
  124. endfunction
  125. function vimexpect#Parser(initial_state, target)
  126. return s:Parser.create(a:initial_state, a:target)
  127. endfunction
  128. function vimexpect#State(patterns)
  129. return s:State.create(a:patterns)
  130. endfunction