rbfind 1.7.1 → 2.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -2,42 +2,13 @@
2
2
  # rbfind.rb -- Find replacement with many features
3
3
  #
4
4
 
5
- # :stopdoc:
6
- unless String.public_method_defined? :ord then
7
- class String ; def ord ; self[0].ord ; end ; end
8
- end
9
- # :startdoc:
10
-
11
- class Dir
12
-
13
- SPECIAL_DIRS = %w(. ..)
14
- CUR_DIR, SUPER_DIR = *SPECIAL_DIRS
15
-
16
- # :call-seq:
17
- # each!() { |e| ... } -> self
18
- #
19
- # Call block for all entries except "." and "..".
20
- #
21
- def each!
22
- s = SPECIAL_DIRS.dup
23
- each { |f|
24
- next if s.delete f
25
- yield f
26
- }
27
- end
28
-
29
- # :call-seq:
30
- # entries!() -> ary
31
- #
32
- # All entries except "." and "..".
33
- #
34
- method_defined? :entries! or def entries!
35
- entries - SPECIAL_DIRS
36
- end
5
+ require "rbfind/core"
6
+ require "rbfind/csv"
37
7
 
38
- end
39
8
 
9
+ module RbFind
40
10
 
11
+ VERSION = "2.0.1".freeze
41
12
 
42
13
  =begin rdoc
43
14
 
@@ -48,130 +19,130 @@ tool.
48
19
 
49
20
  In Ruby programs, you may call:
50
21
 
51
- RbFind.open do |f| puts f.path end
52
- RbFind.open "dir" do |f| puts f.path end
53
- RbFind.open "dir1", "dir2" do |f| puts f.path end
54
- RbFind.open %w(dir1 dir2) do |f| puts f.path end
55
- RbFind.open "dir", :max_depth => 3 do |f| puts f.path end
56
-
57
- # more terse
58
- RbFind.run do puts path end
59
- RbFind.run "dir" do puts path end
22
+ RbFind.run do puts path end
23
+ RbFind.run "dir" do puts path end
24
+ RbFind.run "dir1", "dir2" do puts path end
25
+ RbFind.run %w(dir1 dir2) do puts path end
26
+ RbFind.run "dir", :max_depth => 3 do puts path end
60
27
 
61
28
 
62
29
  == File properties
63
30
 
64
- f.name # file name (*)
65
- f.path # file path relative to working directory (*)
66
- f.fullpath # full file path (*)
67
- f.path! # directories with a slash appended (*)
68
- f.fullpath! # directories with a slash appended (*)
69
- f.dirname # dirname of path
70
- f.ext # file name extension
71
- f.without_ext # file name without extension
72
- f.depth # step depth
73
- f.hidden? # filename starting with "." (No Windows version, yet.)
74
- f.visible? # not hidden?
75
- f.stat # file status information (File::Stat object)
76
- f.mode # access mode (like 0755, 0644)
77
- f.age # age in seconds since walk started
78
- f.age_s # dto. (alias)
79
- f.age_secs # dto. (alias)
80
- f.age_m # age in minutes
81
- f.age_mins # dto. (alias)
82
- f.age_h # age in hours
83
- f.age_hours # dto. (alias)
84
- f.age_d # age in days
85
- f.age_days # dto. (alias)
86
- f.user # owner
87
- f.owner # dto. (alias)
88
- f.group # group owner
89
- f.readlink # symlink pointer or nil (*)
90
- f.broken_link? # what you expect
91
- f.arrow # ls-style "-> symlink" suffix (*)
31
+ name # file name (*)
32
+ path # file path relative to working directory (*)
33
+ fullpath # full file path (*)
34
+ path! # directories with a slash appended (*)
35
+ fullpath! # directories with a slash appended (*)
36
+ dirname # dirname of path
37
+ ext # file name extension
38
+ without_ext # file name without extension
39
+ depth # step depth
40
+ hidden? # filename starting with "." (No Windows version, yet.)
41
+ visible? # not hidden?
42
+ stat # file status information (File::Stat object)
43
+ mode # access mode (like 0755, 0644)
44
+ mtime # modify time (atime, ctime as well)
45
+ mage # age in seconds since walk started
46
+ age # alias for mage
47
+ user # owner
48
+ owner # dto. (alias)
49
+ group # group owner
50
+ user! # owner, "." if process owner
51
+ owner! # dto. (alias)
52
+ group! # group owner, "." if process owner
53
+ readlink # symlink pointer or nil (*)
54
+ broken_link? # what you expect
55
+ arrow # ls-style "-> symlink" suffix (*)
92
56
 
93
57
  # (*) = colored version available (see below)
94
58
 
95
- f.empty? # directory is empty
96
- f.entries # directory entries
59
+ empty? # directory is empty
60
+ entries # directory entries
97
61
 
98
- f.open { |o| ... } # open file
99
- f.read n = nil # read first n bytes, nil reads to eof
100
- f.lines { |l,i| ... } # open file and yield each |line,lineno|
101
- f.grep re # lines with `l =~ re and colsep path, i, l'
102
- f.binary? n = 1 # test whether first n blocks contain null characters
103
- f.bin? # alias for binary?
62
+ open { |o| ... } # open file
63
+ read n = nil # read first n bytes, nil reads to eof
64
+ lines { |l,i| ... } # open file and yield each |line,lineno|
65
+ grep re # lines with `l =~ re and colsep path, i, l'
66
+ binary? n = 1 # test whether first n blocks contain null characters
67
+ bin? # alias for binary?
104
68
 
105
- f.vimswap? # it is a Vim swapfile
69
+ vimswap? # it is a Vim swapfile
106
70
 
107
71
  Further will be redirected to the stat object (selective):
108
72
 
