scriptty 0.5.0-java

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. data/.gitattributes +1 -0
  2. data/.gitignore +3 -0
  3. data/COPYING +674 -0
  4. data/COPYING.LESSER +165 -0
  5. data/README.rdoc +31 -0
  6. data/Rakefile +49 -0
  7. data/VERSION +1 -0
  8. data/bin/scriptty-capture +5 -0
  9. data/bin/scriptty-dump-screens +4 -0
  10. data/bin/scriptty-replay +5 -0
  11. data/bin/scriptty-term-test +4 -0
  12. data/bin/scriptty-transcript-parse +4 -0
  13. data/examples/captures/xterm-overlong-line-prompt.bin +9 -0
  14. data/examples/captures/xterm-vim-session.bin +262 -0
  15. data/examples/demo-capture.rb +19 -0
  16. data/examples/telnet-nego.rb +55 -0
  17. data/lib/scriptty/apps/capture_app/console.rb +104 -0
  18. data/lib/scriptty/apps/capture_app/password_prompt.rb +65 -0
  19. data/lib/scriptty/apps/capture_app.rb +213 -0
  20. data/lib/scriptty/apps/dump_screens_app.rb +166 -0
  21. data/lib/scriptty/apps/replay_app.rb +229 -0
  22. data/lib/scriptty/apps/term_test_app.rb +124 -0
  23. data/lib/scriptty/apps/transcript_parse_app.rb +143 -0
  24. data/lib/scriptty/cursor.rb +39 -0
  25. data/lib/scriptty/exception.rb +38 -0
  26. data/lib/scriptty/expect.rb +392 -0
  27. data/lib/scriptty/multiline_buffer.rb +192 -0
  28. data/lib/scriptty/net/event_loop.rb +610 -0
  29. data/lib/scriptty/screen_pattern/generator.rb +398 -0
  30. data/lib/scriptty/screen_pattern/parser.rb +558 -0
  31. data/lib/scriptty/screen_pattern.rb +104 -0
  32. data/lib/scriptty/term/dg410/dg410-client-escapes.txt +37 -0
  33. data/lib/scriptty/term/dg410/dg410-escapes.txt +82 -0
  34. data/lib/scriptty/term/dg410/parser.rb +162 -0
  35. data/lib/scriptty/term/dg410.rb +489 -0
  36. data/lib/scriptty/term/xterm/xterm-escapes.txt +73 -0
  37. data/lib/scriptty/term/xterm.rb +661 -0
  38. data/lib/scriptty/term.rb +40 -0
  39. data/lib/scriptty/util/fsm/definition_parser.rb +111 -0
  40. data/lib/scriptty/util/fsm/scriptty_fsm_definition.treetop +189 -0
  41. data/lib/scriptty/util/fsm.rb +177 -0
  42. data/lib/scriptty/util/transcript/reader.rb +96 -0
  43. data/lib/scriptty/util/transcript/writer.rb +111 -0
  44. data/test/apps/capture_app_test.rb +123 -0
  45. data/test/apps/transcript_parse_app_test.rb +118 -0
  46. data/test/cursor_test.rb +51 -0
  47. data/test/fsm_definition_parser_test.rb +220 -0
  48. data/test/fsm_test.rb +322 -0
  49. data/test/multiline_buffer_test.rb +275 -0
  50. data/test/net/event_loop_test.rb +402 -0
  51. data/test/screen_pattern/generator_test.rb +408 -0
  52. data/test/screen_pattern/parser_test/explicit_cursor_pattern.txt +14 -0
  53. data/test/screen_pattern/parser_test/explicit_fields.txt +22 -0
  54. data/test/screen_pattern/parser_test/multiple_patterns.txt +42 -0
  55. data/test/screen_pattern/parser_test/simple_pattern.txt +14 -0
  56. data/test/screen_pattern/parser_test/truncated_heredoc.txt +12 -0
  57. data/test/screen_pattern/parser_test/utf16bebom_pattern.bin +0 -0
  58. data/test/screen_pattern/parser_test/utf16lebom_pattern.bin +0 -0
  59. data/test/screen_pattern/parser_test/utf8_pattern.bin +14 -0
  60. data/test/screen_pattern/parser_test/utf8_unix_pattern.bin +14 -0
  61. data/test/screen_pattern/parser_test/utf8bom_pattern.bin +14 -0
  62. data/test/screen_pattern/parser_test.rb +266 -0
  63. data/test/term/dg410/parser_test.rb +139 -0
  64. data/test/term/xterm_test.rb +327 -0
  65. data/test/test_helper.rb +3 -0
  66. data/test/util/transcript/reader_test.rb +131 -0
  67. data/test/util/transcript/writer_test.rb +126 -0
  68. data/test.watchr +29 -0
  69. metadata +175 -0
