rbfind 1.8 → 2.0.2

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.2".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,728 +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.8".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
- @max_depth += 1 if @max_depth
327
- File.lstat path
328
- @wd = nil unless absolute_path? path
329
- @levels.push path
330
- walk
331
- else
332
- build_path
333
- 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
334
267
  end
335
- end
336
268
 
337
- private
269
+ private
338
270
 
339
- 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
340
279
 
341
- public
280
+ ostat = $stdout.stat
281
+ @ostat = ostat if ostat.file?
342
282
 
343
- def name ; @levels.last ; end
344
- def path ; @path ; end
345
- def fullpath ; @fullpath ; end
346
- def path! ; append_slash @path ; end
347
- def fullpath! ; append_slash @fullpath ; end
283
+ @wd, @start, @count = Dir.getwd, Time.now, 0
284
+ end
348
285
 
349
- def dirname
350
- d = File.dirname @fullpath
351
- File.basename d
352
- 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
353
294
 
354
- def ext ; File.extname name ; end
355
- def without_ext ; name[ /^(.+?)(?:\.[^.]+)?$/, 1 ].to_s ; end
295
+ public
356
296
 
357
- def depth ; @levels.size ; end
297
+ attr_reader :wd, :start, :count, :depth
358
298
 
359
- def hidden? ; name =~ /^\./ ; end
360
- 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
361
316
 
362
- def stat ; File.lstat @path ; end
363
- def mode ; stat.mode ; end
317
+ private
364
318
 
365
- def readlink
366
- File.readlink @path if stat.symlink?
367
- end
319
+ def join_path
320
+ (File.join @levels).freeze
321
+ end
368
322
 
369
- def creadlink
370
- if stat.symlink? then
371
- s = File.stat @path rescue nil
372
- l = File.readlink @path
373
- (col_stat l, s)
323
+ def visit filename
324
+ @depth += 1
325
+ visit_depth filename
326
+ ensure
327
+ @depth -= 1
374
328
  end
375
- end
376
329
 
377
- # :call-seq:
378
- # broken_link?() -> true or false
379
- #
380
- def broken_link?
381
- return unless stat.symlink?
382
- !File.stat @path rescue true
383
- 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
384
361
 
385
- 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
386
369
 
387
- def arrow
388
- ARROW + (File.readlink @path) if stat.symlink?
389
- 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
390
393
 
391
- def carrow
392
- r = creadlink
393
- ARROW + r if r
394
- 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
395
404
 
396
- def now ; @start ; end
397
- def age ; @start - stat.mtime ; end
398
- alias age_secs age
399
- alias age_s age_secs
400
-
401
- # :stopdoc:
402
- MINUTE = 60
403
- HOUR = 60*MINUTE
404
- DAY = 24*HOUR
405
- # :startdoc:
406
-
407
- def age_mins ; age / MINUTE ; end
408
- alias age_m age_mins
409
- def age_hours ; age / HOUR ; end
410
- alias age_h age_hours
411
- def age_days ; age / DAY ; end
412
- alias age_d age_days
413
-
414
- private
415
-
416
- def method_missing sym, *args, &block
417
- stat.send sym, *args, &block
418
- rescue NoMethodError
419
- super
420
405
  end
421
406
 
422
- public
423
-
424
- def dir? ; stat.directory? ; end
425
-
426
- # :call-seq:
427
- # stype() -> str
428
- #
429
- def stype
430
- m = stat.mode >> 12 rescue nil
431
- case m
432
- when 001 then "p"
433
- when 002 then "c"
434
- when 004 then "d"
435
- when 006 then "b"
436
- when 010 then "-"
437
- when 012 then "l"
438
- when 014 then "s"
439
- when 016 then "w"
440
- when nil then "#"
441
- 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
442
414
  end
443
- end
444
415
 