109
- f.directory?
110
- f.executable?
111
- f.file?
112
- f.pipe?
113
- f.socket?
114
- f.symlink?
73
+ directory?
74
+ executable?
75
+ file?
76
+ pipe?
77
+ socket?
78
+ symlink?
115
79
 
116
- f.readable?
117
- f.writable?
118
- f.size
119
- f.zero?
80
+ readable?
81
+ writable?
82
+ size
83
+ zero?
120
84
 
121
- f.uid
122
- f.gid
123
- f.owned?
124
- f.grpowned?
85
+ uid
86
+ gid
87
+ owned?
88
+ grpowned?
125
89
 
126
- f.dir? # alias for f.directory?
90
+ dir? # alias for directory?
127
91
 
128
92
  Derivated from stat:
129
93
 
130
- f.stype # one-letter (short) version of ftype
131
- f.modes # rwxr-xr-x style modes
94
+ stype # one-letter (short) version of ftype
95
+ modes # rwxr-xr-x style modes
132
96
 
133
- f.filesize # returns size for files, else nil
134
- f.filesize { |s| s > 1024 } # returns block result for files
97
+ filesize # returns size for files, else nil
98
+ filesize { |s| s > 1024 } # returns block result for files
135
99
 
136
100
 
137
101
  == Actions
138
102
 
139
- f.prune # do not descend directory; abort current entry
140
- f.novcs # omit .svn, CVS and .git directories
103
+ done # exit from current entry
104
+ done! # dto.
105
+ prune # do not descend directory; abort current entry
106
+ prune! # dto.
107
+ no_vcs # omit .svn, CVS and .git directories
108
+ novcs # dto.
141
109
 
142
- f.colsep path, ... # output parameters in a line separated by colons
143
- f.col_sep # dto. (alias)
144
- f.tabsep path, ... # separate by tabs
145
- f.tab_sep #
146
- f.spcsep path, ... # separate by spaces
147
- f.spc_sep #
148
- f.spacesep path, ... #
149
- f.space_sep #
150
- f.csv sep, path, ... # separate by user-defined separator
110
+ colsep path, ... # output parameters in a line separated by colons
111
+ col_sep # dto. (alias)
112
+ tabsep path, ... # separate by tabs
113
+ tab_sep #
114
+ spcsep path, ... # separate by spaces
115
+ spc_sep #
116
+ spacesep path, ... #
117
+ space_sep #
118
+ p # alias for space_sep
119
+ csv sep, path, ... # separate by user-defined separator
151
120
 
152
- f.rename newname # rename, but leave it in the same directory
121
+ rename newname # rename, but leave it in the same directory
153
122
 
154
123
 
155
124
  == Color support
156
125
 
157
- f.cname # colorized name
158
- f.cpath # colorized path
159
- f.cpath! # colorized path!
160
- f.cfullpath # colorized fullpath
161
- f.cfullpath! # colorized fullpath!
162
- f.creadlink # colored symlink pointer
163
- f.carrow # colored "-> symlink" suffix
126
+ cname # colorized name
127
+ cpath # colorized path
128
+ cpath! # colorized path!
129
+ cfullpath # colorized fullpath
130
+ cfullpath! # colorized fullpath!
131
+ creadlink # colored symlink pointer
132
+ carrow # colored "-> symlink" suffix
164
133
 
165
- f.color arg # colorize argument
166
- f.colour arg # alias
134
+ color arg # colorize argument
135
+ colour arg # alias
167
136
 
168
137
  RbFind.colors str # define colors
169
138
  RbFind.colours str # alias
170
139
 
171
140
  Default color setup is "xxHbexfxcxdxbxegedabagacadAx".
172
- In case you did not call RbFind.colors, the environment variables
141
+ In case you did not call RbFind::Walk.colors, the environment variables
173
142
  RBFIND_COLORS and RBFIND_COLOURS are looked up. If neither is given
174
143
  but LSCOLORS is set, the fields 2-13 default to that.
144
+ A Gnu LS_COLOR-style string may also be given, though glob patterns will
145
+ not be unregarded. If LS_COLORS is set, the colors default to that.
175
146
 
176
147
  The letters mean:
177
148
  a = black, b = red, c = green, d = brown, e = blue,
@@ -197,7 +168,7 @@ Derivated from stat:
197
168
  13 whiteout
198
169
  14 unknown
199
170
 
200
- f.suffix # ls-like suffixes |@=/%* for pipe, ..., executable
171
+ suffix # ls-like suffixes |@=/%* for pipe, ..., executable
201
172
 
202
173
 
203
174
  == Encoding issues
@@ -205,12 +176,12 @@ Derivated from stat:
205
176
  Ruby raises an ArgumentError if, for example, an ISO8859-1-encoded
206
177
  string gets read in as UTF-8-encoded and then is matched against a
207
178
  UTF-8-encoded regular expression. This will happen if you are
208
- running RbFind from an environment with something like
209
- LC_ALL="de_DE.UTF-8" and if you are searching directories containing
179
+ running RbFind::Walk from an environment with something like
180
+ LANG="de_DE.UTF-8" and if you are searching directories containing
210
181
  single-byte encoded file names or files with single-byte or binary
211
182
  content.
212
183
 
213
- The #grep facility will will condone encoding mismatches by calling
184
+ The #grep facility will condone encoding mismatches by calling
214
185
  the String#scrub! method. But neither the #lines and #read function
215
186
  will do any transformation nor will the file names and the path
216
187
  specifications be changed.
@@ -227,727 +198,612 @@ variables because these will be used in the further processing.
227
198
 
228
199
  Find them all:
229
200
 
230
- RbFind.open do |f| puts f.path end
201
+ RbFind.run do puts path end
231
202
 
232
203
  Omit version control:
233
204
 
234
- RbFind.open "myproject" do |f|
235
- f.prune if f.name == ".svn"
236
- puts f.path
205
+ RbFind.run "myproject" do
206
+ prune if name == ".svn"
207
+ puts path
237
208
  end
