scriptty 0.5.0-java
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitattributes +1 -0
- data/.gitignore +3 -0
- data/COPYING +674 -0
- data/COPYING.LESSER +165 -0
- data/README.rdoc +31 -0
- data/Rakefile +49 -0
- data/VERSION +1 -0
- data/bin/scriptty-capture +5 -0
- data/bin/scriptty-dump-screens +4 -0
- data/bin/scriptty-replay +5 -0
- data/bin/scriptty-term-test +4 -0
- data/bin/scriptty-transcript-parse +4 -0
- data/examples/captures/xterm-overlong-line-prompt.bin +9 -0
- data/examples/captures/xterm-vim-session.bin +262 -0
- data/examples/demo-capture.rb +19 -0
- data/examples/telnet-nego.rb +55 -0
- data/lib/scriptty/apps/capture_app/console.rb +104 -0
- data/lib/scriptty/apps/capture_app/password_prompt.rb +65 -0
- data/lib/scriptty/apps/capture_app.rb +213 -0
- data/lib/scriptty/apps/dump_screens_app.rb +166 -0
- data/lib/scriptty/apps/replay_app.rb +229 -0
- data/lib/scriptty/apps/term_test_app.rb +124 -0
- data/lib/scriptty/apps/transcript_parse_app.rb +143 -0
- data/lib/scriptty/cursor.rb +39 -0
- data/lib/scriptty/exception.rb +38 -0
- data/lib/scriptty/expect.rb +392 -0
- data/lib/scriptty/multiline_buffer.rb +192 -0
- data/lib/scriptty/net/event_loop.rb +610 -0
- data/lib/scriptty/screen_pattern/generator.rb +398 -0
- data/lib/scriptty/screen_pattern/parser.rb +558 -0
- data/lib/scriptty/screen_pattern.rb +104 -0
- data/lib/scriptty/term/dg410/dg410-client-escapes.txt +37 -0
- data/lib/scriptty/term/dg410/dg410-escapes.txt +82 -0
- data/lib/scriptty/term/dg410/parser.rb +162 -0
- data/lib/scriptty/term/dg410.rb +489 -0
- data/lib/scriptty/term/xterm/xterm-escapes.txt +73 -0
- data/lib/scriptty/term/xterm.rb +661 -0
- data/lib/scriptty/term.rb +40 -0
- data/lib/scriptty/util/fsm/definition_parser.rb +111 -0
- data/lib/scriptty/util/fsm/scriptty_fsm_definition.treetop +189 -0
- data/lib/scriptty/util/fsm.rb +177 -0
- data/lib/scriptty/util/transcript/reader.rb +96 -0
- data/lib/scriptty/util/transcript/writer.rb +111 -0
- data/test/apps/capture_app_test.rb +123 -0
- data/test/apps/transcript_parse_app_test.rb +118 -0
- data/test/cursor_test.rb +51 -0
- data/test/fsm_definition_parser_test.rb +220 -0
- data/test/fsm_test.rb +322 -0
- data/test/multiline_buffer_test.rb +275 -0
- data/test/net/event_loop_test.rb +402 -0
- data/test/screen_pattern/generator_test.rb +408 -0
- data/test/screen_pattern/parser_test/explicit_cursor_pattern.txt +14 -0
- data/test/screen_pattern/parser_test/explicit_fields.txt +22 -0
- data/test/screen_pattern/parser_test/multiple_patterns.txt +42 -0
- data/test/screen_pattern/parser_test/simple_pattern.txt +14 -0
- data/test/screen_pattern/parser_test/truncated_heredoc.txt +12 -0
- data/test/screen_pattern/parser_test/utf16bebom_pattern.bin +0 -0
- data/test/screen_pattern/parser_test/utf16lebom_pattern.bin +0 -0
- data/test/screen_pattern/parser_test/utf8_pattern.bin +14 -0
- data/test/screen_pattern/parser_test/utf8_unix_pattern.bin +14 -0
- data/test/screen_pattern/parser_test/utf8bom_pattern.bin +14 -0
- data/test/screen_pattern/parser_test.rb +266 -0
- data/test/term/dg410/parser_test.rb +139 -0
- data/test/term/xterm_test.rb +327 -0
- data/test/test_helper.rb +3 -0
- data/test/util/transcript/reader_test.rb +131 -0
- data/test/util/transcript/writer_test.rb +126 -0
- data/test.watchr +29 -0
- 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
|