445
- # :call-seq:
446
- # modes() -> str
447
- #
448
- def modes
449
- m = stat.mode
450
- r = ""
451
- 3.times {
452
- h = m & 07
453
- m >>= 3
454
- r.insert 0, ((h & 01).nonzero? ? "x" : "-")
455
- r.insert 0, ((h & 02).nonzero? ? "w" : "-")
456
- r.insert 0, ((h & 04).nonzero? ? "r" : "-")
457
- }
458
- if (m & 04).nonzero? then
459
- r[ 2] = r[ 2, 1] == "x" ? "s" : "S"
460
- end
461
- if (m & 02).nonzero? then
462
- r[ 5] = r[ 5, 1] == "x" ? "s" : "S"
463
- end
464
- if (m & 01).nonzero? then
465
- r[ 8] = r[ 8, 1] == "x" ? "t" : "T"
466
- end
467
- r
468
- 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
+
469
424
 
470
- # :call-seq:
471
- # filesize => nil or int
472
- # filesize { |size| ... } => obj
473
- #
474
- # Returns the files size. When the object is not a regular file,
475
- # nil will be returned or the block will not be called.
476
- #
477
- def filesize
478
- if block_given? then
479
- yield stat.size if file?
480
- else
481
- 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
482
434
  end
483
- end
484
435
 
485
436
 
486
- def cname ; color name ; end
487
- def cpath ; color path ; end
488
- def cfullpath ; color fullpath ; end
489
- def cpath! ; color path! ; end
490
- def cfullpath! ; color fullpath! ; end
437
+ def ext ; File.extname name ; end
438
+ def without_ext ; name[ /^(.+?)(?:\.[^.]+)?$/, 1 ].to_s ; end
491
439
 
492
- def color arg
493
- col_stat arg, stat
494
- end
495
- alias colour color
440
+ def hidden? ; name =~ /^\./ ; end
441
+ def visible? ; not hidden? ; end
496
442
 
497
- DEFAULT_COLORS = "xxHbexfxcxdxbxegedabagacadAx"
498
443
 
499
- class <<self
444
+ def mode ; stat.mode ; end
500
445
 
501
- def colors str
502
- @cols = []
503
- str.scan /(.)(.)/i do
504
- fg, bg = $~.captures.map { |x| x.downcase.ord - ?a.ord }
505
- a = []
506
- case fg
507
- when 0..7 then a.push 30 + fg
508
- end
509
- a.push 1 if $1 == $1.upcase
510
- case bg
511
- when 0..7 then a.push 40 + bg
512
- end
513
- e = a.join ";"
514
- @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
515
452
  end
516
453
  end
517
- alias colours colors
454
+ public
455
+
456
+ def dir? ; stat.directory? ; end
518
457
 
519
- def colored arg, num
520
- @cols or colors col_str
521
- "\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
522
477
  end
523
- alias coloured colored
524
478
 
525
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
526
489
 
527
- def col_str
528
- ENV[ "RBFIND_COLORS"] || ENV[ "RBFIND_COLOURS"] || (
529
- env = DEFAULT_COLORS.dup
530
- els = ENV[ "LSCOLORS"]
531
- if els then
532
- env[ 2*2, els.length] = els
533
- end
534
- env
535
- )
490
+ def user
491
+ get_user stat.uid
536
492
  end
493
+ alias owner user
537
494
 
538
- end
495
+ def user!
496
+ u = stat.uid
497
+ u == Process.uid ? "." : (get_user u)
498
+ end
499
+ alias owner! user!
539
500
 
501
+ def group
502
+ get_group stat.gid
503
+ end
540
504
 
541
- # :call-seq:
542
- # suffix() -> str
543
- #
544
- def suffix
545
- m = stat.mode >> 12 rescue nil
546
- case m
547
- when 001 then "|"
548
- when 002 then " "
549
- when 004 then "/"
550
- when 006 then " "
551
- when 010 then stat.executable? ? "*" : " "
552
- when 012 then "@"
553
- when 014 then "="
554
- when 016 then "%"
555
- else "?"
505
+ def group!
506
+ g = stat.gid
507
+ g == Process.gid ? "." : (get_group g)
556
508
  end