238
209
 
239
210
  # or even
240
- RbFind.open "myproject" do |f|
241
- f.novcs
242
- puts f.path
211
+ RbFind.run "myproject" do
212
+ novcs
213
+ puts path
243
214
  end
244
215
 
245
216
  Mention directory contents before directory itself:
246
217
 
247
- RbFind.open "myproject", :depth => true do |f|
248
- puts f.path
218
+ RbFind.run "myproject", depth_first: true do
219
+ puts path
249
220
  end
250
221
 
251
222
  Limit search depth:
252
223
 
253
- RbFind.open :max_depth => 2 do |f|
254
- puts f.path
224
+ RbFind.run max_depth: 2 do
225
+ puts path
255
226
  end
256
227
 
257
228
  Unsorted (alphabetical sort is default):
258
229
 
259
- RbFind.open :sort => false do |f|
260
- puts f.path
230
+ RbFind.run sort: false do
231
+ puts path
261
232
  end
262
233
 
263
234
  Reverse sort:
264
235
 
265
- RbFind.open :sort => -1 do |f|
266
- puts f.path
236
+ RbFind.run sort: true, reverse: true do
237
+ puts path
267
238
  end
268
239
 
269
240
  Sort without case sensitivity and preceding dot:
270
241
 
271
242
  s = proc { |x| x =~ /^\.?/ ; $'.downcase }
272
- RbFind.open :sort => s do |f|
273
- puts f.path
243
+ RbFind.run sort: s do
244
+ puts path
274
245
  end
275
246
 
276
247
  =end
277
248
 
278
- class RbFind
279
249
 
280
- VERSION = "1.7.1".freeze
250
+ class Done < Exception ; end
251
+ class Prune < Exception ; end
281
252
 
282
253
  class <<self
283
- private :new
284
- def open *args, &block
285
- params = case args.last
286
- when Hash then args.pop
287
- end
288
- args.flatten!
289
- if args.any? then
290
- count = 0
291
- args.each do |path|
292
- f = new path, count, params, &block
293
- count = f.count
294
- end
295
- else
296
- f = new nil, 0, params, &block
297
- f.count
298
- end
299
- end
300
- def run *args, &block
301
- open *args do |f| f.instance_eval &block end
254
+ def run *args, **params, &block
255
+ Walk.run *args, **params, &block
302
256
  end
303
257
  end
304
258
 
305
- attr_reader :count, :wd, :start
306
-
307
- def initialize path, count, params = nil, &block
308
- @levels = []
309
- @block = block
310
-
311
- if params then
312
- params = params.dup
313
- dl = :do_level_depth if params.delete :depth
314
- md = params.delete :max_depth ; @max_depth = md.to_i if md
315
- st = params.delete :sort ; @sort = sort_parser st
316
- @follow = params.delete :follow
317
- @error = params.delete :error
318
- params.empty? or
319
- raise RuntimeError, "Unknown parameter(s): #{params.keys.join ','}."
320
- end
321
- @do_level = method dl||:do_level
322
-
323
- @start, @count = Time.now, count
324
- @wd = Dir.getwd
325
- if path then
326
- File.lstat path
327
- @wd = nil unless absolute_path? path
328
- @levels.push path
329
- walk
330
- else
331
- build_path
332
- scan_dir
259
+ class Walk
260
+
261
+ class <<self
262
+ def run *args, **params, &block
263
+ i = new **params, &block
264
+ i.run *args
265
+ i.count
266
+ end
333
267
  end
334
- end
335
268
 
336
- private
269
+ private
337
270
 
338
- def append_slash s ; (File.directory? s) ? (File.join s, "") : s ; end
271
+ def initialize max_depth: nil, depth_first: nil, follow: nil,
272
+ sort: true, reverse: false, error: nil, &block
273
+ @max_depth = max_depth
274
+ @depth_first = depth_first
275
+ @follow = follow
276
+ @sort = sort_parser sort, reverse
277
+ @error = error
278
+ @block = block
339
279
 
340
- public
280
+ ostat = $stdout.stat
281
+ @ostat = ostat if ostat.file?
341
282
 
342
- def name ; @levels.last ; end
343
- def path ; @path ; end
344
- def fullpath ; @fullpath ; end
345
- def path! ; append_slash @path ; end
346
- def fullpath! ; append_slash @fullpath ; end
283
+ @wd, @start, @count = Dir.getwd, Time.now, 0
284
+ end
347
285
 
348
- def dirname
349
- d = File.dirname @fullpath
350
- File.basename d
351
- end
286
+ def sort_parser st, rev
287
+ r = case st
288
+ when Proc then proc { |l| l.sort_by! &st }
289
+ when nil, false, nil, "" then proc { }
290
+ else proc { |l| l.sort! }
291
+ end
292
+ rev ? proc { |l| r.call l ; l.reverse! } : r
293
+ end
352
294
 
353
- def ext ; File.extname name ; end
354
- def without_ext ; name[ /^(.+?)(?:\.[^.]+)?$/, 1 ].to_s ; end
295
+ public
355
296
 
356
- def depth ; @levels.size ; end
297
+ attr_reader :wd, :start, :count, :depth
357
298
 
358
- def hidden? ; name =~ /^\./ ; end
359
- def visible? ; not hidden? ; end
299
+ def run *args
300
+ @levels, @depth = [], 0
301
+ args.flatten!
302
+ args.compact!
303
+ if args.empty? then
304
+ visit_dir Dir::CUR_DIR
305
+ else
306
+ args.each { |base|
307
+ handle_error do
308
+ File.exists? base or raise "`#{base}` doesn't exist."
309
+ visit_depth base
310
+ end
311
+ }
312
+ end
313
+ ensure
314
+ @levels = @depth = nil
315
+ end
360
316
 
361
- def stat ; File.lstat @path ; end
362
- def mode ; stat.mode ; end
317
+ private
363
318
 
364
- def readlink
365
- File.readlink @path if stat.symlink?
366
- end
319
+ def join_path
320
+ (File.join @levels).freeze
321
+ end
367
322
 
368
- def creadlink
369
- if stat.symlink? then
370
- s = File.stat @path rescue nil
371
- l = File.readlink @path
372
- (col_stat l, s)
323
+ def visit filename
324
+ @depth += 1
325
+ visit_depth filename
326
+ ensure
327
+ @depth -= 1
373
328
  end
374
- end
375
329
 
376
- # :call-seq:
377
- # broken_link?() -> true or false
378
- #
379
- def broken_link?
380
- return unless stat.symlink?
381
- !File.stat @path rescue true
382
- end
330
+ def visit_depth filename
331
+ @levels.push filename.dup.freeze
332
+ p_, @path = @path, join_path
333
+ if @depth_first then
334
+ enter_dir
335
+ call_block or raise "#{self.class}: prune doesn't work with :depth_first."
336
+ else
337
+ call_block and enter_dir
338
+ end
339
+ @count += 1
340
+ ensure
341
+ @path = p_
342
+ @levels.pop
343
+ end
344
+
345
+ def enter_dir
346
+ return unless File.directory? @path
347
+ if File.symlink? @path then
348
+ return unless @follow and handle_error do
349
+ d = @path.dup
350
+ while d != Dir::CUR_DIR do
351
+ d, = File.split d
352
+ raise "cyclic recursion in #@path" if File.identical? d, @path
353
+ end
354
+ true
355
+ end
356
+ end
357
+ handle_error do
358
+ visit_dir @path
359
+ end
360
+ end
383
361
 
384
- ARROW = " -> " # :nodoc:
362
+ def visit_dir dir
363
+ return if @max_depth and @max_depth == @depth
364
+ list = (Dir.new dir).children
365
+ @sort.call list
366
+ list.each { |f| visit f }
367
+ ensure
368
+ end
385
369
 
386
- def arrow
387
- ARROW + (File.readlink @path) if stat.symlink?
388
- end
370
+ def call_block
371
+ e = Entry.new @levels.last, @path, self
372
+ handle_error do
373
+ $_, $. = e.name, count
374
+ begin
375
+ e.instance_eval &@block
376
+ rescue Done
377
+ ensure
378
+ if !(e.name.equal? @levels.last) && e.name != @levels.last then
379
+ p = @path
380
+ e.name == (File.basename e.name) or
381
+ raise "#{self.class}: rename to `#{e.name}' may not be a path."
382
+ e.name.freeze
383
+ @levels.pop
384
+ @levels.push e.name
385
+ @path = join_path
386
+ File.rename p, @path
387
+ end
388
+ end
389
+ true
390
+ end
391
+ rescue Prune
392
+ end
389
393
 
390
- def carrow
391
- r = creadlink
392
- ARROW + r if r
393
- end
394
+ def handle_error
395
+ yield
396
+ rescue
397
+ case @error
398
+ when Proc then @error.call
399
+ when String then instance_eval @error
400
+ else raise
401
+ end
402
+ nil
403
+ end
394
404
 
395
- def now ; @start ; end
396
- def age ; @start - stat.mtime ; end
397
- alias age_secs age
398
- alias age_s age_secs
399
-
400
- # :stopdoc:
401
- MINUTE = 60
402
- HOUR = 60*MINUTE
403
- DAY = 24*HOUR
404
- # :startdoc:
405
-
406
- def age_mins ; age / MINUTE ; end
407
- alias age_m age_mins
408
- def age_hours ; age / HOUR ; end
409
- alias age_h age_hours
410
- def age_days ; age / DAY ; end
411
- alias age_d age_days
412
-
413
- private
414
-
415
- def method_missing sym, *args, &block
416
- stat.send sym, *args, &block
417
- rescue NoMethodError
418
- super
419
405
  end
420
406
 
421
- public
422
-
423
- def dir? ; stat.directory? ; end
424
-
425
- # :call-seq:
426
- # stype() -> str
427
- #
428
- def stype
429
- m = stat.mode >> 12 rescue nil
430
- case m
431
- when 001 then "p"
432
- when 002 then "c"
433
- when 004 then "d"
434
- when 006 then "b"
435
- when 010 then "-"
436
- when 012 then "l"
437
- when 014 then "s"
438
- when 016 then "w"
439
- when nil then "#"
440
- else "?"
407
+
408
+ class Entry
409
+
410
+ attr_reader :path, :name
411
+
412
+ def initialize name, path, walk
413
+ @name, @path, @walk = name, path, walk
441
414
  end
442
- end
443
415
 
444
- # :call-seq:
445
- # modes() -> str
446
- #
447
- def modes
448
- m = stat.mode
449
- r = ""
450
- 3.times {
451
- h = m & 07
452
- m >>= 3
453
- r.insert 0, ((h & 01).nonzero? ? "x" : "-")
454
- r.insert 0, ((h & 02).nonzero? ? "w" : "-")
455
- r.insert 0, ((h & 04).nonzero? ? "r" : "-")
456
- }
457
- if (m & 04).nonzero? then
458
- r[ 2] = r[ 2, 1] == "x" ? "s" : "S"
459
- end
460
- if (m & 02).nonzero? then
461
- r[ 5] = r[ 5, 1] == "x" ? "s" : "S"
462
- end
463
- if (m & 01).nonzero? then
464
- r[ 8] = r[ 8, 1] == "x" ? "t" : "T"
465
- end
466
- r
467
- end
416
+ def depth ; @walk.depth ; end
417
+ def now ; @walk.start ; end
418
+
419
+ def fullpath ; @fullpath ||= File.absolute_path @path, @walk.wd ; end
420
+
421
+ def stat ; @stat ||= File.lstat @path ; end
422
+ def rstat ; @rstat ||= File.stat @path ; end
423
+
468
424
 
469
- # :call-seq:
470
- # filesize => nil or int
471
- # filesize { |size| ... } => obj
472
- #
473
- # Returns the files size. When the object is not a regular file,
474
- # nil will be returned or the block will not be called.
475
- #
476
- def filesize
477
- if block_given? then
478
- yield stat.size if file?
479
- else
480
- stat.size if file?
425
+ private
426
+ def append_slash s ; (File.directory? s) ? (File.join s, "") : s ; end
427
+ public
428
+
429
+ def path! ; append_slash path ; end
430
+ def fullpath! ; append_slash fullpath ; end
431
+
432
+ def dirname
433
+ File.basename File.dirname fullpath
481
434
  end
482
- end
483
435
 
484
436
 
485
- def cname ; color name ; end
486
- def cpath ; color path ; end
487
- def cfullpath ; color fullpath ; end
488
- def cpath! ; color path! ; end
489
- def cfullpath! ; color fullpath! ; end
437
+ def ext ; File.extname name ; end
438
+ def without_ext ; name[ /^(.+?)(?:\.[^.]+)?$/, 1 ].to_s ; end
490
439
 
491
- def color arg
492
- col_stat arg, stat
493
- end
494
- alias colour color
440
+ def hidden? ; name =~ /^\./ ; end
441
+ def visible? ; not hidden? ; end
495
442
 
496
- DEFAULT_COLORS = "xxHbexfxcxdxbxegedabagacadAx"
497
443
 
498
- class <<self
444
+ def mode ; stat.mode ; end
499
445
 
500
- def colors str
501
- @cols = []
502
- str.scan /(.)(.)/i do
503
- fg, bg = $~.captures.map { |x| x.downcase.ord - ?a.ord }
504
- a = []
505
- case fg
506
- when 0..7 then a.push 30 + fg
507
- end
508
- a.push 1 if $1 == $1.upcase
509
- case bg
510
- when 0..7 then a.push 40 + bg
511
- end
512
- e = a.join ";"
513
- @cols.push e
446
+ private
447
+ def method_missing sym, *args, &block
448
+ if stat.respond_to? sym then
449
+ stat.send sym, *args, &block
450
+ else
451
+ super
514
452
  end
515
453
  end
516
- alias colours colors
454
+ public
455
+
456
+ def dir? ; stat.directory? ; end
517
457
 
518
- def colored arg, num
519
- @cols or colors col_str
520
- "\e[#{@cols[num]}m#{arg}\e[m"
458
+ def aage ; @walk.start - stat.atime ; end
459
+ def mage ; @walk.start - stat.mtime ; end
460
+ def cage ; @walk.start - stat.ctime ; end
461
+ alias age mage
462
+
463
+ # :call-seq:
464
+ # filesize => nil or int
465
+ # filesize { |size| ... } => obj
466
+ #
467
+ # Returns the files size. When the object is not a regular file,
468
+ # nil will be returned or the block will not be called.
469
+ #
470
+ def filesize
471
+ stat.file? or return
472
+ if block_given? then
473
+ yield stat.size
474
+ else
475
+ stat.size
476
+ end
521
477
  end
522
- alias coloured colored
523
478
 
524
479
  private
480
+ def etc
481
+ Etc
482
+ rescue NameError
483
+ require "etc" and retry
484
+ raise
485
+ end
486
+ def get_user u ; (etc.getpwuid u).name rescue u.to_s ; end
487
+ def get_group g ; (etc.getgrgid g).name rescue g.to_s ; end
488
+ public
525
489
 
526
- def col_str
527
- ENV[ "RBFIND_COLORS"] || ENV[ "RBFIND_COLOURS"] || (
528
- env = DEFAULT_COLORS.dup
529
- els = ENV[ "LSCOLORS"]
530
- if els then
531
- env[ 2*2, els.length] = els
532
- end
533
- env
534
- )
490
+ def user
491
+ get_user stat.uid
535
492
  end
493
+ alias owner user
536
494
 
537
- end
495
+ def user!
496
+ u = stat.uid
497
+ u == Process.uid ? "." : (get_user u)
498
+ end
499
+ alias owner! user!
538
500
 
501
+ def group
502
+ get_group stat.gid
503
+ end
539
504
 
540
- # :call-seq:
541
- # suffix() -> str
542
- #
543
- def suffix
544
- m = stat.mode >> 12 rescue nil
545
- case m
546
- when 001 then "|"
547
- when 002 then " "
548
- when 004 then "/"
549
- when 006 then " "
550
- when 010 then stat.executable? ? "*" : " "
551
- when 012 then "@"
552
- when 014 then "="
553
- when 016 then "%"
554
- else "?"
505
+ def group!
506
+ g = stat.gid
507
+ g == Process.gid ? "." : (get_group g)
555
508
  end
556
- end
557
509
 
558
- autoload :Etc, "etc"
559
510
 
560
- # :call-seq:
561
- # user() -> str
562
- #
563
- # Return user name or uid as string if unavailable.
564
- #
565
- def user
566
- u = stat.uid
567
- (Etc.getpwuid u).name rescue u.to_s
568
- end
569
- alias owner user
570
-
571
- # :call-seq:
572
- # group() -> str
573
- #
574
- # Return group name or gid as string if unavailable.
575
- #
576
- def group
577
- g = stat.gid
578
- (Etc.getgrgid g).name rescue g.to_s
579
- end
511
+ def readlink ; File.readlink @path if stat.symlink? ; end
580
512
 
513
+ def broken_link?
514
+ return if stat.symlink?
515
+ rstat
516
+ false
517
+ rescue
518
+ true
519
+ end
581
520
 
582
- # :call-seq:
583
- # empty?() -> true or false
584
- #
585
- # Look up if the directory is empty. If the object is not a directory,
586
- # +nil+ is returned.
587
- #
588
- def empty?
589
- read_dir.each! { |f| return false }
590
- true
591
- rescue Errno::ENOTDIR
592
- end
521
+ ARROW = " -> "
522
+ def arrow
523
+ ARROW + (File.readlink @path) if stat.symlink?
524
+ end
593
525
 
594
- # :call-seq:
595
- # contains?( name) -> true or false
596
- #
597
- # Check whether a directory contains an entry.
598
- #
599
- def contains? name
600
- c = File.join @path, name
601
- File.exists? c
602
- end
603
526
 
604
- # :call-seq:
605
- # entires() -> ary
606
- #
607
- # Return all entries in an array. If the object is not a directory,
608
- # +nil+ is returned.
609
- #
610
- def entries
611
- read_dir.entries!
612
- rescue Errno::ENOTDIR
613
- end
614
527
 
615
- # :call-seq:
616
- # open() { |h| ... } -> obj
617
- #
618
- # Open the file for reading. If the object is not a regular file,
619
- # nothing will be done.
620
- #
621
- def open &block
622
- handle_error Errno::EACCES do
528
+ # :call-seq:
529
+ # empty?() -> true or false
530
+ #
531
+ # Look up if the directory is empty. If the object is not a directory
532
+ # or not accessible, +nil+ is returned.
533
+ #
534
+ def empty?
535
+ (Dir.new @path).each_child { |f| return false }
536
+ true
537
+ rescue Errno::ENOTDIR
538
+ end
539
+
540
+ # :call-seq:
541
+ # contains?( name) -> true or false
542
+ #
543
+ # Check whether a directory contains an entry.
544
+ #
545
+ def contains? name
546
+ File.exists? File.join @path, name
547
+ end
548
+
549
+ # :call-seq:
550
+ # entries() -> ary
551
+ #
552
+ # Return all entries in an array. If the object is not a directory,
553
+ # +nil+ is returned.
554
+ #
555
+ def entries
556
+ (Dir.new @path).children
557
+ rescue Errno::ENOTDIR
558
+ end
559
+ alias children entries
560
+
561
+
562
+ def vcs?
563
+ %w(CVS .svn .git .hg .fslckout).include? name
564
+ end
565
+
566
+
567
+ # :call-seq:
568
+ # open() { |h| ... } -> obj
569
+ #
570
+ # Open the file for reading. If the object is not a regular file,
571
+ # nothing will be done.
572
+ #
573
+ def open &block
574
+ @ostat and @ostat.identical? @path and
575
+ raise "Refusing to open output file."
623
576
  File.open @path, &block if file?
624
577
  end
625
- end
626
578
 
627
- # :call-seq:
628
- # read( n = nil) -> str or nil
629
- # read( n = nil) { |b| ... } -> nil
630
- #
631
- # Read the first +n+ bytes or return +nil+ for others than regular
632
- # files. +nil+ reads to end of file. If a block is given, chonks of
633
- # +n+ bytes (or all) will be yielded.
634
- #
635
- def read n = nil
636
- open { |o|
637
- if block_given? then
638
- while (r = o.read n) do
639
- yield r
579
+ # :call-seq:
580
+ # read( n = nil) -> str or nil
581
+ # read( n = nil) { |b| ... } -> nil
582
+ #
583
+ # Read the first +n+ bytes or return +nil+ for others than regular
584
+ # files. +nil+ reads to end of file. If a block is given, chonks of
585
+ # +n+ bytes (or all) will be yielded.
586
+ #
587
+ def read n = nil
588
+ open { |o|
589
+ if block_given? then
590
+ if n then
591
+ while (r = o.read n) do
592
+ yield r
593
+ end
594
+ else
595
+ yield o.read
596
+ end
597
+ else
598
+ o.read n
640
599
  end
641
- else
642
- o.read n
643
- end
644
- }
645
- end
600
+ }
601
+ end
646
602
 
647
- # :call-seq:
648
- # lines { |l,i| ... } -> nil
649
- #
650
- # Yield line by line together with the line number <code>i</code>.
651
- #
652
- def lines
653
- block_given? or return lines do end
654
- open { |file|
655
- n = 0
656
- file.each_line { |l|
657
- l.chomp!
658
- n += 1
659
- set_predefs l, n
660
- yield l, n
603
+ # :call-seq:
604
+ # lines { |l,i| ... } -> nil
605
+ #
606
+ # Yield line by line together with the line number <code>i</code>.
607
+ #
608
+ def lines
609
+ block_given? or return lines do end
610
+ r = false
611
+ open { |file|
612
+ n = 0
613
+ file.each_line { |l|
614
+ l.chomp!
615
+ n += 1
616
+ $_, $. = l, n
617
+ r ||= true if yield l, n
618
+ }
619
+ r
661
620
  }
662
- n
663
- }
664
- end
621
+ end
665
622
 
666
- def grep re, color = nil
667
- case color
668
- when /\A\d+(?:;\d+)*\z/, nil, false then
669
- when true then color = "31;1" # red
670
- else raise "Illegal color spec: #{color}"
671
- end
672
- lines { |l,i|
673
- l.scrub!
674
- l =~ re or next
675
- if color then
676
- l = "#$`\e[#{color}m#$&\e[m#$'"
623
+ def grep re, color = nil
624
+ case color
625
+ when /\A\d+(?:;\d+)*\z/, nil, false then
626
+ when true then color = "31;1" # red
627
+ else raise "Illegal color spec: #{color}"
677
628
  end
678
- colsep @path, i, l
679
- }
680
- end
629
+ lines { |l,i|
630
+ l.scrub!
631
+ l =~ re or next
632
+ color and l = "#$`\e[#{color}m#$&\e[m#$'"
633
+ colsep @path, i, l
634
+ true
635
+ }
636
+ end
681
637
 
682
- BLOCK_SIZE = 512 # :nodoc:
683
-
684
- # :call-seq:
685
- # binary?( n = 1) -> true or false
686
- #
687
- # Test whether the first <code>n</code> blocks contain null characters.
688
- #
689
- def binary? n = 1
690
- open { |file|
691
- loop do
692
- if n then
693
- break if n <= 0
694
- n -= 1
638
+ # :call-seq:
639
+ # binary?( n = 1) -> true or false
640
+ #
641
+ # Test whether the first <code>n</code> blocks contain null characters.
642
+ #
643
+ def binary? n = 1
644
+ bs = stat.blksize
645
+ open { |file|
646
+ loop do
647
+ if n then
648
+ break if n <= 0
649
+ n -= 1
650
+ end
651
+ b = file.read bs
652
+ b or break
653
+ return true if b[ "\0"]
695
654
  end
696
- b = file.read BLOCK_SIZE
697
- b or break
698
- return true if b[ "\0"]
655
+ }
656
+ false
657
+ end
658
+ alias bin? binary?
659
+
660
+ def vimswap?
661
+ if name =~ /\A(\..+)?\.sw[a-z]\z/i then
662
+ mark = read 5
663
+ mark == "b0VIM"
699
664
  end
700
- }
701
- false
702
- end
703
- alias bin? binary?
665
+ end
704
666
 
705
667
 
706
- # :stopdoc:
707
- class Prune < Exception ; end
708
- # :startdoc:
709
-
710
- # :call-seq:
711
- # prune() -> (does not return)
712
- #
713
- # Abandon the current object (directory) and ignore all subdirectories.
714
- #
715
- def prune ; raise Prune ; end
716
-
717
- # :call-seq:
718
- # novcs() -> nil
719
- #
720
- # Perform <code>prune</code> if the current object is a CVS, Subversion or
721
- # Git directory.
722
- #
723
- def novcs
724
- prune if %w(CVS .svn .git .hg .fslckout).include? name
725
- end
726
- alias no_vcs novcs
727
-
728
- # :call-seq:
729
- # vimswap? -> true or false
730
- #
731
- # Check whether the current object is a Vim swapfile.
732
- #
733
- def vimswap?
734
- if name =~ /\A(\..+)?\.sw[a-z]\z/i then
735
- mark = read 5
736
- mark == "b0VIM"
668
+
669
+ def done ; raise Done ; end
670
+ alias done! done
671
+
672
+ def prune ; raise Prune ; end
673
+ alias prune! prune
674
+
675
+ def novcs
676
+ prune if vcs?
737
677
  end
738
- end
678
+ alias no_vcs novcs
739
679
 
740
- # Windows has ":" in filenames ("C:\...")
741
- COLON = File::ALT_SEPARATOR ? "|" : ":" # :nodoc:
742
680
 
743
- def colsep *args
744
- csv COLON, *args
745
- end
746
- alias col_sep colsep
681
+ include Csv
747
682
 
748
- def tabsep *args
749
- csv "\t", *args
750
- end
751
- alias tab_sep tabsep
752
683
 
753
- def spcsep *args
754
- csv " ", *args
755
- end
756
- alias spc_sep spcsep
757
- alias space_sep spc_sep
758
- alias spacesep spcsep
684
+ def rename newname ; @name = newname ; end
759
685
 
760
- def csv sep, *args
761
- e = args.join sep
762
- puts e
763
- end
764
686
 
765
- def rename newname
766
- p = @path
767
- nb = File.basename newname
768
- newname == nb or raise RuntimeError,
769
- "#{self.class}: rename to `#{newname}' may not be a path."
770
- nb.freeze
771
- @levels.pop
772
- @levels.push nb
773
- build_path
774
- File.rename p, @path
775
- nil
776
- end
777
687
 
778
- private
688
+ def cname ; color name ; end
689
+ def cpath ; color path ; end
690
+ def cfullpath ; color fullpath ; end
691
+ def cpath! ; color path! ; end
692
+ def cfullpath! ; color fullpath! ; end
779
693
 
780
- def sort_parser st
781
- case st
782
- when Proc then st
783
- when Numeric then st
784
- when true then +1
785
- when false then 0
786
- when nil then +1
787
- else
788
- case st.to_s
789
- when "^", "reverse", /^desc/, /^-/ then -1
790
- when "unsorted", "*" then 0
791
- else +1
792
- end
694
+ def creadlink
695
+ l = readlink
696
+ if l then
697
+ s = rstat rescue nil
698
+ color_stat l, s
699
+ end
793
700
  end
794
- end
795
701
 
796
- def absolute_path? p
797
- loop do
798
- q = File.dirname p
799
- break if q == p
800
- p = q
702
+ def carrow
703
+ r = creadlink
704
+ ARROW + r if r
801
705
  end
802
- p != Dir::CUR_DIR
803
- end
804
706
 
805
- def build_path
806
- @path = File.join @levels
807
- @fullpath = if @wd then
808
- File.join @wd, @path
809
- else
810
- @path
707
+
708
+ def color arg
709
+ color_stat arg, stat
811
710
  end
812
- if @path.empty? then @path = Dir::CUR_DIR end
813
- @fullpath.freeze
814
- @path.freeze
815
- end
711
+ alias colour color
816
712
 
817
- def walk
818
- return if @max_depth and depth > @max_depth
819
- build_path
820
- @count += 1
821
- @do_level.call
822
- end
713
+ private
823
714
 
824
- def do_level
825
- begin
826
- call_block
827
- rescue Prune
828
- return
715
+ def color_stat arg, s
716
+ m = s.mode if s
717
+ code = case m && m >> 12
718
+ when 001 then 5
719
+ when 002 then 8
720
+ when 004 then
721
+ if (m & 0002).nonzero? then
722
+ if (m & 01000).nonzero? then 11
723
+ else 12
724
+ end
725
+ else 2
726
+ end
727
+ when 006 then 7
728
+ when 010 then
729
+ if (m & 0111).nonzero? then
730
+ if (m & 04000).nonzero? then 9
731
+ elsif (m & 02000).nonzero? then 10
732
+ else 6
733
+ end
734
+ else col_type or 0
735
+ end
736
+ when 012 then 3
737
+ when 014 then 4
738
+ when 016 then 13
739
+ when nil then 1
740
+ else 14
741
+ end
742
+ self.class.colored arg, code
829
743
  end
830
- scan_dir
831
- end
832
744
 
833
- def do_level_depth
834
- begin
835
- path, fullpath = @path, @fullpath
836
- scan_dir
837
- ensure
838
- @path, @fullpath = path, fullpath
745
+ def col_type
746
+ # Overwrite this to define custom colors
747
+ # Example:
748
+ # case ext
749
+ # when ".png", /\.jpe?g$/, /\.tiff?$/ then 15
750
+ # when /\.tar\.(gz|bz2)$/ then 16
751
+ # end
839
752
  end
840
- begin
841
- call_block
842
- rescue Prune
843
- raise RuntimeError, "#{self.class}: prune doesn't work with :depth."
844
- end
845
- end
846
753
 
847
- def call_block
848
- set_predefs name, count
849
- @block.call self
850
- end
754
+ DEFAULT_COLORS = "xxHbexfxcxdxbxegedabagacadAx"
851
755
 
852
- def set_predefs l, n
853
- b = @block.binding
854
- b.local_variable_set "_", [ l, n]
855
- b.eval "$_, $. = *_"
856
- end
756
+ class <<self
857
757
 
858
- def read_dir
859
- handle_error Errno::EACCES do
860
- Dir.new @path
861
- end
862
- end
863
-
864
- def scan_dir
865
- return unless File.directory? @path
866
- if File.symlink? @path then
867
- return unless @follow and handle_error do
868
- d = @path
869
- while d != Dir::CUR_DIR do
870
- d, = File.split d
871
- raise "circular recursion in #@path" if File.identical? d, @path
872
- end
873
- true
758
+ def colored arg, num
759
+ colors col_str
760
+ "\e[#{@colors[num]}m#{arg}\e[m"
874
761
  end
875
- end
876
- dir = (read_dir or return).entries!
877
- if @sort.respond_to? :call then
878
- dir = dir.sort_by &@sort
879
- elsif @sort and @sort.nonzero? then
880
- dir.sort!
881
- dir.reverse! if @sort < 0
882
- end
883
- dir.each { |f|
884
- f.freeze
885
- begin
886
- @levels.push f
887
- walk
888
- ensure
889
- @levels.pop
762
+ alias coloured colored
763
+
764
+ def colors str
765
+ @colors ||= if str =~ /:/ then
766
+ h = {}
767
+ (str.split ":").each { |a|
768
+ t, c = a.split "="
769
+ h[ t] = c
770
+ }
771
+ %w(rs or di ln so pi ex bd cd su sg tw ow - -).map { |t| h[ t] }
772
+ else
773
+ cols = []
774
+ str.scan /(.)(.)/i do
775
+ fg, bg = $~.captures.map { |x| x.downcase.ord - ?a.ord }
776
+ a = []
777
+ case fg
778
+ when 0..7 then a.push 30 + fg
779
+ end
780
+ a.push 1 if $1 == $1.upcase
781
+ case bg
782
+ when 0..7 then a.push 40 + bg
783
+ end
784
+ e = a.join ";"
785
+ cols.push e
786
+ end
787
+ cols
788
+ end
890
789
  end
891
- }
892
- end
790
+ alias colours colors
893
791
 
894
- def handle_error err = nil
895
- yield
896
- rescue err||StandardError
897
- if @error.respond_to? :call then
898
- @error.call
899
- elsif @error then
900
- instance_eval @error
901
- else
902
- raise
903
- end
904
- nil
905
- end
792
+ private
906
793
 
907
- def col_stat arg, s
908
- m = s.mode if s
909
- code = case m && m >> 12
910
- when 001 then 5
911
- when 002 then 8
912
- when 004 then
913
- if (m & 0002).nonzero? then
914
- if (m & 01000).nonzero? then 11
915
- else 12
794
+ def col_str
795
+ ENV[ "RBFIND_COLORS"] || ENV[ "RBFIND_COLOURS"] || ENV[ "LS_COLORS"] || (
796
+ env = DEFAULT_COLORS.dup
797
+ els = ENV[ "LSCOLORS"]
798
+ if els then
799
+ env[ 2*2, els.length] = els
916
800
  end
917
- else 2
918
- end
919
- when 006 then 7
920
- when 010 then
921
- col_type or \
922
- if (m & 0111).nonzero? then
923
- if (m & 04000).nonzero? then 9
924
- elsif (m & 02000).nonzero? then 10
925
- else 6
926
- end
927
- else 0
928
- end
929
- when 012 then 3
930
- when 014 then 4
931
- when 016 then 13
932
- when nil then 1
933
- else 14
934
- end
935
- self.class.colored arg, code
936
- end
937
-
938
- def col_type
939
- # Overwrite this to define custom colors
940
- # Example:
941
- # case ext
942
- # when ".png", /\.jpe?g$/, /\.tiff?$/ then 15
943
- # when /\.tar\.(gz|bz2)$/ then 16
944
- # end
945
- end
801
+ env
802
+ )
803
+ end
946
804
 
947
- class <<self
948
- def find *args
949
- raise NotImplementedError, "This is not the standard Find."
950
805
  end
806
+
951
807
  end
952
808
 
953
809
  end