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.
- 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
|