@@ -0,0 +1,558 @@
1
+ # = Parser for screen pattern files
2
+ # Copyright (C) 2010 Infonium Inc.
3
+ #
4
+ # This file is part of ScripTTY.
5
+ #
6
+ # ScripTTY is free software: you can redistribute it and/or modify
7
+ # it under the terms of the GNU Lesser General Public License as published
8
+ # by the Free Software Foundation, either version 3 of the License, or
9
+ # (at your option) any later version.
10
+ #
11
+ # ScripTTY is distributed in the hope that it will be useful,
12
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ # GNU Lesser General Public License for more details.
15
+ #
16
+ # You should have received a copy of the GNU Lesser General Public License
17
+ # along with ScripTTY. If not, see <http://www.gnu.org/licenses/>.
18
+
19
+ require 'multibyte'
20
+ require 'iconv'
21
+ require 'strscan'
22
+ require 'scriptty/screen_pattern'
23
+
24
+ module ScripTTY
25
+ class ScreenPattern # reopen
26
+ # Parser for screen pattern files
27
+ #
28
+ # Parses a file containing screen patterns, yielding hashes.
29
+ class Parser
30
+ # NOTE: Ruby Regexp matching depends on the value of $KCODE, which gets
31
+ # changed by the 'multibyte' library (and Rails) from the default of "NONE" to "UTF8".
32
+ # The regexps here are designed to work exclusively with individual BYTES
33
+ # in UTF-8 strings, so we need to use the //n flag in all regexps here so
34
+ # that $KCODE is ignored.
35
+ # NOTE: The //n flag is not preserved by Regexp#to_s; you need the //n
36
+ # flag on the regexp object that is actually being matched.
37
+
38
+ COMMENT = /\s*#.*$/no
39
+ OPTIONAL_COMMENT = /#{COMMENT}|\s*$/no
40
+ IDENTIFIER = /[a-zA-Z0-9_]+/n
41
+ NIL = /nil/n
42
+ RECTANGLE = /\(\s*\d+\s*,\s*\d+\s*\)\s*-\s*\(\s*\d+\s*,\s*\d+\s*\)/n
43
+ STR_UNESCAPED = /[^"\\\t\r\n]/n
44
+ STR_OCTAL = /\\[0-7]{3}/n
45
+ STR_HEX = /\\x[0-9A-Fa-f]{2}/n
46
+ STR_SINGLE = /\\[enrt\\]|\\[^a-zA-Z0-9]/n
47
+ STRING = /"(?:#{STR_UNESCAPED}|#{STR_OCTAL}|#{STR_HEX}|#{STR_SINGLE})*"/no
48
+ INTEGER = /-?\d+/no
49
+ TWO_INTEGER_TUPLE = /\(\s*#{INTEGER}\s*,\s*#{INTEGER}\s*\)/no
50
+ TUPLE_ELEMENT = /#{INTEGER}|#{STRING}|#{NIL}|#{TWO_INTEGER_TUPLE}/no # XXX HACK: We actually want nested tuples, but we can't use regexp matching for that
51
+ TUPLE = /\(\s*#{TUPLE_ELEMENT}(?:\s*,\s*#{TUPLE_ELEMENT})*\s*\)/no
52
+ HEREDOCSTART = /<<#{IDENTIFIER}/no
53
+ SCREENNAME_LINE = /^\[(#{IDENTIFIER})\]\s*$/no
54
+ BLANK_LINE = /^\s*$/no
55
+ COMMENT_LINE = /^#{OPTIONAL_COMMENT}$/no
56
+
57
+ SINGLE_CHAR_PROPERTIES = %w( char_cursor char_ignore char_field )
58
+ TWO_TUPLE_PROPERTIES = %w( position size cursor_pos )
59
+ RECOGNIZED_PROPERTIES = SINGLE_CHAR_PROPERTIES + TWO_TUPLE_PROPERTIES + %w( rectangle fields text )
60
+
61
+ class <<self
62
+ def parse(s, &block)
63
+ new(s, &block).parse
64
+ nil
65
+ end
66
+ protected :new # Users should not instantiate this object directly
67
+ end
68
+
69
+ def initialize(s, &block)
70
+ raise ArgumentError.new("no block given") unless block
71
+ @block = block
72
+ @lines = preprocess(s).split("\n").map{|line| "#{line}\n"}
73
+ @line = nil
74
+ @lineno = 0
75
+ @state = :start
76
+ end
77
+
78
+ def parse
79
+ until @lines.empty?
80
+ @line = @lines.shift
81
+ @lineno += 1
82
+ send("handle_#{@state}_state")
83
+ end
84
+ handle_eof
85
+ end
86
+
87
+ private
88
+
89
+ # Top level of the configuration file
90
+ def handle_start_state
91
+ return if @line =~ BLANK_LINE
92
+ return if @line =~ COMMENT_LINE
93
+ @screen_name = nil
94
+
95
+ if @line =~ SCREENNAME_LINE # start of screen "[screen_name]"
96
+ @screen_name = $1
97
+ @screen_properties = {}
98
+ @state = :screen
99
+ return
100
+ end
101
+
102
+ parse_fail("expected [identifier]")
103
+ end
104
+
105
+ def handle_screen_state
106
+ return if @line =~ BLANK_LINE
107
+ return if @line =~ COMMENT_LINE
108
+ if @line =~ SCREENNAME_LINE
109
+ handle_done_screen
110
+ return handle_start_state
111
+ end
112
+ if @line =~ /^(#{IDENTIFIER})\s*:\s*(?:(#{STRING})|(#{RECTANGLE})|(#{HEREDOCSTART}|(#{TUPLE})))#{OPTIONAL_COMMENT}$/no
113
+ k, v_str, v_rect, v_heredoc, v_tuple = [$1, parse_string($2), parse_rectangle($3), parse_heredocstart($4), parse_tuple($5)]
114
+ if v_str
115
+ set_screen_property(k, v_str)
116
+ elsif v_rect
117
+ set_screen_property(k, v_rect)
118
+ elsif v_tuple
119
+ set_screen_property(k, v_tuple)
120
+ elsif v_heredoc
121
+ @heredoc = {
122
+ :propname => k,
123
+ :delimiter => v_heredoc,
124
+ :content => "",
125
+ :lineno => @lineno,
126
+ }
127
+ @state = :heredoc
128
+ else
129
+ raise "BUG"
130
+ end
131
+ else
132
+ parse_fail("expected: key:value or [identifier]")
133
+ end
134
+ end
135
+
136
+ def handle_eof
137
+ if @state == :start
138
+ # Do nothing
139
+ elsif @state == :screen
140
+ handle_done_screen
141
+ elsif @state == :heredoc
142
+ parse_fail("expected: #{@heredoc[:delimiter].inspect}, got EOF")
143
+ else
144
+ raise "BUG: unhandled EOF on state #{@state}"
145
+ end
146
+ end
147
+
148
+ def handle_heredoc_state
149
+ if @line =~ /^#{Regexp.escape(@heredoc[:delimiter])}\s*$/n
150
+ # End of here-document
151
+ set_screen_property(@heredoc[:propname], @heredoc[:content])
152
+ @heredoc = nil
153
+ @state = :screen
154
+ else
155
+ @heredoc[:content] << @line
156
+ end
157
+ end
158
+
159
+ def handle_done_screen
160
+ # Remove stuff that's irrelevant once the screen is parsed
161
+ @screen_properties.delete("char_field")
162
+ @screen_properties.delete("char_cursor")
163
+ @screen_properties.delete("char_ignore")
164
+
165
+ # Invoke the passed-in block with the parsed screen information
166
+ @block.call({ :name => @screen_name, :properties => @screen_properties })
167
+
168
+ # Reset to the initial state
169
+ @screen_name = @screen_properties = nil
170
+ @state = :start
171
+ end
172
+
173
+ def validate_single_char_property(k, v)
174
+ c = v.chars.to_a # Split field into array of single-character (but possibly multi-byte) strings
175
+ unless c.length == 1
176
+ parse_fail("#{k} must be a single character or Unicode code point", property_lineno)
177
+ end
178
+ end
179
+
180
+ def validate_tuple_property(k, v, length=2)
181
+ parse_fail("#{k} must be a #{length}-tuple", property_lineno) unless v.length == length
182
+ parse_fail("#{k} must contain positive integers", property_lineno) unless v[0] >=0 and v[1] >= 0
183
+ end
184
+
185
+ def set_screen_text(k, v, lineno=nil)
186
+ lineno ||= property_lineno
187
+
188
+ text = v.split("\n").map{|line| line.rstrip} # Split on newlines and strip trailing whitespace
189
+
190
+ # Get the implicit size of the screen from the text
191
+ parse_fail("#{k} must be surrounded by identical +-----+ lines", lineno) unless text[0] =~ /^\+(-+)\+$/
192
+ width = $1.length
193
+ height = text.length-2
194
+ parse_fail("#{k} must be surrounded by identical +-----+ lines", lineno + text.length) unless text[-1] == text[0] # TODO - test if this is the correct offset
195
+ text = text[1..-2] # strip top and bottom +------+ lines
196
+ lineno += 1 # Increment line number to compensate
197
+
198
+ # If there is an explicitly-specified size of the screen, compare against it.
199
+ # If there is no explicitly-specified size of the screen, use the implicit size.
200
+ explicit_height, explicit_width = @screen_properties['size']
201
+ explicit_height ||= height ; explicit_width ||= width # Default to the implicit size
202
+ if (explicit_height != height) or (explicit_width != width)
203
+ parse_fail("#{k} dimensions (#{height}x#{width}) conflict with explicit size (#{explicit_height}x#{explicit_width})", lineno)
204
+ else
205
+ set_screen_property("size", [height, width]) # in case it wasn't set explicitly
206
+ end
207
+
208
+ match_list = set_properties_from_grid(text,
209
+ :property_name => k,
210
+ :start_lineno => lineno+1,
211
+ :width => width,
212
+ :char_cursor => @screen_properties['char_cursor'],
213
+ :char_field => @screen_properties['char_field'],
214
+ :char_ignore => @screen_properties['char_ignore'])
215
+
216
+ if match_list and !match_list.empty?
217
+ @screen_properties['matches'] = match_list # XXX TODO - This will probably need to change
218
+ end
219
+ end
220
+
221
+ # If no position has been specified for this pattern, default to the top-left corner of the window.
222
+ def ensure_position
223
+ set_screen_property('position', [0,0]) unless @screen_properties['position']
224
+ end
225
+
226
+ # Convert a relative [row,column] or [row, col1..col2] into an absolute position or range.
227
+ def abs_pos(relative_pos)
228
+ screen_pos = @screen_properties['position']
229
+ if relative_pos[1].is_a?(Range)
230
+ [relative_pos[0]+screen_pos[0], relative_pos[1].first+screen_pos[1]..relative_pos[1].last+screen_pos[1]]
231
+ else
232
+ [relative_pos[0]+screen_pos[0], relative_pos[1]+screen_pos[1]]
233
+ end
234
+ end
235
+
236
+ # Walk through all the characters in the pattern, building up the
237
+ # pattern-matching data structures.
238
+ def set_properties_from_grid(lines, options={})
239
+ # Get options
240
+ height = lines.length
241
+ k = options[:property_name]
242
+ width = options[:width]
243
+ start_lineno = options[:start_lineno] || 1
244
+ char_cursor = options[:char_cursor]
245
+ char_field = options[:char_field]
246
+ char_ignore = options[:char_ignore]
247
+
248
+ # Convert each row into an array of single-character (possibly multi-byte) strings
249
+ lines_chars = lines.map{|line| line.chars.to_a}
250
+
251
+ # Each row consists of a grid bordered by vertical bars,
252
+ # followed by field names, e.g.:
253
+ # |.......| ("foo", "bar")
254
+ # Separate the grid from the field names
255
+ grid = []
256
+ row_field_names = []
257
+ (0..height-1).each do |row|
258
+ start_border = lines_chars[row][0]
259
+ end_border = lines_chars[row][width+1]
260
+ parse_fail("column 1: expected '|', got #{start_border.inspect}", start_lineno + row) unless start_border == "|"
261
+ parse_fail("column #{width+2}: expected '|', got #{end_border.inspect}", start_lineno + row) unless end_border == "|"
262
+ grid << lines_chars[row][1..width]
263
+ row_field_names << (parse_string_or_null_tuple(lines_chars[row][width+2..-1].join, start_lineno + row, width+3) || [])
264
+ end
265
+
266
+ match_positions = []
267
+ field_positions = []
268
+ cursor_pos = nil
269
+ (0..height-1).each do |row|
270
+ (0..width-1).each do |column|
271
+ pos = [row, column]
272
+ c = grid[row][column]
273
+ if c.nil? or c == char_ignore
274
+ # do nothing; ignore this character cell
275
+ elsif c == char_field
276
+ field_positions << pos
277
+ elsif c == char_cursor
278
+ parse_fail("column #{column+2}: multiple cursors", start_lineno + row) if cursor_pos
279
+ cursor_pos = pos
280
+ else
281
+ match_positions << pos
282
+ end
283
+ end
284
+ end
285
+ match_ranges_by_row = consolidate_positions(match_positions)
286
+ field_ranges_by_row = consolidate_positions(field_positions)
287
+
288
+ # Set the cursor position
289
+ set_screen_property('cursor_pos', cursor_pos, start_lineno + cursor_pos[0]) if cursor_pos
290
+
291
+ # Add fields to the screen. Complain if there's a mismatch between
292
+ # the number of fields found and the number identified.
293
+ (0..height-1).each do |row|
294
+ col_ranges = field_ranges_by_row[row] || []
295
+ unless row_field_names[row].length == col_ranges.length
296
+ parse_fail("field count mismatch: #{col_ranges.length} fields found, #{row_field_names[row].length} fields named or ignored", start_lineno + row)
297
+ end
298
+ row_field_names[row].zip(col_ranges) do |field_name, col_range|
299
+ next unless field_name # skip nil field names
300
+ add_field_to_screen(field_name, abs_pos([row, col_range]), start_lineno + row)
301
+ end
302
+ end
303
+
304
+ # Return the match list
305
+ # i.e. a list of [[row, start_col], string]
306
+ match_list = []
307
+ match_ranges_by_row.keys.sort.each do |row|
308
+ col_ranges = match_ranges_by_row[row]
309
+ col_ranges.each do |col_range|
310
+ s = grid[row][col_range].join
311
+ # Ensure that all remaining characters are printable ASCII. We
312
+ # don't support Unicode matching right now. (But we can probably
313
+ # just remove this check when adding support later.)
314
+ if (c = (s =~ /([^\x20-\x7e])/u))
315
+ parse_fail("column #{c+2}: non-ASCII-printable character #{$1.inspect}", start_lineno + row)
316
+ end
317
+ match_list << [abs_pos([row, col_range.first]), s]
318
+ end
319
+ end
320
+ match_list
321
+ end
322
+
323
+ # Consolidate adjacent positions and group them by row
324
+ #
325
+ # Example input:
326
+ # [[0,0], [0,1], [0,2], [1,1], [1,2], [1,4]]
327
+ # Example output:
328
+ # {0: [0..2], 1: [1..2, 4..4]]
329
+ def consolidate_positions(positions)
330
+ results = []
331
+ positions.each do |row, col|
332
+ if results[-1] and results[-1][0] == row and results[-1][1].last == col-1
333
+ results[-1][1] = results[-1][1].first..col
334
+ else
335
+ results << [row, col..col]
336
+ end
337
+ end
338
+
339
+ # Collect ranges by row
340
+ results_by_row = {}
341
+ results.each do |row, col_range|
342
+ results_by_row[row] ||= []
343
+ results_by_row[row] << col_range
344
+ end
345
+ results_by_row
346
+ end
347
+
348
+ # Return the line number of the current property.
349
+ #
350
+ # This will be either @lineno, or @heredoc[:lineno] (if the latter is present)
351
+ def property_lineno
352
+ @heredoc ? @heredoc[:lineno] : @lineno
353
+ end
354
+
355
+ # Add a field to the 'fields' property.
356
+ #
357
+ # Raise an exception if the field already exists.
358
+ def add_field_to_screen(name, row_and_col_range, lineno=nil)
359
+ lineno ||= property_lineno
360
+ row, col_range = row_and_col_range
361
+ @screen_properties['fields'] ||= {}
362
+ if @screen_properties['fields'].include?(name) and @screen_properties['fields'][name] != [row, col_range]
363
+ parse_fail("field #{format_field(name, row, col_range)} conflicts with previous definition #{format_field(name, *@screen_properties['fields'][name])}", lineno)
364
+ end
365
+ @screen_properties['fields'][name] = [row, col_range]
366
+ nil
367
+ end
368
+
369
+ def set_screen_property(k,v, lineno=nil)
370
+ lineno ||= property_lineno
371
+ parse_fail("Unrecognized property name #{k}", lineno) unless RECOGNIZED_PROPERTIES.include?(k)
372
+ validate_single_char_property(k, v) if SINGLE_CHAR_PROPERTIES.include?(k)
373
+ validate_tuple_property(k, v, 2) if TWO_TUPLE_PROPERTIES.include?(k)
374
+ if k == "rectangle"
375
+ set_screen_property("position", v[0,2], lineno)
376
+ set_screen_property("size", [v[2]-v[0]+1, v[3]-v[1]+1], lineno)
377
+ elsif k == "text"
378
+ ensure_position # "position", if set, must be set before this field is set
379
+ set_screen_text(k,v, lineno)
380
+ elsif k == "fields"
381
+ ensure_position # "position", if set, must be set before this field is set
382
+ v.split("\n", -1).each_with_index do |raw_tuple, i|
383
+ next if raw_tuple.nil? or raw_tuple.empty?
384
+ t = parse_tuple(raw_tuple.strip, lineno+i)
385
+ field_name, rel_pos, length = t
386
+ unless (field_name.is_a?(String) and rel_pos.is_a?(Array) and
387
+ rel_pos[0].is_a?(Integer) and rel_pos[1].is_a?(Integer) and length.is_a?(Integer))
388
+ parse_fail("incorrect field format: should be (name, (row, col), length)", lineno+i)
389
+ end
390
+ unless length > 0
391
+ parse_fail("field length must be positive", lineno+i)
392
+ end
393
+ rel_range = [rel_pos[0], rel_pos[1]..rel_pos[1]+length-1]
394
+ add_field_to_screen(field_name, abs_pos(rel_range), lineno+i)
395
+ end
396
+ else
397
+ v = abs_pos(v) if k == "cursor_pos"
398
+ # Don't allow setting a screen property more than once to different values.
399
+ old_value = @screen_properties[k]
400
+ unless old_value.nil? or old_value == v
401
+ if k == "position"
402
+ extra_note = " NOTE: 'position' should occur before other properties in the screen definition"
403
+ else
404
+ extra_note = ""
405
+ end
406
+ parse_fail("property #{k} value #{v.inspect} conflicts with already-set value #{old_value.inspect}#{extra_note}", lineno)
407
+ end
408
+ @screen_properties[k] = v
409
+ end
410
+ end
411
+
412
+ def parse_string(str, lineno=nil)
413
+ return nil unless str
414
+ retval = []
415
+ s = StringScanner.new(str)
416
+ unless s.scan /"/n
417
+ parse_fail("unable to parse string #{str.inspect}", lineno)
418
+ end
419
+ until s.eos?
420
+ if (m = s.scan STR_UNESCAPED)
421
+ retval << m
422
+ elsif (m = s.scan STR_OCTAL)
423
+ retval << [m[1..-1].to_i(8)].pack("C*")
424
+ elsif (m = s.scan STR_HEX)
425
+ retval << [m[2..-1].to_i(16)].pack("C*")
426
+ elsif (m = s.scan STR_SINGLE)
427
+ c = m[1..-1]
428
+ retval << case c
429
+ when 'e'
430
+ "\e"
431
+ when 'n'
432
+ "\n"
433
+ when 'r'
434
+ "\r"
435
+ when 't'
436
+ "\t"
437
+ when /[^a-zA-Z]/n
438
+ c
439
+ else
440
+ raise "BUG"
441
+ end
442
+ elsif (m = s.scan /"/) # End of string
443
+ parse_fail("unable to parse string #{str.inspect}", lineno) unless s.eos?
444
+ else
445
+ parse_fail("unable to parse string #{str.inspect}", lineno)
446
+ end
447
+ end
448
+ retval.join
449
+ end
450
+
451
+ # Parse (row1,col1)-(row2,col2) into [row1, col1, row2, col2]
452
+ def parse_rectangle(str)
453
+ return nil unless str
454
+ str.split(/[(,)\-\s]+/n, -1)[1..-2].map{|n| n.to_i}
455
+ end
456
+
457
+ def parse_heredocstart(str)
458
+ return nil unless str
459
+ str[2..-1]
460
+ end
461
+
462
+ # Parse (a, b, ...) into [a, b, ...]
463
+ def parse_tuple(str, line=nil, column=nil)
464
+ return nil unless str
465
+ column ||= 1
466
+ retval = []
467
+ s = StringScanner.new(str)
468
+
469
+ # Leading parenthesis
470
+ done = false
471
+ expect_comma = false
472
+ if s.scan /\s*\(/n
473
+ # start of tuple
474
+ elsif s.scan COMMENT_LINE
475
+ # Comment or blank line
476
+ return nil
477
+ else
478
+ parse_fail("column #{column+s.pos}: expected '(', got #{s.rest.chars.to_a[0].inspect}", line)
479
+ end
480
+ until s.eos?
481
+ if s.scan /\)/n # final parenthesis
482
+ done = true
483
+ break
484
+ end
485
+ next if s.scan /\s+/n # strip whitespace
486
+ if expect_comma
487
+ if s.scan /,/n
488
+ expect_comma = false
489
+ else
490
+ parse_fail("column #{column+s.pos}: expected ',', got #{s.rest.chars.to_a[0].inspect}", line)
491
+ end
492
+ else
493
+ if (m = s.scan STRING)
494
+ retval << parse_string(m)
495
+ elsif (m = s.scan INTEGER)
496
+ retval << m.to_i
497
+ elsif (m = s.scan NIL)
498
+ retval << nil
499
+ elsif (m = s.scan TWO_INTEGER_TUPLE)
500
+ retval << parse_rectangle(m) # parse_rectangle is dumb, so it will work here
501
+ else
502
+ parse_fail("column #{column+s.pos}: expected STRING, INTEGER, TWO_INTEGER_TUPLE, or NIL, got #{s.rest.chars.to_a[0].inspect}", line)
503
+ end
504
+ expect_comma = true
505
+ end
506
+ end
507
+ parse_fail("column #{column+s.pos}: tuple truncated", line) unless done
508
+ s.scan OPTIONAL_COMMENT
509
+ parse_fail("column #{column+s.pos}: extra junk found: #{s.rest.inspect}", line) unless s.eos?
510
+ retval
511
+ end
512
+
513
+ def parse_string_or_null_tuple(str, line=nil, column=nil)
514
+ t = parse_tuple(str, line, column)
515
+ return nil unless t
516
+ t.each_with_index do |v, i|
517
+ parse_fail("element #{i+1} of tuple is #{v.class.name}, but a string or null is required", line) unless v.nil? or v.is_a?(String)
518
+ end
519
+ t
520
+ end
521
+
522
+ # Return the display form of a field
523
+ def format_field(name, row, col_range)
524
+ "(#{name.inspect}, (#{row}, #{col_range.first}), #{col_range.count})"
525
+ end
526
+
527
+ def parse_fail(message=nil, line=nil)
528
+ line ||= @lineno
529
+ raise ArgumentError.new("error:line #{line}: #{message || 'parse error'}")
530
+ end
531
+
532
+ # Pre-process an input string.
533
+ #
534
+ # This converts the string into UTF-8, and replaces platform-specific newlines with "\n"
535
+ def preprocess(s, source_encoding=nil)
536
+ unless source_encoding
537
+ # Text files on Windows can be saved in a few different "Unicode"
538
+ # encodings. Decode the common ones into UTF-8.
539
+ source_encoding =
540
+ if s =~ /\A\xef\xbb\xbf/ # UTF-8+BOM
541
+ "UTF-8"
542
+ elsif s =~ /\A(\xfe\xff|\xff\xfe)/ # UTF-16 BOM (big or little endian)
543
+ "UTF-16"
544
+ else
545
+ "UTF-8" # assume UTF-8
546
+ end
547
+ end
548
+ # XXX TODO FIXME: There's a bug in JRuby's Iconv that prevents
549
+ # Iconv::IllegalSequence from being raised. Instead, the source string
550
+ # is returned. We should handle this somehow.
551
+ (s,) = Iconv.iconv("UTF-8", source_encoding, s)
552
+ s = s.gsub(/\xef\xbb\xbf/, "") # Strip the UTF-8 BYTE ORDER MARK (U+FEFF)
553
+ s = s.gsub(/\r\n/, "\n").gsub(/\r/, "\n") # Replace CR and CRLF with LF (Unix newline)
554
+ s = Multibyte::Chars.new(s).normalize(:c).to_a.join # Unicode Normalization Form C
555
+ end
556
+ end
557
+ end
558
+ end
@@ -0,0 +1,104 @@
1
+ # = Screen pattern object
2
+ # Copyright (C) 2010 Infonium Inc.
3
+ #
4
+ # This file is part of ScripTTY.
5
+ #
6
+ # ScripTTY is free software: you can redistribute it and/or modify
7
+ # it under the terms of the GNU Lesser General Public License as published
8
+ # by the Free Software Foundation, either version 3 of the License, or
9
+ # (at your option) any later version.
10
+ #
11
+ # ScripTTY is distributed in the hope that it will be useful,
12
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ # GNU Lesser General Public License for more details.
15
+ #
16
+ # You should have received a copy of the GNU Lesser General Public License
17
+ # along with ScripTTY. If not, see <http://www.gnu.org/licenses/>.
18
+
19
+ module ScripTTY
20
+ class ScreenPattern
21
+ class <<self
22
+ # Parse a pattern file and return an array of ScreenPattern objects
23
+ def parse(s)
24
+ retval = []
25
+ Parser.parse(s) do |spec|
26
+ retval << new(spec[:name], spec[:properties])
27
+ end
28
+ retval
29
+ end
30
+
31
+ def from_term(term, name=nil)
32
+ from_text(term.text, :name => name, :cursor_pos => term.cursor_pos)
33
+ end
34
+
35
+ def from_text(text, opts={})
36
+ text = text.split(/\r?\n/n) if text.is_a?(String)
37
+ name ||= opts[:name] || "untitled"
38
+
39
+ width = text.map{|line| line.chars.to_a.length}.max
40
+ height = text.length
41
+ properties = {}
42
+ properties['cursor_pos'] = opts[:cursor_pos]
43
+ properties['size'] = [height, width]
44
+ properties['matches'] = []
45
+ text.each_with_index{|line, i|
46
+ properties['matches'] << [[i, 0], line]
47
+ }
48
+ new(name, properties)
49
+ end
50
+ protected :new # Users should not instantiate this object directly
51
+ end
52
+
53
+ # The name given to this pattern
54
+ attr_accessor :name
55
+
56
+ # The [row, column] of the cursor position (or nil if unspecified)
57
+ attr_accessor :cursor_pos
58
+
59
+ def initialize(name, properties) # :nodoc:
60
+ @name = name
61
+ @position = properties["position"]
62
+ @size = properties["size"]
63
+ @cursor_pos = properties["cursor_pos"]
64
+ @field_ranges = properties["fields"] # Hash of "field_name" => [row, col1..col2] ranges
65
+ @matches = properties["matches"] # Array of [[row,col], string] records to match
66
+ end
67
+
68
+ def inspect
69
+ "#<#{self.class.name}:#{sprintf("0x%x", object_id)} name=#{@name}>"
70
+ end
71
+
72
+ # Match this pattern against a Term object. If the match succeeds, return
73
+ # the Hash of fields extracted from the screen. Otherwise, return nil.
74
+ def match_term(term)
75
+ return nil if @cursor_pos and @cursor_pos != term.cursor_pos
76
+
77
+ # XXX UNICODE
78
+ if @matches
79
+ text = term.text
80
+ @matches.each do |pos, str|
81
+ row, col = pos
82
+ col_range = col..col+str.length-1
83
+ return nil unless text[row][col_range] == str
84
+ end
85
+ end
86
+
87
+ fields = {}
88
+ if @field_ranges
89
+ @field_ranges.each_pair do |k, range|
90
+ row, col_range = range
91
+ fields[k] = text[row][col_range]
92
+ end
93
+ end
94
+ fields
95
+ end
96
+
97
+ def generate(name=nil)
98
+ Generator.generate(name || "untitled", :cursor_pos => @cursor_pos, :matches => @matches, :fields => @field_ranges, :position => @position, :size => @size)
99
+ end
100
+ end
101
+ end
102
+
103
+ require 'scriptty/screen_pattern/parser'
104
+ require 'scriptty/screen_pattern/generator'
@@ -0,0 +1,37 @@
1
+ '\006' => t_client_ack
2
+ '\012' => t_new_line
3
+ '\015' => t_carriage_return
4
+ '\036' => {
5
+ 'o' => {
6
+ '#' => {
7
+ '*' => {
8
+ * => {
9
+ * => t_client_response_read_model_id
10
+ }
11
+ }
12
+ }
13
+ }
14
+ }
15
+ '\037' => {
16
+ * => {
17
+ * => t_client_response_read_window_address
18
+ }
19
+ }
20
+ '\377' => {
21
+ '\372' => t_parse_telnet_sb => {
22
+ * => t_telnet_subnegotiation
23
+ }
24
+ '\373' => {
25
+ * => t_telnet_will
26
+ }
27
+ '\374' => {
28
+ * => t_telnet_wont
29
+ }
30
+ '\375' => {
31
+ * => t_telnet_do
32
+ }
33
+ '\376' => {
34
+ * => t_telnet_dont
35
+ }
36
+ }
37
+ [\x20-\x7e] => t_printable