scriptty 0.5.0-java

Sign up to get free protection for your applications and to get access to all the features.
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