557
- end
558
509
 
559
- autoload :Etc, "etc"
560
510
 
561
- # :call-seq:
562
- # user() -> str
563
- #
564
- # Return user name or uid as string if unavailable.
565
- #
566
- def user
567
- u = stat.uid
568
- (Etc.getpwuid u).name rescue u.to_s
569
- end
570
- alias owner user
571
-
572
- # :call-seq:
573
- # group() -> str
574
- #
575
- # Return group name or gid as string if unavailable.
576
- #
577
- def group
578
- g = stat.gid
579
- (Etc.getgrgid g).name rescue g.to_s
580
- end
511
+ def readlink ; File.readlink @path if stat.symlink? ; end
581
512
 
513
+ def broken_link?
514
+ return if stat.symlink?
515
+ rstat
516
+ false
517
+ rescue
518
+ true
519
+ end
582
520
 
583
- # :call-seq:
584
- # empty?() -> true or false
585
- #
586
- # Look up if the directory is empty. If the object is not a directory,
587
- # +nil+ is returned.
588
- #
589
- def empty?
590
- read_dir.each! { |f| return false }
591
- true
592
- rescue Errno::ENOTDIR
593
- end
521
+ ARROW = " -> "
522
+ def arrow
523
+ ARROW + (File.readlink @path) if stat.symlink?
524
+ end
594
525
 
595
- # :call-seq:
596
- # contains?( name) -> true or false
597
- #
598
- # Check whether a directory contains an entry.
599
- #
600
- def contains? name
601
- c = File.join @path, name
602
- File.exists? c
603
- end
604
526
 
605
- # :call-seq:
606
- # entires() -> ary
607
- #
608
- # Return all entries in an array. If the object is not a directory,
609
- # +nil+ is returned.
610
- #
611
- def entries
612
- read_dir.entries!
613
- rescue Errno::ENOTDIR
614
- end
615
527
 
616
- # :call-seq:
617
- # open() { |h| ... } -> obj
618
- #
619
- # Open the file for reading. If the object is not a regular file,
620
- # nothing will be done.
621
- #
622
- def open &block
623
- 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."
624
576
  File.open @path, &block if file?
625
577
  end
626
- end
627
578
 
628
- # :call-seq:
629
- # read( n = nil) -> str or nil
630
- # read( n = nil) { |b| ... } -> nil
631
- #
632
- # Read the first +n+ bytes or return +nil+ for others than regular
633
- # files. +nil+ reads to end of file. If a block is given, chonks of
634
- # +n+ bytes (or all) will be yielded.
635
- #
636
- def read n = nil
637
- open { |o|
638
- if block_given? then
639
- while (r = o.read n) do
640
- 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
641
599
  end
642
- else
643
- o.read n
644
- end
645
- }
646
- end
600
+ }
601
+ end
647
602
 
648
- # :call-seq:
649
- # lines { |l,i| ... } -> nil
650
- #
651
- # Yield line by line together with the line number <code>i</code>.
652
- #
653
- def lines
654
- block_given? or return lines do end
655
- open { |file|
656
- n = 0
657
- file.each_line { |l|
658
- l.chomp!
659
- n += 1
660
- set_predefs l, n
661
- 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
662
620
  }
663
- n
664
- }
665
- end
621
+ end
666
622
 
667
- def grep re, color = nil
668
- case color
669
- when /\A\d+(?:;\d+)*\z/, nil, false then
670
- when true then color = "31;1" # red
671
- else raise "Illegal color spec: #{color}"
672
- end
673
- lines { |l,i|
674
- l.scrub!
675
- l =~ re or next
676
- if color then
677
- 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}"
678
628
  end
679
- colsep @path, i, l
680
- }
681
- 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
682
637
 
