cless 0.3.20

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