cless 0.3.20

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,633 @@
1
+ require 'fcntl'
2
+ require 'cless/assert'
3
+
4
+ # Including class must respond to #each_line
5
+ module MappedCommon
6
+ def parse_header(allowed = [])
7
+ a = []
8
+ i = 0
9
+ each_line do |l|
10
+ break unless l =~ %r{^\s*#(.*)$}
11
+ i += 1
12
+ s = $1
13
+ case s
14
+ when /^\s*cless:(.*)$/
15
+ s = $1.strip
16
+ a += s.split_with_quotes
17
+ when /^\s*(\w+):(.*)$/
18
+ k, v = $1.strip, $2.strip
19
+ a << "--#{k}" << v if allowed.include?(k)
20
+ end
21
+ end
22
+ return i, a
23
+ end
24
+
25
+ def count_lines_upto(stop_off)
26
+ cur = 0
27
+ nb = 0
28
+ while cur <= stop_off do
29
+ cur = @ptr.index("\n", cur)
30
+ if cur
31
+ cur += 1
32
+ nb += 1
33
+ else
34
+ break
35
+ end
36
+ end
37
+ nb
38
+ end
39
+ end
40
+
41
+ # Read from a stream. Write data to a temporary file, which is mmap.
42
+ # Read more data from stream on a need basis, when some index operation fail.
43
+ class MappedStream
44
+ include MappedCommon
45
+
46
+ DEFAULTS = {
47
+ :buf_size => 64*1024,
48
+ :tmp_dir => Dir.tmpdir,
49
+ }
50
+ attr_reader :ptr, :more, :fd
51
+ def initialize(fd, args = {})
52
+ @fd = fd
53
+ flags = fd.fcntl(Fcntl::F_GETFL)
54
+ fd.fcntl(Fcntl::F_SETFL, flags | Fcntl::O_NONBLOCK)
55
+ @more = true
56
+ @buf = ""
57
+ @lines = nil
58
+
59
+ DEFAULTS.each { |k, v|
60
+ instance_variable_set("@#{k}", args[k] || v)
61
+ }
62
+ if false
63
+ # if $have_mmap
64
+ @tfd = Tempfile.new(Process.pid.to_s, @tmp_dir)
65
+ @ptr = Mmap.new(@tfd.path, "w")
66
+ @ptr.extend(10 * @buf_size)
67
+ else
68
+ @ptr = ""
69
+ end
70
+
71
+ if block_given?
72
+ begin
73
+ yield(self)
74
+ ensure
75
+ munmap
76
+ end
77
+ end
78
+ end
79
+
80
+ def file_path; @tfd ? @tfd.path : nil; end
81
+
82
+ def munmap
83
+ @ptr.munmap rescue nil
84
+ @tfd.close! rescue nil
85
+ end
86
+
87
+ def size; @ptr.size; end
88
+ def rindex(*args); @ptr.rindex(*args); end
89
+
90
+ def read_block
91
+ @fd.read_nonblock(@buf_size, @buf)
92
+ @ptr << @buf
93
+ true
94
+ rescue Errno::EWOULDBLOCK, Errno::EAGAIN, Errno::EINTR => e
95
+ false
96
+ rescue EOFError
97
+ @more = false
98
+ false
99
+ end
100
+
101
+
102
+ def index(substr, off = 0)
103
+ loop do
104
+ r = @ptr.index(substr, off) and return r
105
+ return nil unless @more
106
+ off = (@ptr.rindex("\n", @ptr.size) || -1) + 1
107
+ read_block or return nil
108
+ end
109
+ end
110
+
111
+ def search_rindex(*args); rindex(*args); end
112
+ def search_index(substr, off = 0)
113
+ loop do
114
+ r = @ptr.index(substr, off) and return r
115
+ off = (@ptr.rindex("\n", @ptr.size) || -1) + 1
116
+ select_or_cancel(@fd) or return nil
117
+ read_block or return nil
118
+ end
119
+ end
120
+ def more_fd
121
+ @more ? @fd : nil
122
+ end
123
+
124
+ def [](*args); @ptr[*args]; end
125
+
126
+ # Get the total number of lines
127
+ # Stop if line_stop or offset_stop limits are crossed.
128
+ def lines(line_stop = nil, offset_stop = nil)
129
+ return @lines unless @more || @lines.nil?
130
+ lines = @ptr.count("\n")
131
+ while @more
132
+ unless read_block
133
+ select_or_cancel(@fd) or break
134
+ next
135
+ end
136
+ lines += @buf.count("\n")
137
+ return lines if line_stop && lines >= line_stop
138
+ return @ptr.size if offset_stop && @ptr.size >= offset_stop
139
+ end
140
+ @lines = lines
141
+ @lines += 1 if @ptr[-1] != ?\n
142
+ return @lines
143
+ end
144
+
145
+ def each_line
146
+ off = 0
147
+ loop do
148
+ r = @ptr.index("\n", off)
149
+ if r
150
+ yield(@ptr[off..r])
151
+ off = r + 1
152
+ else
153
+ read_block or break
154
+ end
155
+ end
156
+ end
157
+ end
158
+
159
+ class MappedFile
160
+ include MappedCommon
161
+
162
+ attr_reader :file_path
163
+
164
+ def initialize(fname)
165
+ @ptr = Mmap.new(fname)
166
+ @lines = nil
167
+ @file_path = fname
168
+
169
+ if block_given?
170
+ begin
171
+ yield(self)
172
+ ensure
173
+ munmap
174
+ end
175
+ end
176
+ end
177
+
178
+ def size; @ptr.size; end
179
+ def munmap; @ptr.munmap; end
180
+ def rindex(*args); @ptr.rindex(*args); end
181
+ def index(*args); @ptr.index(*args); end
182
+ def search_rindex(*args); rindex(*args); end
183
+ def search_index(*args); index(*args); end
184
+ def [](*args); @ptr[*args]; end
185
+ def each_line(&b); @ptr.each_line(&b); end
186
+ def more_fd; nil; end
187
+
188
+ def lines
189
+ return @lines if @lines
190
+ @lines = @ptr.count("\n")
191
+ @lines += 1 if @ptr[-1] != ?\n
192
+ return @lines
193
+ end
194
+ end
195
+
196
+ # Similar interface to MatchData. Return the entire string as being a match.
197
+ class FieldMatch
198
+ def initialize(str); @str = str; end
199
+ def string; @str; end
200
+ def pre_match; ""; end
201
+ def post_match; ""; end
202
+ def [](i); (i == 0) ? @str : nil; end
203
+ end
204
+
205
+ class Line
206
+ attr_reader :has_match, :off
207
+ attr_accessor :highlight
208
+ alias :highlight? :highlight
209
+
210
+ def initialize(a, onl = nil, off = nil)
211
+ @a, @onl = a, onl
212
+ @m = []
213
+ @has_match = false
214
+ @off = off
215
+ @highlight = false
216
+ end
217
+
218
+ def ignored; false; end
219
+ alias :ignored? :ignored
220
+
221
+ def values_at(*args); @a.values_at(*args); end
222
+ def onl_at(*args); (@onl || @a).values_at(*args); end
223
+ def matches_at(*args); @m.values_at(*args); end
224
+
225
+ # onl is the line before any formatting or transformation.
226
+ # If a field doesn't match pattern but old representation does,
227
+ # hilight entire field as a match.
228
+ def match(pattern)
229
+ does_match = false
230
+ @a.each_with_index { |f, i|
231
+ if m = f.match(pattern)
232
+ does_match = true
233
+ @m[i] = m
234
+ elsif @onl && @onl[i].match(pattern)
235
+ does_match = true
236
+ @m[i] = FieldMatch.new(f)
237
+ end
238
+ }
239
+ @has_match = does_match
240
+ end
241
+
242
+ def clear_match; @has_match = false; @m.clear; end
243
+ end
244
+
245
+ class IgnoredLine
246
+ attr_reader :has_match, :str, :off
247
+
248
+ def initialize(str, off)
249
+ @str = str
250
+ @has_match = false
251
+ @off = off # Byte offset in file of beginning of line
252
+ end
253
+
254
+ def match(pattern)
255
+ if m = @str.match(pattern)
256
+ @m = m
257
+ @has_match = true
258
+ end
259
+ end
260
+
261
+ def matches; @m; end
262
+ def clear_match; @has_match = false; @m = nil; end
263
+
264
+ def ignored; true; end
265
+ alias :ignored? :ignored
266
+
267
+ # An ignored line is never highlighted
268
+ def highlight; false; end
269
+ alias :highlight? :highlight
270
+
271
+ def highlight=(*args); end
272
+ end
273
+
274
+ class MapData
275
+ attr_reader :sizes, :line, :line2, :pattern, :split_regexp
276
+ def initialize(str, split_regexp = nil)
277
+ @str = str
278
+ @line = @line2 = 0
279
+ @off = @off2 = 0 # @off = first character of first line in cache
280
+ # @off2 = first character of first line past cache
281
+ @cache = []
282
+ @sizes = []
283
+ @pattern = nil # search pattern
284
+ @formats = nil # formating strings. When not nil, a hash.
285
+ @ignores = nil # line ignored index or pattern.
286
+ @split_regexp = split_regexp # split pattern
287
+ @highlight_regexp = nil # highlight pattern
288
+ @need_more = false # true if last cache_fill needs data
289
+ end
290
+
291
+ def file_path; @str.file_path; end
292
+ def write_to(fd)
293
+ @str.lines # Make sure we have all the data
294
+ block = 64*1024
295
+
296
+ (@str.size / block).times do |i|
297
+ fd.syswrite(@str[i*block, block])
298
+ end
299
+ if (r = @str.size % block) > 0
300
+ fd.syswrite(@str[@str.size - r, r])
301
+ end
302
+ @str.size
303
+ end
304
+
305
+ # Return a file descriptor to listen on (select) if there is less
306
+ # than n lines in the cache, or nil if not needed, or file is memory
307
+ # mapped and no listening is necessary.
308
+ def select_fd(n)
309
+ @cache.size < n ? @str.more_fd : nil
310
+ end
311
+ # if @need_more && @str.respond_to?(:more) && @str.respond_to?(:fd)
312
+ # @str.more ? @str.fd : nil
313
+ # else
314
+ # nil
315
+ # end
316
+ # end
317
+
318
+ # yield n lines with length len to be displayed
319
+ def lines(n)
320
+ @cache.each_with_index { |l, i|
321
+ break if i >= n
322
+ yield l
323
+ }
324
+ end
325
+
326
+ def goto_start
327
+ return false if @line == 0
328
+ @line = @line2 = 0
329
+ @off = @off2 = 0
330
+ @cache.clear
331
+ return true
332
+ end
333
+
334
+ def goto_end
335
+ old_line = @line2
336
+ @line2 = @str.lines
337
+ return false if @line2 == old_line
338
+ @line = @line2
339
+ @off2 = @off = @str.size
340
+ cache_size = @cache.size
341
+ @cache.clear
342
+ scroll(-cache_size)
343
+ return true
344
+ end
345
+
346
+ def goto_line(nb)
347
+ delta = nb - @line - 1
348
+ scroll(delta)
349
+ end
350
+
351
+ def goto_percent(percent)
352
+ percent = 0.0 if percent < 0.0
353
+ percent = 100.0 if percent > 100.0
354
+ percent = percent.to_f
355
+ line = (@str.lines * percent / 100).round
356
+ goto_line(line)
357
+ end
358
+
359
+ def goto_offset(off)
360
+ @str.lines(nil, off) if off > @str.size
361
+ off -= 1 if @str[off] == ?\n && off > 0
362
+ @off = @off2 = (@str.rindex("\n", off) || -1) + 1
363
+ @line = @line2 = @str.count_lines_upto(@off)
364
+ @cache.clear
365
+ end
366
+
367
+ # Return true if pattern found, false otherwise
368
+ def search(pattern, dir = :forward)
369
+ search_clear if @pattern
370
+ @pattern = pattern
371
+ first_line = nil
372
+ cache = (dir == :forward) ? @cache : @cache.reverse
373
+ cache.each_with_index { |l, i|
374
+ l.match(@pattern) and first_line ||= i
375
+ }
376
+ if first_line
377
+ scroll((dir == :forward) ? first_line : -first_line)
378
+ return true
379
+ else
380
+ return search_next(dir)
381
+ end
382
+ end
383
+
384
+ def repeat_search(dir = :forward)
385
+ first_line = nil
386
+ cache = (dir == :forward) ? @cache : @cache.reverse
387
+ cache[1..-1].each_with_index { |l, i|
388
+ if l.has_match
389
+ first_line = i
390
+ break
391
+ end
392
+ }
393
+ if first_line
394
+ scroll((dir == :forward) ? first_line + 1 : -first_line - 1)
395
+ return true
396
+ else
397
+ return search_next(dir)
398
+ end
399
+ end
400
+
401
+ def search_clear
402
+ @pattern = nil
403
+ @cache.each { |l| l.clear_match }
404
+ end
405
+
406
+ # delta > for scrolling down (forward in file)
407
+ def scroll(delta)
408
+ return if delta == 0
409
+ cache_size = @cache.size
410
+ if delta > 0
411
+ skipped = skip_forward(delta)
412
+ @cache.slice!(0, skipped)
413
+ cache_forward([cache_size, skipped].min)
414
+ else
415
+ skipped = skip_backward(-delta)
416
+ delta = -@cache.size if -delta > @cache.size
417
+ @cache.slice!((delta..-1))
418
+ cache_backward([cache_size, skipped].min)
419
+ end
420
+ end
421
+
422
+ def cache_fill(n)
423
+ @need_more = @cache.size < n
424
+ cache_forward(n - @cache.size) if @need_more
425
+ end
426
+
427
+ def clear_cache
428
+ @cache.clear
429
+ @line2 = @line
430
+ @off2 = @off
431
+ @sizes.clear
432
+ end
433
+
434
+ def refresh
435
+ size = @cache.size
436
+ clear_cache
437
+ cache_fill(size)
438
+ end
439
+
440
+ FMT_LETTERS = "bcdEefGgiIosuXxp"
441
+ FMT_REGEXP = /(^|[^%])(%[ \d#+*.-]*)([#{FMT_LETTERS}])/
442
+ FLOAT_REGEXP = /^[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?$/
443
+ INT_REGEXP = /^[-+]?[0-9]+$/
444
+ FLOAT_PROC = proc { |x| x.to_f }
445
+ INT_PROC = proc { |x| x.to_i }
446
+ BIGINT_PROC = proc { |x|
447
+ x = x.to_f.round
448
+ neg = (x < 0)
449
+ x = x.abs.to_s.reverse.gsub(/(...)/, '\1,').chomp(",").reverse
450
+ neg ? "-#{x}" : x
451
+ }
452
+ PERCENTAGE_PROC = proc { |x|
453
+ x.to_f * 100.0
454
+ }
455
+ def set_format_column(fmt, *cols)
456
+ if fmt =~ FMT_REGEXP
457
+ a = case $3
458
+ when "b", "c", "d", "i", "o", "u", "X", "x"; [fmt, INT_REGEXP, INT_PROC]
459
+ when "E", "e", "f", "G", "g"; [fmt, FLOAT_REGEXP, FLOAT_PROC]
460
+ when "I"; ["#{$`}#{$1}#{$2}s#{$'}", FLOAT_REGEXP, BIGINT_PROC]
461
+ when "p"; ["#{$`}#{$1}#{$2}f%#{$'}", FLOAT_REGEXP, PERCENTAGE_PROC]
462
+ end
463
+ end
464
+ @formats ||= {}
465
+ cols.each { |c| @formats[c] = a }
466
+ true
467
+ end
468
+
469
+ def unset_format_column(col)
470
+ return nil unless @formats
471
+ r = @formats.delete(col)
472
+ @formats = nil if @formats.empty?
473
+ r
474
+ end
475
+
476
+ def get_format_column(col)
477
+ return nil unless @formats
478
+ @formats[col]
479
+ end
480
+
481
+ def formatted_column_list; @formats ? @formats.keys : []; end
482
+
483
+ def add_ignore(pattern)
484
+ return nil unless [Integer, Range, Regexp].any? { |c| c === pattern }
485
+ (@ignored ||= []) << pattern
486
+ true
487
+ end
488
+
489
+ def split_regexp=(regexp)
490
+ @split_regexp = regexp
491
+ clear_cache
492
+ end
493
+
494
+ def highlight_regexp=(regexp)
495
+ @highlight_regexp = regexp
496
+ clear_cache
497
+ end
498
+
499
+ def remove_ignore(pattern)
500
+ if pattern.nil?
501
+ r, @ignored = @ignored, nil
502
+ return r
503
+ end
504
+ return nil unless [Integer, Range, Regexp].any? { |c| c === pattern }
505
+ r = @ignored.delete(pattern)
506
+ @ignored = nil if @ignored.empty?
507
+ r
508
+ end
509
+
510
+ def ignore_pattern_list; @ignored ? @ignored : []; end
511
+
512
+ # Returns the maximum line offset of all lines in cache
513
+ def max_offset
514
+ @cache.collect { |l| l.off }.max
515
+ end
516
+
517
+ private
518
+ def search_next(dir = :forward)
519
+ if dir == :forward
520
+ m = @str.search_index(@pattern, @off2)
521
+ else
522
+ m = @str.search_rindex(@pattern, @off)
523
+ end
524
+ return false if !m
525
+ if dir == :forward
526
+ old_off2, old_line2 = @off2, @line2
527
+ @off = @off2 = (@str.rindex("\n", m) || -1) + 1
528
+ @line = @line2 = old_line2 + @str[old_off2..@off2].count("\n")
529
+ @cache.clear
530
+ else
531
+ old_off, old_line = @off, @line
532
+ @off = @off2 = (@str.rindex("\n", m) || -1) + 1
533
+ @line = @line2 = old_line - @str[@off..old_off].count("\n")
534
+ cache_size = @cache.size
535
+ @cache.clear
536
+ scroll(-cache_size+1)
537
+ end
538
+ return true
539
+ end
540
+
541
+ def reformat(nl)
542
+ onl = nl.dup
543
+ @formats.each do |i, f|
544
+ s = nl[i] or next
545
+ fmt, cond, proc = *f
546
+ cond =~ s or next
547
+ s = proc[s] if proc
548
+ nl[i] = fmt % s rescue "###"
549
+ end
550
+ onl
551
+ end
552
+
553
+ def line_ignore?(str, i)
554
+ @ignored.any? do |pat|
555
+ case pat
556
+ when Range, Integer; pat === i
557
+ when Regexp; pat === str
558
+ else false
559
+ end
560
+ end
561
+ end
562
+
563
+ # str = line
564
+ # i = line number
565
+ def line_massage(str, i, off)
566
+ if @ignored && line_ignore?(str, i)
567
+ l = IgnoredLine.new(str, off)
568
+ else
569
+ onl, nl = nil, str.split(@split_regexp)
570
+ onl = reformat(nl) if @formats
571
+ @sizes.max_update(nl.collect { |x| x.size })
572
+ l = Line.new(nl, onl, off)
573
+ end
574
+ l.match(@pattern) if @pattern
575
+ l.highlight = @highlight_regexp && (@highlight_regexp =~ str)
576
+ l
577
+ end
578
+
579
+ def cache_forward(n)
580
+ lnb = @line + @cache.size
581
+ n.times do |i|
582
+ noff2 = @str.index("\n", @off2) or break
583
+ @cache << line_massage(@str[@off2, noff2 - @off2], lnb + i, @off2)
584
+ @off2 = noff2 + 1
585
+ end
586
+ @line2 = @line + @cache.size
587
+ end
588
+
589
+ def cache_backward(n)
590
+ lnb = @line - 1
591
+ n.times do |i|
592
+ break if @off == 0
593
+ ooff = @off
594
+ @off = search_backward_to_new_line(@off)
595
+ @cache.unshift(line_massage(@str[@off, ooff - @off - 1], lnb - i, @off))
596
+ end
597
+ @line = @line2 - @cache.size
598
+ end
599
+
600
+ def search_backward_to_new_line(start)
601
+ return 0 if start < 2
602
+ npos = @str.rindex("\n", start - 2)
603
+ return npos ? npos + 1 : 0
604
+ end
605
+
606
+ # Move @off by n lines. Make sure that @off2 >= @off
607
+ def skip_forward(n)
608
+ i = 0
609
+ n.times do
610
+ noff = @str.search_index("\n", @off) or break
611
+ @off = noff + 1
612
+ i += 1
613
+ end
614
+ @off2 = @off if @off2 < @off
615
+ @line += i
616
+ @line2 = @line if @line > @line2
617
+ i
618
+ end
619
+
620
+ # Move @off2 back by n lines. Make sure that @off <= @off2
621
+ def skip_backward(n)
622
+ i = 0
623
+ n.times do
624
+ break if @off2 == 0
625
+ @off2 = search_backward_to_new_line(@off2)
626
+ i += 1
627
+ end
628
+ @off = @off2 if @off > @off2
629
+ @line2 -= i
630
+ @line = @line2 if @line2 < @line
631
+ i
632
+ end
633
+ end