683
- BLOCK_SIZE = 512 # :nodoc:
684
-
685
- # :call-seq:
686
- # binary?( n = 1) -> true or false
687
- #
688
- # Test whether the first <code>n</code> blocks contain null characters.
689
- #
690
- def binary? n = 1
691
- open { |file|
692
- loop do
693
- if n then
694
- break if n <= 0
695
- 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"]
696
654
  end
697
- b = file.read BLOCK_SIZE
698
- b or break
699
- 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"
700
664
  end
701
- }
702
- false
703
- end
704
- alias bin? binary?
665
+ end
705
666
 
706
667
 
707
- # :stopdoc:
708
- class Prune < Exception ; end
709
- # :startdoc:
710
-
711
- # :call-seq:
712
- # prune() -> (does not return)
713
- #
714
- # Abandon the current object (directory) and ignore all subdirectories.
715
- #
716
- def prune ; raise Prune ; end
717
-
718
- # :call-seq:
719
- # novcs() -> nil
720
- #
721
- # Perform <code>prune</code> if the current object is a CVS, Subversion or
722
- # Git directory.
723
- #
724
- def novcs
725
- prune if %w(CVS .svn .git .hg .fslckout).include? name
726
- end
727
- alias no_vcs novcs
728
-
729
- # :call-seq:
730
- # vimswap? -> true or false
731
- #
732
- # Check whether the current object is a Vim swapfile.
733
- #
734
- def vimswap?
735
- if name =~ /\A(\..+)?\.sw[a-z]\z/i then
736
- mark = read 5
737
- 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?
738
677
  end
739
- end
678
+ alias no_vcs novcs
740
679
 
741
- # Windows has ":" in filenames ("C:\...")
742
- COLON = File::ALT_SEPARATOR ? "|" : ":" # :nodoc:
743
680
 
744
- def colsep *args
745
- csv COLON, *args
746
- end
747
- alias col_sep colsep
681
+ include Csv
748
682
 
749
- def tabsep *args
750
- csv "\t", *args
751
- end
752
- alias tab_sep tabsep
753
683
 
754
- def spcsep *args
755
- csv " ", *args
756
- end
757
- alias spc_sep spcsep
758
- alias space_sep spc_sep
759
- alias spacesep spcsep
684
+ def rename newname ; @name = newname ; end
760
685
 
761
- def csv sep, *args
762
- e = args.join sep
763
- puts e
764
- end
765
686
 
766
- def rename newname
767
- p = @path
768
- nb = File.basename newname
769
- newname == nb or raise RuntimeError,
770
- "#{self.class}: rename to `#{newname}' may not be a path."
771
- nb.freeze
772
- @levels.pop
773
- @levels.push nb
774
- build_path
775
- File.rename p, @path
776
- nil
777
- end
778
687
 
779
- 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
780
693
 
781
- def sort_parser st
782
- case st
783
- when Proc then st
784
- when Numeric then st
785
- when true then +1
786
- when false then 0
787
- when nil then +1
788
- else
789
- case st.to_s
790
- when "^", "reverse", /^desc/, /^-/ then -1
791
- when "unsorted", "*" then 0
792
- else +1
793
- end
694
+ def creadlink
695
+ l = readlink
696
+ if l then
697
+ s = rstat rescue nil
698
+ color_stat l, s
699
+ end
794
700
  end
795
- end
796
701
 
797
- def absolute_path? p
798
- loop do
799
- q = File.dirname p
800
- break if q == p
801
- p = q
702
+ def carrow
703
+ r = creadlink
704
+ ARROW + r if r
802
705
  end
803
- p != Dir::CUR_DIR
804
- end
805
706
 
806
- def build_path
807
- @path = File.join @levels
808
- @fullpath = if @wd then
809
- File.join @wd, @path
810
- else
811
- @path
707
+
708
+ def color arg
709
+ color_stat arg, stat
812
710
  end
813
- if @path.empty? then @path = Dir::CUR_DIR end
814
- @fullpath.freeze
815
- @path.freeze
816
- end
711
+ alias colour color
817
712
 
