rbfind 1.11 → 2.1

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