1# 2# = pathname.rb 3# 4# Object-Oriented Pathname Class 5# 6# Author:: Tanaka Akira <akr@m17n.org> 7# Documentation:: Author and Gavin Sinclair 8# 9# For documentation, see class Pathname. 10# 11 12require 'pathname.so' 13 14class Pathname 15 16 # :stopdoc: 17 if RUBY_VERSION < "1.9" 18 TO_PATH = :to_str 19 else 20 # to_path is implemented so Pathname objects are usable with File.open, etc. 21 TO_PATH = :to_path 22 end 23 24 SAME_PATHS = if File::FNM_SYSCASE.nonzero? 25 proc {|a, b| a.casecmp(b).zero?} 26 else 27 proc {|a, b| a == b} 28 end 29 30 31 if File::ALT_SEPARATOR 32 SEPARATOR_LIST = "#{Regexp.quote File::ALT_SEPARATOR}#{Regexp.quote File::SEPARATOR}" 33 SEPARATOR_PAT = /[#{SEPARATOR_LIST}]/ 34 else 35 SEPARATOR_LIST = "#{Regexp.quote File::SEPARATOR}" 36 SEPARATOR_PAT = /#{Regexp.quote File::SEPARATOR}/ 37 end 38 39 # :startdoc: 40 41 # chop_basename(path) -> [pre-basename, basename] or nil 42 def chop_basename(path) # :nodoc: 43 base = File.basename(path) 44 if /\A#{SEPARATOR_PAT}?\z/o =~ base 45 return nil 46 else 47 return path[0, path.rindex(base)], base 48 end 49 end 50 private :chop_basename 51 52 # split_names(path) -> prefix, [name, ...] 53 def split_names(path) # :nodoc: 54 names = [] 55 while r = chop_basename(path) 56 path, basename = r 57 names.unshift basename 58 end 59 return path, names 60 end 61 private :split_names 62 63 def prepend_prefix(prefix, relpath) # :nodoc: 64 if relpath.empty? 65 File.dirname(prefix) 66 elsif /#{SEPARATOR_PAT}/o =~ prefix 67 prefix = File.dirname(prefix) 68 prefix = File.join(prefix, "") if File.basename(prefix + 'a') != 'a' 69 prefix + relpath 70 else 71 prefix + relpath 72 end 73 end 74 private :prepend_prefix 75 76 # Returns clean pathname of +self+ with consecutive slashes and useless dots 77 # removed. The filesystem is not accessed. 78 # 79 # If +consider_symlink+ is +true+, then a more conservative algorithm is used 80 # to avoid breaking symbolic linkages. This may retain more +..+ 81 # entries than absolutely necessary, but without accessing the filesystem, 82 # this can't be avoided. 83 # 84 # See Pathname#realpath. 85 # 86 def cleanpath(consider_symlink=false) 87 if consider_symlink 88 cleanpath_conservative 89 else 90 cleanpath_aggressive 91 end 92 end 93 94 # 95 # Clean the path simply by resolving and removing excess +.+ and +..+ entries. 96 # Nothing more, nothing less. 97 # 98 def cleanpath_aggressive # :nodoc: 99 path = @path 100 names = [] 101 pre = path 102 while r = chop_basename(pre) 103 pre, base = r 104 case base 105 when '.' 106 when '..' 107 names.unshift base 108 else 109 if names[0] == '..' 110 names.shift 111 else 112 names.unshift base 113 end 114 end 115 end 116 if /#{SEPARATOR_PAT}/o =~ File.basename(pre) 117 names.shift while names[0] == '..' 118 end 119 self.class.new(prepend_prefix(pre, File.join(*names))) 120 end 121 private :cleanpath_aggressive 122 123 # has_trailing_separator?(path) -> bool 124 def has_trailing_separator?(path) # :nodoc: 125 if r = chop_basename(path) 126 pre, basename = r 127 pre.length + basename.length < path.length 128 else 129 false 130 end 131 end 132 private :has_trailing_separator? 133 134 # add_trailing_separator(path) -> path 135 def add_trailing_separator(path) # :nodoc: 136 if File.basename(path + 'a') == 'a' 137 path 138 else 139 File.join(path, "") # xxx: Is File.join is appropriate to add separator? 140 end 141 end 142 private :add_trailing_separator 143 144 def del_trailing_separator(path) # :nodoc: 145 if r = chop_basename(path) 146 pre, basename = r 147 pre + basename 148 elsif /#{SEPARATOR_PAT}+\z/o =~ path 149 $` + File.dirname(path)[/#{SEPARATOR_PAT}*\z/o] 150 else 151 path 152 end 153 end 154 private :del_trailing_separator 155 156 def cleanpath_conservative # :nodoc: 157 path = @path 158 names = [] 159 pre = path 160 while r = chop_basename(pre) 161 pre, base = r 162 names.unshift base if base != '.' 163 end 164 if /#{SEPARATOR_PAT}/o =~ File.basename(pre) 165 names.shift while names[0] == '..' 166 end 167 if names.empty? 168 self.class.new(File.dirname(pre)) 169 else 170 if names.last != '..' && File.basename(path) == '.' 171 names << '.' 172 end 173 result = prepend_prefix(pre, File.join(*names)) 174 if /\A(?:\.|\.\.)\z/ !~ names.last && has_trailing_separator?(path) 175 self.class.new(add_trailing_separator(result)) 176 else 177 self.class.new(result) 178 end 179 end 180 end 181 private :cleanpath_conservative 182 183 # Returns the parent directory. 184 # 185 # This is same as <code>self + '..'</code>. 186 def parent 187 self + '..' 188 end 189 190 # Returns +true+ if +self+ points to a mountpoint. 191 def mountpoint? 192 begin 193 stat1 = self.lstat 194 stat2 = self.parent.lstat 195 stat1.dev == stat2.dev && stat1.ino == stat2.ino || 196 stat1.dev != stat2.dev 197 rescue Errno::ENOENT 198 false 199 end 200 end 201 202 # 203 # Predicate method for root directories. Returns +true+ if the 204 # pathname consists of consecutive slashes. 205 # 206 # It doesn't access the filesystem. So it may return +false+ for some 207 # pathnames which points to roots such as <tt>/usr/..</tt>. 208 # 209 def root? 210 !!(chop_basename(@path) == nil && /#{SEPARATOR_PAT}/o =~ @path) 211 end 212 213 # Predicate method for testing whether a path is absolute. 214 # 215 # It returns +true+ if the pathname begins with a slash. 216 # 217 # p = Pathname.new('/im/sure') 218 # p.absolute? 219 # #=> true 220 # 221 # p = Pathname.new('not/so/sure') 222 # p.absolute? 223 # #=> false 224 def absolute? 225 !relative? 226 end 227 228 # The opposite of Pathname#absolute? 229 # 230 # It returns +false+ if the pathname begins with a slash. 231 # 232 # p = Pathname.new('/im/sure') 233 # p.relative? 234 # #=> false 235 # 236 # p = Pathname.new('not/so/sure') 237 # p.relative? 238 # #=> true 239 def relative? 240 path = @path 241 while r = chop_basename(path) 242 path, = r 243 end 244 path == '' 245 end 246 247 # 248 # Iterates over each component of the path. 249 # 250 # Pathname.new("/usr/bin/ruby").each_filename {|filename| ... } 251 # # yields "usr", "bin", and "ruby". 252 # 253 # Returns an Enumerator if no block was given. 254 # 255 # enum = Pathname.new("/usr/bin/ruby").each_filename 256 # # ... do stuff ... 257 # enum.each { |e| ... } 258 # # yields "usr", "bin", and "ruby". 259 # 260 def each_filename # :yield: filename 261 return to_enum(__method__) unless block_given? 262 _, names = split_names(@path) 263 names.each {|filename| yield filename } 264 nil 265 end 266 267 # Iterates over and yields a new Pathname object 268 # for each element in the given path in descending order. 269 # 270 # Pathname.new('/path/to/some/file.rb').descend {|v| p v} 271 # #<Pathname:/> 272 # #<Pathname:/path> 273 # #<Pathname:/path/to> 274 # #<Pathname:/path/to/some> 275 # #<Pathname:/path/to/some/file.rb> 276 # 277 # Pathname.new('path/to/some/file.rb').descend {|v| p v} 278 # #<Pathname:path> 279 # #<Pathname:path/to> 280 # #<Pathname:path/to/some> 281 # #<Pathname:path/to/some/file.rb> 282 # 283 # It doesn't access the filesystem. 284 # 285 def descend 286 vs = [] 287 ascend {|v| vs << v } 288 vs.reverse_each {|v| yield v } 289 nil 290 end 291 292 # Iterates over and yields a new Pathname object 293 # for each element in the given path in ascending order. 294 # 295 # Pathname.new('/path/to/some/file.rb').ascend {|v| p v} 296 # #<Pathname:/path/to/some/file.rb> 297 # #<Pathname:/path/to/some> 298 # #<Pathname:/path/to> 299 # #<Pathname:/path> 300 # #<Pathname:/> 301 # 302 # Pathname.new('path/to/some/file.rb').ascend {|v| p v} 303 # #<Pathname:path/to/some/file.rb> 304 # #<Pathname:path/to/some> 305 # #<Pathname:path/to> 306 # #<Pathname:path> 307 # 308 # It doesn't access the filesystem. 309 # 310 def ascend 311 path = @path 312 yield self 313 while r = chop_basename(path) 314 path, = r 315 break if path.empty? 316 yield self.class.new(del_trailing_separator(path)) 317 end 318 end 319 320 # 321 # Appends a pathname fragment to +self+ to produce a new Pathname object. 322 # 323 # p1 = Pathname.new("/usr") # Pathname:/usr 324 # p2 = p1 + "bin/ruby" # Pathname:/usr/bin/ruby 325 # p3 = p1 + "/etc/passwd" # Pathname:/etc/passwd 326 # 327 # This method doesn't access the file system; it is pure string manipulation. 328 # 329 def +(other) 330 other = Pathname.new(other) unless Pathname === other 331 Pathname.new(plus(@path, other.to_s)) 332 end 333 334 def plus(path1, path2) # -> path # :nodoc: 335 prefix2 = path2 336 index_list2 = [] 337 basename_list2 = [] 338 while r2 = chop_basename(prefix2) 339 prefix2, basename2 = r2 340 index_list2.unshift prefix2.length 341 basename_list2.unshift basename2 342 end 343 return path2 if prefix2 != '' 344 prefix1 = path1 345 while true 346 while !basename_list2.empty? && basename_list2.first == '.' 347 index_list2.shift 348 basename_list2.shift 349 end 350 break unless r1 = chop_basename(prefix1) 351 prefix1, basename1 = r1 352 next if basename1 == '.' 353 if basename1 == '..' || basename_list2.empty? || basename_list2.first != '..' 354 prefix1 = prefix1 + basename1 355 break 356 end 357 index_list2.shift 358 basename_list2.shift 359 end 360 r1 = chop_basename(prefix1) 361 if !r1 && /#{SEPARATOR_PAT}/o =~ File.basename(prefix1) 362 while !basename_list2.empty? && basename_list2.first == '..' 363 index_list2.shift 364 basename_list2.shift 365 end 366 end 367 if !basename_list2.empty? 368 suffix2 = path2[index_list2.first..-1] 369 r1 ? File.join(prefix1, suffix2) : prefix1 + suffix2 370 else 371 r1 ? prefix1 : File.dirname(prefix1) 372 end 373 end 374 private :plus 375 376 # 377 # Joins the given pathnames onto +self+ to create a new Pathname object. 378 # 379 # path0 = Pathname.new("/usr") # Pathname:/usr 380 # path0 = path0.join("bin/ruby") # Pathname:/usr/bin/ruby 381 # # is the same as 382 # path1 = Pathname.new("/usr") + "bin/ruby" # Pathname:/usr/bin/ruby 383 # path0 == path1 384 # #=> true 385 # 386 def join(*args) 387 args.unshift self 388 result = args.pop 389 result = Pathname.new(result) unless Pathname === result 390 return result if result.absolute? 391 args.reverse_each {|arg| 392 arg = Pathname.new(arg) unless Pathname === arg 393 result = arg + result 394 return result if result.absolute? 395 } 396 result 397 end 398 399 # 400 # Returns the children of the directory (files and subdirectories, not 401 # recursive) as an array of Pathname objects. 402 # 403 # By default, the returned pathnames will have enough information to access 404 # the files. If you set +with_directory+ to +false+, then the returned 405 # pathnames will contain the filename only. 406 # 407 # For example: 408 # pn = Pathname("/usr/lib/ruby/1.8") 409 # pn.children 410 # # -> [ Pathname:/usr/lib/ruby/1.8/English.rb, 411 # Pathname:/usr/lib/ruby/1.8/Env.rb, 412 # Pathname:/usr/lib/ruby/1.8/abbrev.rb, ... ] 413 # pn.children(false) 414 # # -> [ Pathname:English.rb, Pathname:Env.rb, Pathname:abbrev.rb, ... ] 415 # 416 # Note that the results never contain the entries +.+ and +..+ in 417 # the directory because they are not children. 418 # 419 def children(with_directory=true) 420 with_directory = false if @path == '.' 421 result = [] 422 Dir.foreach(@path) {|e| 423 next if e == '.' || e == '..' 424 if with_directory 425 result << self.class.new(File.join(@path, e)) 426 else 427 result << self.class.new(e) 428 end 429 } 430 result 431 end 432 433 # Iterates over the children of the directory 434 # (files and subdirectories, not recursive). 435 # 436 # It yields Pathname object for each child. 437 # 438 # By default, the yielded pathnames will have enough information to access 439 # the files. 440 # 441 # If you set +with_directory+ to +false+, then the returned pathnames will 442 # contain the filename only. 443 # 444 # Pathname("/usr/local").each_child {|f| p f } 445 # #=> #<Pathname:/usr/local/share> 446 # # #<Pathname:/usr/local/bin> 447 # # #<Pathname:/usr/local/games> 448 # # #<Pathname:/usr/local/lib> 449 # # #<Pathname:/usr/local/include> 450 # # #<Pathname:/usr/local/sbin> 451 # # #<Pathname:/usr/local/src> 452 # # #<Pathname:/usr/local/man> 453 # 454 # Pathname("/usr/local").each_child(false) {|f| p f } 455 # #=> #<Pathname:share> 456 # # #<Pathname:bin> 457 # # #<Pathname:games> 458 # # #<Pathname:lib> 459 # # #<Pathname:include> 460 # # #<Pathname:sbin> 461 # # #<Pathname:src> 462 # # #<Pathname:man> 463 # 464 # Note that the results never contain the entries +.+ and +..+ in 465 # the directory because they are not children. 466 # 467 # See Pathname#children 468 # 469 def each_child(with_directory=true, &b) 470 children(with_directory).each(&b) 471 end 472 473 # 474 # Returns a relative path from the given +base_directory+ to the receiver. 475 # 476 # If +self+ is absolute, then +base_directory+ must be absolute too. 477 # 478 # If +self+ is relative, then +base_directory+ must be relative too. 479 # 480 # This method doesn't access the filesystem. It assumes no symlinks. 481 # 482 # ArgumentError is raised when it cannot find a relative path. 483 # 484 def relative_path_from(base_directory) 485 dest_directory = self.cleanpath.to_s 486 base_directory = base_directory.cleanpath.to_s 487 dest_prefix = dest_directory 488 dest_names = [] 489 while r = chop_basename(dest_prefix) 490 dest_prefix, basename = r 491 dest_names.unshift basename if basename != '.' 492 end 493 base_prefix = base_directory 494 base_names = [] 495 while r = chop_basename(base_prefix) 496 base_prefix, basename = r 497 base_names.unshift basename if basename != '.' 498 end 499 unless SAME_PATHS[dest_prefix, base_prefix] 500 raise ArgumentError, "different prefix: #{dest_prefix.inspect} and #{base_directory.inspect}" 501 end 502 while !dest_names.empty? && 503 !base_names.empty? && 504 SAME_PATHS[dest_names.first, base_names.first] 505 dest_names.shift 506 base_names.shift 507 end 508 if base_names.include? '..' 509 raise ArgumentError, "base_directory has ..: #{base_directory.inspect}" 510 end 511 base_names.fill('..') 512 relpath_names = base_names + dest_names 513 if relpath_names.empty? 514 Pathname.new('.') 515 else 516 Pathname.new(File.join(*relpath_names)) 517 end 518 end 519end 520 521 522class Pathname # * Find * 523 # 524 # Iterates over the directory tree in a depth first manner, yielding a 525 # Pathname for each file under "this" directory. 526 # 527 # Returns an Enumerator if no block is given. 528 # 529 # Since it is implemented by the standard library module Find, Find.prune can 530 # be used to control the traversal. 531 # 532 # If +self+ is +.+, yielded pathnames begin with a filename in the 533 # current directory, not +./+. 534 # 535 # See Find.find 536 # 537 def find # :yield: pathname 538 return to_enum(__method__) unless block_given? 539 require 'find' 540 if @path == '.' 541 Find.find(@path) {|f| yield self.class.new(f.sub(%r{\A\./}, '')) } 542 else 543 Find.find(@path) {|f| yield self.class.new(f) } 544 end 545 end 546end 547 548 549class Pathname # * FileUtils * 550 # Creates a full path, including any intermediate directories that don't yet 551 # exist. 552 # 553 # See FileUtils.mkpath and FileUtils.mkdir_p 554 def mkpath 555 require 'fileutils' 556 FileUtils.mkpath(@path) 557 nil 558 end 559 560 # Recursively deletes a directory, including all directories beneath it. 561 # 562 # See FileUtils.rm_r 563 def rmtree 564 # The name "rmtree" is borrowed from File::Path of Perl. 565 # File::Path provides "mkpath" and "rmtree". 566 require 'fileutils' 567 FileUtils.rm_r(@path) 568 nil 569 end 570end 571 572