818
- def walk
819
- return if @max_depth and depth > @max_depth
820
- build_path
821
- @count += 1
822
- @do_level.call
823
- end
713
+ private
824
714
 
825
- def do_level
826
- begin
827
- call_block
828
- rescue Prune
829
- 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
830
743
  end
831
- scan_dir
832
- end
833
744
 
834
- def do_level_depth
835
- begin
836
- path, fullpath = @path, @fullpath
837
- scan_dir
838
- ensure
839
- @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
840
752
  end
841
- begin
842
- call_block
843
- rescue Prune
844
- raise RuntimeError, "#{self.class}: prune doesn't work with :depth."
845
- end
846
- end
847
753
 
848
- def call_block
849
- set_predefs name, count
850
- @block.call self
851
- end
754
+ DEFAULT_COLORS = "xxHbexfxcxdxbxegedabagacadAx"
852
755
 
853
- def set_predefs l, n
854
- b = @block.binding
855
- b.local_variable_set "_", [ l, n]
856
- b.eval "$_, $. = *_"
857
- end
756
+ class <<self
858
757
 
859
- def read_dir
860
- handle_error Errno::EACCES do
861
- Dir.new @path
862
- end
863
- end
864
-
865
- def scan_dir
866
- return unless File.directory? @path
867
- if File.symlink? @path then
868
- return unless @follow and handle_error do
869
- d = @path
870
- while d != Dir::CUR_DIR do
871
- d, = File.split d
872
- raise "circular recursion in #@path" if File.identical? d, @path
873
- end
874
- true
758
+ def colored arg, num
759
+ colors col_str
760
+ "\e[#{@colors[num]}m#{arg}\e[m"
875
761
  end
876
- end
877
- dir = (read_dir or return).entries!
878
- if @sort.respond_to? :call then
879
- dir = dir.sort_by &@sort
880
- elsif @sort and @sort.nonzero? then
881
- dir.sort!
882
- dir.reverse! if @sort < 0
883
- end
884
- dir.each { |f|
885
- f.freeze
886
- begin
887
- @levels.push f
888
- walk
889
- ensure
890
- @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
891
789
  end
892
- }
893
- end
790
+ alias colours colors
894
791
 
895
- def handle_error err = nil
896
- yield
897
- rescue err||StandardError
898
- if @error.respond_to? :call then
899
- @error.call
900
- elsif @error then
901
- instance_eval @error
902
- else
903
- raise
904
- end
905
- nil
906
- end
792
+ private
907
793
 
908
- def col_stat arg, s
909
- m = s.mode if s
910
- code = case m && m >> 12
911
- when 001 then 5
912
- when 002 then 8
913
- when 004 then
914
- if (m & 0002).nonzero? then
915
- if (m & 01000).nonzero? then 11
916
- 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
917
800
  end
918
- else 2
919
- end
920
- when 006 then 7
921
- when 010 then
922
- col_type or \
923
- if (m & 0111).nonzero? then
924
- if (m & 04000).nonzero? then 9
925
- elsif (m & 02000).nonzero? then 10
926
- else 6
927
- end
928
- else 0
929
- end
930
- when 012 then 3
931
- when 014 then 4
932
- when 016 then 13
933
- when nil then 1
934
- else 14
935
- end
936
- self.class.colored arg, code
937
- end
938
-
939
- def col_type
940
- # Overwrite this to define custom colors
941
- # Example:
942
- # case ext
943
- # when ".png", /\.jpe?g$/, /\.tiff?$/ then 15
944
- # when /\.tar\.(gz|bz2)$/ then 16
945
- # end
946
- end
801
+ env
802
+ )
803
+ end
947
804
 
948
- class <<self
949
- def find *args
950
- raise NotImplementedError, "This is not the standard Find."
951
805
  end
806
+
952
807
  end
953
808
 
954
809
  end