1# frozen_string_literal: true 2 3begin 4 require 'rbconfig' 5rescue LoadError 6 # for make mjit-headers 7end 8 9require "fileutils/version" 10 11# 12# = fileutils.rb 13# 14# Copyright (c) 2000-2007 Minero Aoki 15# 16# This program is free software. 17# You can distribute/modify this program under the same terms of ruby. 18# 19# == module FileUtils 20# 21# Namespace for several file utility methods for copying, moving, removing, etc. 22# 23# === Module Functions 24# 25# require 'fileutils' 26# 27# FileUtils.cd(dir, options) 28# FileUtils.cd(dir, options) {|dir| block } 29# FileUtils.pwd() 30# FileUtils.mkdir(dir, options) 31# FileUtils.mkdir(list, options) 32# FileUtils.mkdir_p(dir, options) 33# FileUtils.mkdir_p(list, options) 34# FileUtils.rmdir(dir, options) 35# FileUtils.rmdir(list, options) 36# FileUtils.ln(target, link, options) 37# FileUtils.ln(targets, dir, options) 38# FileUtils.ln_s(target, link, options) 39# FileUtils.ln_s(targets, dir, options) 40# FileUtils.ln_sf(target, link, options) 41# FileUtils.cp(src, dest, options) 42# FileUtils.cp(list, dir, options) 43# FileUtils.cp_r(src, dest, options) 44# FileUtils.cp_r(list, dir, options) 45# FileUtils.mv(src, dest, options) 46# FileUtils.mv(list, dir, options) 47# FileUtils.rm(list, options) 48# FileUtils.rm_r(list, options) 49# FileUtils.rm_rf(list, options) 50# FileUtils.install(src, dest, options) 51# FileUtils.chmod(mode, list, options) 52# FileUtils.chmod_R(mode, list, options) 53# FileUtils.chown(user, group, list, options) 54# FileUtils.chown_R(user, group, list, options) 55# FileUtils.touch(list, options) 56# 57# The <tt>options</tt> parameter is a hash of options, taken from the list 58# <tt>:force</tt>, <tt>:noop</tt>, <tt>:preserve</tt>, and <tt>:verbose</tt>. 59# <tt>:noop</tt> means that no changes are made. The other three are obvious. 60# Each method documents the options that it honours. 61# 62# All methods that have the concept of a "source" file or directory can take 63# either one file or a list of files in that argument. See the method 64# documentation for examples. 65# 66# There are some `low level' methods, which do not accept any option: 67# 68# FileUtils.copy_entry(src, dest, preserve = false, dereference_root = false, remove_destination = false) 69# FileUtils.copy_file(src, dest, preserve = false, dereference = true) 70# FileUtils.copy_stream(srcstream, deststream) 71# FileUtils.remove_entry(path, force = false) 72# FileUtils.remove_entry_secure(path, force = false) 73# FileUtils.remove_file(path, force = false) 74# FileUtils.compare_file(path_a, path_b) 75# FileUtils.compare_stream(stream_a, stream_b) 76# FileUtils.uptodate?(file, cmp_list) 77# 78# == module FileUtils::Verbose 79# 80# This module has all methods of FileUtils module, but it outputs messages 81# before acting. This equates to passing the <tt>:verbose</tt> flag to methods 82# in FileUtils. 83# 84# == module FileUtils::NoWrite 85# 86# This module has all methods of FileUtils module, but never changes 87# files/directories. This equates to passing the <tt>:noop</tt> flag to methods 88# in FileUtils. 89# 90# == module FileUtils::DryRun 91# 92# This module has all methods of FileUtils module, but never changes 93# files/directories. This equates to passing the <tt>:noop</tt> and 94# <tt>:verbose</tt> flags to methods in FileUtils. 95# 96module FileUtils 97 98 def self.private_module_function(name) #:nodoc: 99 module_function name 100 private_class_method name 101 end 102 103 # 104 # Returns the name of the current directory. 105 # 106 def pwd 107 Dir.pwd 108 end 109 module_function :pwd 110 111 alias getwd pwd 112 module_function :getwd 113 114 # 115 # Changes the current directory to the directory +dir+. 116 # 117 # If this method is called with block, resumes to the old 118 # working directory after the block execution finished. 119 # 120 # FileUtils.cd('/', :verbose => true) # chdir and report it 121 # 122 # FileUtils.cd('/') do # chdir 123 # # ... # do something 124 # end # return to original directory 125 # 126 def cd(dir, verbose: nil, &block) # :yield: dir 127 fu_output_message "cd #{dir}" if verbose 128 result = Dir.chdir(dir, &block) 129 fu_output_message 'cd -' if verbose and block 130 result 131 end 132 module_function :cd 133 134 alias chdir cd 135 module_function :chdir 136 137 # 138 # Returns true if +new+ is newer than all +old_list+. 139 # Non-existent files are older than any file. 140 # 141 # FileUtils.uptodate?('hello.o', %w(hello.c hello.h)) or \ 142 # system 'make hello.o' 143 # 144 def uptodate?(new, old_list) 145 return false unless File.exist?(new) 146 new_time = File.mtime(new) 147 old_list.each do |old| 148 if File.exist?(old) 149 return false unless new_time > File.mtime(old) 150 end 151 end 152 true 153 end 154 module_function :uptodate? 155 156 def remove_trailing_slash(dir) #:nodoc: 157 dir == '/' ? dir : dir.chomp(?/) 158 end 159 private_module_function :remove_trailing_slash 160 161 # 162 # Creates one or more directories. 163 # 164 # FileUtils.mkdir 'test' 165 # FileUtils.mkdir %w( tmp data ) 166 # FileUtils.mkdir 'notexist', :noop => true # Does not really create. 167 # FileUtils.mkdir 'tmp', :mode => 0700 168 # 169 def mkdir(list, mode: nil, noop: nil, verbose: nil) 170 list = fu_list(list) 171 fu_output_message "mkdir #{mode ? ('-m %03o ' % mode) : ''}#{list.join ' '}" if verbose 172 return if noop 173 174 list.each do |dir| 175 fu_mkdir dir, mode 176 end 177 end 178 module_function :mkdir 179 180 # 181 # Creates a directory and all its parent directories. 182 # For example, 183 # 184 # FileUtils.mkdir_p '/usr/local/lib/ruby' 185 # 186 # causes to make following directories, if it does not exist. 187 # 188 # * /usr 189 # * /usr/local 190 # * /usr/local/lib 191 # * /usr/local/lib/ruby 192 # 193 # You can pass several directories at a time in a list. 194 # 195 def mkdir_p(list, mode: nil, noop: nil, verbose: nil) 196 list = fu_list(list) 197 fu_output_message "mkdir -p #{mode ? ('-m %03o ' % mode) : ''}#{list.join ' '}" if verbose 198 return *list if noop 199 200 list.map {|path| remove_trailing_slash(path)}.each do |path| 201 # optimize for the most common case 202 begin 203 fu_mkdir path, mode 204 next 205 rescue SystemCallError 206 next if File.directory?(path) 207 end 208 209 stack = [] 210 until path == stack.last # dirname("/")=="/", dirname("C:/")=="C:/" 211 stack.push path 212 path = File.dirname(path) 213 end 214 stack.pop # root directory should exist 215 stack.reverse_each do |dir| 216 begin 217 fu_mkdir dir, mode 218 rescue SystemCallError 219 raise unless File.directory?(dir) 220 end 221 end 222 end 223 224 return *list 225 end 226 module_function :mkdir_p 227 228 alias mkpath mkdir_p 229 alias makedirs mkdir_p 230 module_function :mkpath 231 module_function :makedirs 232 233 def fu_mkdir(path, mode) #:nodoc: 234 path = remove_trailing_slash(path) 235 if mode 236 Dir.mkdir path, mode 237 File.chmod mode, path 238 else 239 Dir.mkdir path 240 end 241 end 242 private_module_function :fu_mkdir 243 244 # 245 # Removes one or more directories. 246 # 247 # FileUtils.rmdir 'somedir' 248 # FileUtils.rmdir %w(somedir anydir otherdir) 249 # # Does not really remove directory; outputs message. 250 # FileUtils.rmdir 'somedir', :verbose => true, :noop => true 251 # 252 def rmdir(list, parents: nil, noop: nil, verbose: nil) 253 list = fu_list(list) 254 fu_output_message "rmdir #{parents ? '-p ' : ''}#{list.join ' '}" if verbose 255 return if noop 256 list.each do |dir| 257 Dir.rmdir(dir = remove_trailing_slash(dir)) 258 if parents 259 begin 260 until (parent = File.dirname(dir)) == '.' or parent == dir 261 dir = parent 262 Dir.rmdir(dir) 263 end 264 rescue Errno::ENOTEMPTY, Errno::EEXIST, Errno::ENOENT 265 end 266 end 267 end 268 end 269 module_function :rmdir 270 271 # 272 # :call-seq: 273 # FileUtils.ln(target, link, force: nil, noop: nil, verbose: nil) 274 # FileUtils.ln(target, dir, force: nil, noop: nil, verbose: nil) 275 # FileUtils.ln(targets, dir, force: nil, noop: nil, verbose: nil) 276 # 277 # In the first form, creates a hard link +link+ which points to +target+. 278 # If +link+ already exists, raises Errno::EEXIST. 279 # But if the :force option is set, overwrites +link+. 280 # 281 # FileUtils.ln 'gcc', 'cc', verbose: true 282 # FileUtils.ln '/usr/bin/emacs21', '/usr/bin/emacs' 283 # 284 # In the second form, creates a link +dir/target+ pointing to +target+. 285 # In the third form, creates several hard links in the directory +dir+, 286 # pointing to each item in +targets+. 287 # If +dir+ is not a directory, raises Errno::ENOTDIR. 288 # 289 # FileUtils.cd '/sbin' 290 # FileUtils.ln %w(cp mv mkdir), '/bin' # Now /sbin/cp and /bin/cp are linked. 291 # 292 def ln(src, dest, force: nil, noop: nil, verbose: nil) 293 fu_output_message "ln#{force ? ' -f' : ''} #{[src,dest].flatten.join ' '}" if verbose 294 return if noop 295 fu_each_src_dest0(src, dest) do |s,d| 296 remove_file d, true if force 297 File.link s, d 298 end 299 end 300 module_function :ln 301 302 alias link ln 303 module_function :link 304 305 # 306 # :call-seq: 307 # FileUtils.cp_lr(src, dest, noop: nil, verbose: nil, dereference_root: true, remove_destination: false) 308 # 309 # Hard link +src+ to +dest+. If +src+ is a directory, this method links 310 # all its contents recursively. If +dest+ is a directory, links 311 # +src+ to +dest/src+. 312 # 313 # +src+ can be a list of files. 314 # 315 # # Installing the library "mylib" under the site_ruby directory. 316 # FileUtils.rm_r site_ruby + '/mylib', :force => true 317 # FileUtils.cp_lr 'lib/', site_ruby + '/mylib' 318 # 319 # # Examples of linking several files to target directory. 320 # FileUtils.cp_lr %w(mail.rb field.rb debug/), site_ruby + '/tmail' 321 # FileUtils.cp_lr Dir.glob('*.rb'), '/home/aamine/lib/ruby', :noop => true, :verbose => true 322 # 323 # # If you want to link all contents of a directory instead of the 324 # # directory itself, c.f. src/x -> dest/x, src/y -> dest/y, 325 # # use the following code. 326 # FileUtils.cp_lr 'src/.', 'dest' # cp_lr('src', 'dest') makes dest/src, but this doesn't. 327 # 328 def cp_lr(src, dest, noop: nil, verbose: nil, 329 dereference_root: true, remove_destination: false) 330 fu_output_message "cp -lr#{remove_destination ? ' --remove-destination' : ''} #{[src,dest].flatten.join ' '}" if verbose 331 return if noop 332 fu_each_src_dest(src, dest) do |s, d| 333 link_entry s, d, dereference_root, remove_destination 334 end 335 end 336 module_function :cp_lr 337 338 # 339 # :call-seq: 340 # FileUtils.ln_s(target, link, force: nil, noop: nil, verbose: nil) 341 # FileUtils.ln_s(target, dir, force: nil, noop: nil, verbose: nil) 342 # FileUtils.ln_s(targets, dir, force: nil, noop: nil, verbose: nil) 343 # 344 # In the first form, creates a symbolic link +link+ which points to +target+. 345 # If +link+ already exists, raises Errno::EEXIST. 346 # But if the :force option is set, overwrites +link+. 347 # 348 # FileUtils.ln_s '/usr/bin/ruby', '/usr/local/bin/ruby' 349 # FileUtils.ln_s 'verylongsourcefilename.c', 'c', force: true 350 # 351 # In the second form, creates a link +dir/target+ pointing to +target+. 352 # In the third form, creates several symbolic links in the directory +dir+, 353 # pointing to each item in +targets+. 354 # If +dir+ is not a directory, raises Errno::ENOTDIR. 355 # 356 # FileUtils.ln_s Dir.glob('/bin/*.rb'), '/home/foo/bin' 357 # 358 def ln_s(src, dest, force: nil, noop: nil, verbose: nil) 359 fu_output_message "ln -s#{force ? 'f' : ''} #{[src,dest].flatten.join ' '}" if verbose 360 return if noop 361 fu_each_src_dest0(src, dest) do |s,d| 362 remove_file d, true if force 363 File.symlink s, d 364 end 365 end 366 module_function :ln_s 367 368 alias symlink ln_s 369 module_function :symlink 370 371 # 372 # :call-seq: 373 # FileUtils.ln_sf(*args) 374 # 375 # Same as 376 # 377 # FileUtils.ln_s(*args, force: true) 378 # 379 def ln_sf(src, dest, noop: nil, verbose: nil) 380 ln_s src, dest, force: true, noop: noop, verbose: verbose 381 end 382 module_function :ln_sf 383 384 # 385 # Hard links a file system entry +src+ to +dest+. 386 # If +src+ is a directory, this method links its contents recursively. 387 # 388 # Both of +src+ and +dest+ must be a path name. 389 # +src+ must exist, +dest+ must not exist. 390 # 391 # If +dereference_root+ is true, this method dereferences the tree root. 392 # 393 # If +remove_destination+ is true, this method removes each destination file before copy. 394 # 395 def link_entry(src, dest, dereference_root = false, remove_destination = false) 396 Entry_.new(src, nil, dereference_root).traverse do |ent| 397 destent = Entry_.new(dest, ent.rel, false) 398 File.unlink destent.path if remove_destination && File.file?(destent.path) 399 ent.link destent.path 400 end 401 end 402 module_function :link_entry 403 404 # 405 # Copies a file content +src+ to +dest+. If +dest+ is a directory, 406 # copies +src+ to +dest/src+. 407 # 408 # If +src+ is a list of files, then +dest+ must be a directory. 409 # 410 # FileUtils.cp 'eval.c', 'eval.c.org' 411 # FileUtils.cp %w(cgi.rb complex.rb date.rb), '/usr/lib/ruby/1.6' 412 # FileUtils.cp %w(cgi.rb complex.rb date.rb), '/usr/lib/ruby/1.6', :verbose => true 413 # FileUtils.cp 'symlink', 'dest' # copy content, "dest" is not a symlink 414 # 415 def cp(src, dest, preserve: nil, noop: nil, verbose: nil) 416 fu_output_message "cp#{preserve ? ' -p' : ''} #{[src,dest].flatten.join ' '}" if verbose 417 return if noop 418 fu_each_src_dest(src, dest) do |s, d| 419 copy_file s, d, preserve 420 end 421 end 422 module_function :cp 423 424 alias copy cp 425 module_function :copy 426 427 # 428 # Copies +src+ to +dest+. If +src+ is a directory, this method copies 429 # all its contents recursively. If +dest+ is a directory, copies 430 # +src+ to +dest/src+. 431 # 432 # +src+ can be a list of files. 433 # 434 # # Installing Ruby library "mylib" under the site_ruby 435 # FileUtils.rm_r site_ruby + '/mylib', :force 436 # FileUtils.cp_r 'lib/', site_ruby + '/mylib' 437 # 438 # # Examples of copying several files to target directory. 439 # FileUtils.cp_r %w(mail.rb field.rb debug/), site_ruby + '/tmail' 440 # FileUtils.cp_r Dir.glob('*.rb'), '/home/foo/lib/ruby', :noop => true, :verbose => true 441 # 442 # # If you want to copy all contents of a directory instead of the 443 # # directory itself, c.f. src/x -> dest/x, src/y -> dest/y, 444 # # use following code. 445 # FileUtils.cp_r 'src/.', 'dest' # cp_r('src', 'dest') makes dest/src, 446 # # but this doesn't. 447 # 448 def cp_r(src, dest, preserve: nil, noop: nil, verbose: nil, 449 dereference_root: true, remove_destination: nil) 450 fu_output_message "cp -r#{preserve ? 'p' : ''}#{remove_destination ? ' --remove-destination' : ''} #{[src,dest].flatten.join ' '}" if verbose 451 return if noop 452 fu_each_src_dest(src, dest) do |s, d| 453 copy_entry s, d, preserve, dereference_root, remove_destination 454 end 455 end 456 module_function :cp_r 457 458 # 459 # Copies a file system entry +src+ to +dest+. 460 # If +src+ is a directory, this method copies its contents recursively. 461 # This method preserves file types, c.f. symlink, directory... 462 # (FIFO, device files and etc. are not supported yet) 463 # 464 # Both of +src+ and +dest+ must be a path name. 465 # +src+ must exist, +dest+ must not exist. 466 # 467 # If +preserve+ is true, this method preserves owner, group, and 468 # modified time. Permissions are copied regardless +preserve+. 469 # 470 # If +dereference_root+ is true, this method dereference tree root. 471 # 472 # If +remove_destination+ is true, this method removes each destination file before copy. 473 # 474 def copy_entry(src, dest, preserve = false, dereference_root = false, remove_destination = false) 475 Entry_.new(src, nil, dereference_root).wrap_traverse(proc do |ent| 476 destent = Entry_.new(dest, ent.rel, false) 477 File.unlink destent.path if remove_destination && (File.file?(destent.path) || File.symlink?(destent.path)) 478 ent.copy destent.path 479 end, proc do |ent| 480 destent = Entry_.new(dest, ent.rel, false) 481 ent.copy_metadata destent.path if preserve 482 end) 483 end 484 module_function :copy_entry 485 486 # 487 # Copies file contents of +src+ to +dest+. 488 # Both of +src+ and +dest+ must be a path name. 489 # 490 def copy_file(src, dest, preserve = false, dereference = true) 491 ent = Entry_.new(src, nil, dereference) 492 ent.copy_file dest 493 ent.copy_metadata dest if preserve 494 end 495 module_function :copy_file 496 497 # 498 # Copies stream +src+ to +dest+. 499 # +src+ must respond to #read(n) and 500 # +dest+ must respond to #write(str). 501 # 502 def copy_stream(src, dest) 503 IO.copy_stream(src, dest) 504 end 505 module_function :copy_stream 506 507 # 508 # Moves file(s) +src+ to +dest+. If +file+ and +dest+ exist on the different 509 # disk partition, the file is copied then the original file is removed. 510 # 511 # FileUtils.mv 'badname.rb', 'goodname.rb' 512 # FileUtils.mv 'stuff.rb', '/notexist/lib/ruby', :force => true # no error 513 # 514 # FileUtils.mv %w(junk.txt dust.txt), '/home/foo/.trash/' 515 # FileUtils.mv Dir.glob('test*.rb'), 'test', :noop => true, :verbose => true 516 # 517 def mv(src, dest, force: nil, noop: nil, verbose: nil, secure: nil) 518 fu_output_message "mv#{force ? ' -f' : ''} #{[src,dest].flatten.join ' '}" if verbose 519 return if noop 520 fu_each_src_dest(src, dest) do |s, d| 521 destent = Entry_.new(d, nil, true) 522 begin 523 if destent.exist? 524 if destent.directory? 525 raise Errno::EEXIST, d 526 end 527 end 528 begin 529 File.rename s, d 530 rescue Errno::EXDEV 531 copy_entry s, d, true 532 if secure 533 remove_entry_secure s, force 534 else 535 remove_entry s, force 536 end 537 end 538 rescue SystemCallError 539 raise unless force 540 end 541 end 542 end 543 module_function :mv 544 545 alias move mv 546 module_function :move 547 548 # 549 # Remove file(s) specified in +list+. This method cannot remove directories. 550 # All StandardErrors are ignored when the :force option is set. 551 # 552 # FileUtils.rm %w( junk.txt dust.txt ) 553 # FileUtils.rm Dir.glob('*.so') 554 # FileUtils.rm 'NotExistFile', :force => true # never raises exception 555 # 556 def rm(list, force: nil, noop: nil, verbose: nil) 557 list = fu_list(list) 558 fu_output_message "rm#{force ? ' -f' : ''} #{list.join ' '}" if verbose 559 return if noop 560 561 list.each do |path| 562 remove_file path, force 563 end 564 end 565 module_function :rm 566 567 alias remove rm 568 module_function :remove 569 570 # 571 # Equivalent to 572 # 573 # FileUtils.rm(list, :force => true) 574 # 575 def rm_f(list, noop: nil, verbose: nil) 576 rm list, force: true, noop: noop, verbose: verbose 577 end 578 module_function :rm_f 579 580 alias safe_unlink rm_f 581 module_function :safe_unlink 582 583 # 584 # remove files +list+[0] +list+[1]... If +list+[n] is a directory, 585 # removes its all contents recursively. This method ignores 586 # StandardError when :force option is set. 587 # 588 # FileUtils.rm_r Dir.glob('/tmp/*') 589 # FileUtils.rm_r 'some_dir', :force => true 590 # 591 # WARNING: This method causes local vulnerability 592 # if one of parent directories or removing directory tree are world 593 # writable (including /tmp, whose permission is 1777), and the current 594 # process has strong privilege such as Unix super user (root), and the 595 # system has symbolic link. For secure removing, read the documentation 596 # of #remove_entry_secure carefully, and set :secure option to true. 597 # Default is :secure=>false. 598 # 599 # NOTE: This method calls #remove_entry_secure if :secure option is set. 600 # See also #remove_entry_secure. 601 # 602 def rm_r(list, force: nil, noop: nil, verbose: nil, secure: nil) 603 list = fu_list(list) 604 fu_output_message "rm -r#{force ? 'f' : ''} #{list.join ' '}" if verbose 605 return if noop 606 list.each do |path| 607 if secure 608 remove_entry_secure path, force 609 else 610 remove_entry path, force 611 end 612 end 613 end 614 module_function :rm_r 615 616 # 617 # Equivalent to 618 # 619 # FileUtils.rm_r(list, :force => true) 620 # 621 # WARNING: This method causes local vulnerability. 622 # Read the documentation of #rm_r first. 623 # 624 def rm_rf(list, noop: nil, verbose: nil, secure: nil) 625 rm_r list, force: true, noop: noop, verbose: verbose, secure: secure 626 end 627 module_function :rm_rf 628 629 alias rmtree rm_rf 630 module_function :rmtree 631 632 # 633 # This method removes a file system entry +path+. +path+ shall be a 634 # regular file, a directory, or something. If +path+ is a directory, 635 # remove it recursively. This method is required to avoid TOCTTOU 636 # (time-of-check-to-time-of-use) local security vulnerability of #rm_r. 637 # #rm_r causes security hole when: 638 # 639 # * Parent directory is world writable (including /tmp). 640 # * Removing directory tree includes world writable directory. 641 # * The system has symbolic link. 642 # 643 # To avoid this security hole, this method applies special preprocess. 644 # If +path+ is a directory, this method chown(2) and chmod(2) all 645 # removing directories. This requires the current process is the 646 # owner of the removing whole directory tree, or is the super user (root). 647 # 648 # WARNING: You must ensure that *ALL* parent directories cannot be 649 # moved by other untrusted users. For example, parent directories 650 # should not be owned by untrusted users, and should not be world 651 # writable except when the sticky bit set. 652 # 653 # WARNING: Only the owner of the removing directory tree, or Unix super 654 # user (root) should invoke this method. Otherwise this method does not 655 # work. 656 # 657 # For details of this security vulnerability, see Perl's case: 658 # 659 # * https://cve.mitre.org/cgi-bin/cvename.cgi?name=CAN-2005-0448 660 # * https://cve.mitre.org/cgi-bin/cvename.cgi?name=CAN-2004-0452 661 # 662 # For fileutils.rb, this vulnerability is reported in [ruby-dev:26100]. 663 # 664 def remove_entry_secure(path, force = false) 665 unless fu_have_symlink? 666 remove_entry path, force 667 return 668 end 669 fullpath = File.expand_path(path) 670 st = File.lstat(fullpath) 671 unless st.directory? 672 File.unlink fullpath 673 return 674 end 675 # is a directory. 676 parent_st = File.stat(File.dirname(fullpath)) 677 unless parent_st.world_writable? 678 remove_entry path, force 679 return 680 end 681 unless parent_st.sticky? 682 raise ArgumentError, "parent directory is world writable, FileUtils#remove_entry_secure does not work; abort: #{path.inspect} (parent directory mode #{'%o' % parent_st.mode})" 683 end 684 685 # freeze tree root 686 euid = Process.euid 687 dot_file = fullpath + "/." 688 begin 689 File.open(dot_file) {|f| 690 unless fu_stat_identical_entry?(st, f.stat) 691 # symlink (TOC-to-TOU attack?) 692 File.unlink fullpath 693 return 694 end 695 f.chown euid, -1 696 f.chmod 0700 697 } 698 rescue Errno::EISDIR # JRuby in non-native mode can't open files as dirs 699 File.lstat(dot_file).tap {|fstat| 700 unless fu_stat_identical_entry?(st, fstat) 701 # symlink (TOC-to-TOU attack?) 702 File.unlink fullpath 703 return 704 end 705 File.chown euid, -1, dot_file 706 File.chmod 0700, dot_file 707 } 708 end 709 710 unless fu_stat_identical_entry?(st, File.lstat(fullpath)) 711 # TOC-to-TOU attack? 712 File.unlink fullpath 713 return 714 end 715 716 # ---- tree root is frozen ---- 717 root = Entry_.new(path) 718 root.preorder_traverse do |ent| 719 if ent.directory? 720 ent.chown euid, -1 721 ent.chmod 0700 722 end 723 end 724 root.postorder_traverse do |ent| 725 begin 726 ent.remove 727 rescue 728 raise unless force 729 end 730 end 731 rescue 732 raise unless force 733 end 734 module_function :remove_entry_secure 735 736 def fu_have_symlink? #:nodoc: 737 File.symlink nil, nil 738 rescue NotImplementedError 739 return false 740 rescue TypeError 741 return true 742 end 743 private_module_function :fu_have_symlink? 744 745 def fu_stat_identical_entry?(a, b) #:nodoc: 746 a.dev == b.dev and a.ino == b.ino 747 end 748 private_module_function :fu_stat_identical_entry? 749 750 # 751 # This method removes a file system entry +path+. 752 # +path+ might be a regular file, a directory, or something. 753 # If +path+ is a directory, remove it recursively. 754 # 755 # See also #remove_entry_secure. 756 # 757 def remove_entry(path, force = false) 758 Entry_.new(path).postorder_traverse do |ent| 759 begin 760 ent.remove 761 rescue 762 raise unless force 763 end 764 end 765 rescue 766 raise unless force 767 end 768 module_function :remove_entry 769 770 # 771 # Removes a file +path+. 772 # This method ignores StandardError if +force+ is true. 773 # 774 def remove_file(path, force = false) 775 Entry_.new(path).remove_file 776 rescue 777 raise unless force 778 end 779 module_function :remove_file 780 781 # 782 # Removes a directory +dir+ and its contents recursively. 783 # This method ignores StandardError if +force+ is true. 784 # 785 def remove_dir(path, force = false) 786 remove_entry path, force # FIXME?? check if it is a directory 787 end 788 module_function :remove_dir 789 790 # 791 # Returns true if the contents of a file +a+ and a file +b+ are identical. 792 # 793 # FileUtils.compare_file('somefile', 'somefile') #=> true 794 # FileUtils.compare_file('/dev/null', '/dev/urandom') #=> false 795 # 796 def compare_file(a, b) 797 return false unless File.size(a) == File.size(b) 798 File.open(a, 'rb') {|fa| 799 File.open(b, 'rb') {|fb| 800 return compare_stream(fa, fb) 801 } 802 } 803 end 804 module_function :compare_file 805 806 alias identical? compare_file 807 alias cmp compare_file 808 module_function :identical? 809 module_function :cmp 810 811 # 812 # Returns true if the contents of a stream +a+ and +b+ are identical. 813 # 814 def compare_stream(a, b) 815 bsize = fu_stream_blksize(a, b) 816 817 if RUBY_VERSION > "2.4" 818 sa = String.new(capacity: bsize) 819 sb = String.new(capacity: bsize) 820 else 821 sa = String.new 822 sb = String.new 823 end 824 825 begin 826 a.read(bsize, sa) 827 b.read(bsize, sb) 828 return true if sa.empty? && sb.empty? 829 end while sa == sb 830 false 831 end 832 module_function :compare_stream 833 834 # 835 # If +src+ is not same as +dest+, copies it and changes the permission 836 # mode to +mode+. If +dest+ is a directory, destination is +dest+/+src+. 837 # This method removes destination before copy. 838 # 839 # FileUtils.install 'ruby', '/usr/local/bin/ruby', :mode => 0755, :verbose => true 840 # FileUtils.install 'lib.rb', '/usr/local/lib/ruby/site_ruby', :verbose => true 841 # 842 def install(src, dest, mode: nil, owner: nil, group: nil, preserve: nil, 843 noop: nil, verbose: nil) 844 if verbose 845 msg = +"install -c" 846 msg << ' -p' if preserve 847 msg << ' -m ' << mode_to_s(mode) if mode 848 msg << " -o #{owner}" if owner 849 msg << " -g #{group}" if group 850 msg << ' ' << [src,dest].flatten.join(' ') 851 fu_output_message msg 852 end 853 return if noop 854 uid = fu_get_uid(owner) 855 gid = fu_get_gid(group) 856 fu_each_src_dest(src, dest) do |s, d| 857 st = File.stat(s) 858 unless File.exist?(d) and compare_file(s, d) 859 remove_file d, true 860 copy_file s, d 861 File.utime st.atime, st.mtime, d if preserve 862 File.chmod fu_mode(mode, st), d if mode 863 File.chown uid, gid, d if uid or gid 864 end 865 end 866 end 867 module_function :install 868 869 def user_mask(target) #:nodoc: 870 target.each_char.inject(0) do |mask, chr| 871 case chr 872 when "u" 873 mask | 04700 874 when "g" 875 mask | 02070 876 when "o" 877 mask | 01007 878 when "a" 879 mask | 07777 880 else 881 raise ArgumentError, "invalid `who' symbol in file mode: #{chr}" 882 end 883 end 884 end 885 private_module_function :user_mask 886 887 def apply_mask(mode, user_mask, op, mode_mask) #:nodoc: 888 case op 889 when '=' 890 (mode & ~user_mask) | (user_mask & mode_mask) 891 when '+' 892 mode | (user_mask & mode_mask) 893 when '-' 894 mode & ~(user_mask & mode_mask) 895 end 896 end 897 private_module_function :apply_mask 898 899 def symbolic_modes_to_i(mode_sym, path) #:nodoc: 900 mode = if File::Stat === path 901 path.mode 902 else 903 File.stat(path).mode 904 end 905 mode_sym.split(/,/).inject(mode & 07777) do |current_mode, clause| 906 target, *actions = clause.split(/([=+-])/) 907 raise ArgumentError, "invalid file mode: #{mode_sym}" if actions.empty? 908 target = 'a' if target.empty? 909 user_mask = user_mask(target) 910 actions.each_slice(2) do |op, perm| 911 need_apply = op == '=' 912 mode_mask = (perm || '').each_char.inject(0) do |mask, chr| 913 case chr 914 when "r" 915 mask | 0444 916 when "w" 917 mask | 0222 918 when "x" 919 mask | 0111 920 when "X" 921 if FileTest.directory? path 922 mask | 0111 923 else 924 mask 925 end 926 when "s" 927 mask | 06000 928 when "t" 929 mask | 01000 930 when "u", "g", "o" 931 if mask.nonzero? 932 current_mode = apply_mask(current_mode, user_mask, op, mask) 933 end 934 need_apply = false 935 copy_mask = user_mask(chr) 936 (current_mode & copy_mask) / (copy_mask & 0111) * (user_mask & 0111) 937 else 938 raise ArgumentError, "invalid `perm' symbol in file mode: #{chr}" 939 end 940 end 941 942 if mode_mask.nonzero? || need_apply 943 current_mode = apply_mask(current_mode, user_mask, op, mode_mask) 944 end 945 end 946 current_mode 947 end 948 end 949 private_module_function :symbolic_modes_to_i 950 951 def fu_mode(mode, path) #:nodoc: 952 mode.is_a?(String) ? symbolic_modes_to_i(mode, path) : mode 953 end 954 private_module_function :fu_mode 955 956 def mode_to_s(mode) #:nodoc: 957 mode.is_a?(String) ? mode : "%o" % mode 958 end 959 private_module_function :mode_to_s 960 961 # 962 # Changes permission bits on the named files (in +list+) to the bit pattern 963 # represented by +mode+. 964 # 965 # +mode+ is the symbolic and absolute mode can be used. 966 # 967 # Absolute mode is 968 # FileUtils.chmod 0755, 'somecommand' 969 # FileUtils.chmod 0644, %w(my.rb your.rb his.rb her.rb) 970 # FileUtils.chmod 0755, '/usr/bin/ruby', :verbose => true 971 # 972 # Symbolic mode is 973 # FileUtils.chmod "u=wrx,go=rx", 'somecommand' 974 # FileUtils.chmod "u=wr,go=rr", %w(my.rb your.rb his.rb her.rb) 975 # FileUtils.chmod "u=wrx,go=rx", '/usr/bin/ruby', :verbose => true 976 # 977 # "a" :: is user, group, other mask. 978 # "u" :: is user's mask. 979 # "g" :: is group's mask. 980 # "o" :: is other's mask. 981 # "w" :: is write permission. 982 # "r" :: is read permission. 983 # "x" :: is execute permission. 984 # "X" :: 985 # is execute permission for directories only, must be used in conjunction with "+" 986 # "s" :: is uid, gid. 987 # "t" :: is sticky bit. 988 # "+" :: is added to a class given the specified mode. 989 # "-" :: Is removed from a given class given mode. 990 # "=" :: Is the exact nature of the class will be given a specified mode. 991 992 def chmod(mode, list, noop: nil, verbose: nil) 993 list = fu_list(list) 994 fu_output_message sprintf('chmod %s %s', mode_to_s(mode), list.join(' ')) if verbose 995 return if noop 996 list.each do |path| 997 Entry_.new(path).chmod(fu_mode(mode, path)) 998 end 999 end 1000 module_function :chmod 1001 1002 # 1003 # Changes permission bits on the named files (in +list+) 1004 # to the bit pattern represented by +mode+. 1005 # 1006 # FileUtils.chmod_R 0700, "/tmp/app.#{$$}" 1007 # FileUtils.chmod_R "u=wrx", "/tmp/app.#{$$}" 1008 # 1009 def chmod_R(mode, list, noop: nil, verbose: nil, force: nil) 1010 list = fu_list(list) 1011 fu_output_message sprintf('chmod -R%s %s %s', 1012 (force ? 'f' : ''), 1013 mode_to_s(mode), list.join(' ')) if verbose 1014 return if noop 1015 list.each do |root| 1016 Entry_.new(root).traverse do |ent| 1017 begin 1018 ent.chmod(fu_mode(mode, ent.path)) 1019 rescue 1020 raise unless force 1021 end 1022 end 1023 end 1024 end 1025 module_function :chmod_R 1026 1027 # 1028 # Changes owner and group on the named files (in +list+) 1029 # to the user +user+ and the group +group+. +user+ and +group+ 1030 # may be an ID (Integer/String) or a name (String). 1031 # If +user+ or +group+ is nil, this method does not change 1032 # the attribute. 1033 # 1034 # FileUtils.chown 'root', 'staff', '/usr/local/bin/ruby' 1035 # FileUtils.chown nil, 'bin', Dir.glob('/usr/bin/*'), :verbose => true 1036 # 1037 def chown(user, group, list, noop: nil, verbose: nil) 1038 list = fu_list(list) 1039 fu_output_message sprintf('chown %s %s', 1040 (group ? "#{user}:#{group}" : user || ':'), 1041 list.join(' ')) if verbose 1042 return if noop 1043 uid = fu_get_uid(user) 1044 gid = fu_get_gid(group) 1045 list.each do |path| 1046 Entry_.new(path).chown uid, gid 1047 end 1048 end 1049 module_function :chown 1050 1051 # 1052 # Changes owner and group on the named files (in +list+) 1053 # to the user +user+ and the group +group+ recursively. 1054 # +user+ and +group+ may be an ID (Integer/String) or 1055 # a name (String). If +user+ or +group+ is nil, this 1056 # method does not change the attribute. 1057 # 1058 # FileUtils.chown_R 'www', 'www', '/var/www/htdocs' 1059 # FileUtils.chown_R 'cvs', 'cvs', '/var/cvs', :verbose => true 1060 # 1061 def chown_R(user, group, list, noop: nil, verbose: nil, force: nil) 1062 list = fu_list(list) 1063 fu_output_message sprintf('chown -R%s %s %s', 1064 (force ? 'f' : ''), 1065 (group ? "#{user}:#{group}" : user || ':'), 1066 list.join(' ')) if verbose 1067 return if noop 1068 uid = fu_get_uid(user) 1069 gid = fu_get_gid(group) 1070 list.each do |root| 1071 Entry_.new(root).traverse do |ent| 1072 begin 1073 ent.chown uid, gid 1074 rescue 1075 raise unless force 1076 end 1077 end 1078 end 1079 end 1080 module_function :chown_R 1081 1082 begin 1083 require 'etc' 1084 rescue LoadError # rescue LoadError for miniruby 1085 end 1086 1087 def fu_get_uid(user) #:nodoc: 1088 return nil unless user 1089 case user 1090 when Integer 1091 user 1092 when /\A\d+\z/ 1093 user.to_i 1094 else 1095 Etc.getpwnam(user) ? Etc.getpwnam(user).uid : nil 1096 end 1097 end 1098 private_module_function :fu_get_uid 1099 1100 def fu_get_gid(group) #:nodoc: 1101 return nil unless group 1102 case group 1103 when Integer 1104 group 1105 when /\A\d+\z/ 1106 group.to_i 1107 else 1108 Etc.getgrnam(group) ? Etc.getgrnam(group).gid : nil 1109 end 1110 end 1111 private_module_function :fu_get_gid 1112 1113 # 1114 # Updates modification time (mtime) and access time (atime) of file(s) in 1115 # +list+. Files are created if they don't exist. 1116 # 1117 # FileUtils.touch 'timestamp' 1118 # FileUtils.touch Dir.glob('*.c'); system 'make' 1119 # 1120 def touch(list, noop: nil, verbose: nil, mtime: nil, nocreate: nil) 1121 list = fu_list(list) 1122 t = mtime 1123 if verbose 1124 fu_output_message "touch #{nocreate ? '-c ' : ''}#{t ? t.strftime('-t %Y%m%d%H%M.%S ') : ''}#{list.join ' '}" 1125 end 1126 return if noop 1127 list.each do |path| 1128 created = nocreate 1129 begin 1130 File.utime(t, t, path) 1131 rescue Errno::ENOENT 1132 raise if created 1133 File.open(path, 'a') { 1134 ; 1135 } 1136 created = true 1137 retry if t 1138 end 1139 end 1140 end 1141 module_function :touch 1142 1143 private 1144 1145 module StreamUtils_ 1146 private 1147 1148 case (defined?(::RbConfig) ? ::RbConfig::CONFIG['host_os'] : ::RUBY_PLATFORM) 1149 when /mswin|mingw/ 1150 def fu_windows?; true end 1151 else 1152 def fu_windows?; false end 1153 end 1154 1155 def fu_copy_stream0(src, dest, blksize = nil) #:nodoc: 1156 IO.copy_stream(src, dest) 1157 end 1158 1159 def fu_stream_blksize(*streams) 1160 streams.each do |s| 1161 next unless s.respond_to?(:stat) 1162 size = fu_blksize(s.stat) 1163 return size if size 1164 end 1165 fu_default_blksize() 1166 end 1167 1168 def fu_blksize(st) 1169 s = st.blksize 1170 return nil unless s 1171 return nil if s == 0 1172 s 1173 end 1174 1175 def fu_default_blksize 1176 1024 1177 end 1178 end 1179 1180 include StreamUtils_ 1181 extend StreamUtils_ 1182 1183 class Entry_ #:nodoc: internal use only 1184 include StreamUtils_ 1185 1186 def initialize(a, b = nil, deref = false) 1187 @prefix = @rel = @path = nil 1188 if b 1189 @prefix = a 1190 @rel = b 1191 else 1192 @path = a 1193 end 1194 @deref = deref 1195 @stat = nil 1196 @lstat = nil 1197 end 1198 1199 def inspect 1200 "\#<#{self.class} #{path()}>" 1201 end 1202 1203 def path 1204 if @path 1205 File.path(@path) 1206 else 1207 join(@prefix, @rel) 1208 end 1209 end 1210 1211 def prefix 1212 @prefix || @path 1213 end 1214 1215 def rel 1216 @rel 1217 end 1218 1219 def dereference? 1220 @deref 1221 end 1222 1223 def exist? 1224 begin 1225 lstat 1226 true 1227 rescue Errno::ENOENT 1228 false 1229 end 1230 end 1231 1232 def file? 1233 s = lstat! 1234 s and s.file? 1235 end 1236 1237 def directory? 1238 s = lstat! 1239 s and s.directory? 1240 end 1241 1242 def symlink? 1243 s = lstat! 1244 s and s.symlink? 1245 end 1246 1247 def chardev? 1248 s = lstat! 1249 s and s.chardev? 1250 end 1251 1252 def blockdev? 1253 s = lstat! 1254 s and s.blockdev? 1255 end 1256 1257 def socket? 1258 s = lstat! 1259 s and s.socket? 1260 end 1261 1262 def pipe? 1263 s = lstat! 1264 s and s.pipe? 1265 end 1266 1267 S_IF_DOOR = 0xD000 1268 1269 def door? 1270 s = lstat! 1271 s and (s.mode & 0xF000 == S_IF_DOOR) 1272 end 1273 1274 def entries 1275 opts = {} 1276 opts[:encoding] = ::Encoding::UTF_8 if fu_windows? 1277 Dir.children(path, opts)\ 1278 .map {|n| Entry_.new(prefix(), join(rel(), n.untaint)) } 1279 end 1280 1281 def stat 1282 return @stat if @stat 1283 if lstat() and lstat().symlink? 1284 @stat = File.stat(path()) 1285 else 1286 @stat = lstat() 1287 end 1288 @stat 1289 end 1290 1291 def stat! 1292 return @stat if @stat 1293 if lstat! and lstat!.symlink? 1294 @stat = File.stat(path()) 1295 else 1296 @stat = lstat! 1297 end 1298 @stat 1299 rescue SystemCallError 1300 nil 1301 end 1302 1303 def lstat 1304 if dereference? 1305 @lstat ||= File.stat(path()) 1306 else 1307 @lstat ||= File.lstat(path()) 1308 end 1309 end 1310 1311 def lstat! 1312 lstat() 1313 rescue SystemCallError 1314 nil 1315 end 1316 1317 def chmod(mode) 1318 if symlink? 1319 File.lchmod mode, path() if have_lchmod? 1320 else 1321 File.chmod mode, path() 1322 end 1323 rescue Errno::EOPNOTSUPP 1324 end 1325 1326 def chown(uid, gid) 1327 if symlink? 1328 File.lchown uid, gid, path() if have_lchown? 1329 else 1330 File.chown uid, gid, path() 1331 end 1332 end 1333 1334 def link(dest) 1335 case 1336 when directory? 1337 if !File.exist?(dest) and descendant_directory?(dest, path) 1338 raise ArgumentError, "cannot link directory %s to itself %s" % [path, dest] 1339 end 1340 begin 1341 Dir.mkdir dest 1342 rescue 1343 raise unless File.directory?(dest) 1344 end 1345 else 1346 File.link path(), dest 1347 end 1348 end 1349 1350 def copy(dest) 1351 lstat 1352 case 1353 when file? 1354 copy_file dest 1355 when directory? 1356 if !File.exist?(dest) and descendant_directory?(dest, path) 1357 raise ArgumentError, "cannot copy directory %s to itself %s" % [path, dest] 1358 end 1359 begin 1360 Dir.mkdir dest 1361 rescue 1362 raise unless File.directory?(dest) 1363 end 1364 when symlink? 1365 File.symlink File.readlink(path()), dest 1366 when chardev? 1367 raise "cannot handle device file" unless File.respond_to?(:mknod) 1368 mknod dest, ?c, 0666, lstat().rdev 1369 when blockdev? 1370 raise "cannot handle device file" unless File.respond_to?(:mknod) 1371 mknod dest, ?b, 0666, lstat().rdev 1372 when socket? 1373 raise "cannot handle socket" unless File.respond_to?(:mknod) 1374 mknod dest, nil, lstat().mode, 0 1375 when pipe? 1376 raise "cannot handle FIFO" unless File.respond_to?(:mkfifo) 1377 mkfifo dest, 0666 1378 when door? 1379 raise "cannot handle door: #{path()}" 1380 else 1381 raise "unknown file type: #{path()}" 1382 end 1383 end 1384 1385 def copy_file(dest) 1386 File.open(path()) do |s| 1387 File.open(dest, 'wb', s.stat.mode) do |f| 1388 IO.copy_stream(s, f) 1389 end 1390 end 1391 end 1392 1393 def copy_metadata(path) 1394 st = lstat() 1395 if !st.symlink? 1396 File.utime st.atime, st.mtime, path 1397 end 1398 mode = st.mode 1399 begin 1400 if st.symlink? 1401 begin 1402 File.lchown st.uid, st.gid, path 1403 rescue NotImplementedError 1404 end 1405 else 1406 File.chown st.uid, st.gid, path 1407 end 1408 rescue Errno::EPERM, Errno::EACCES 1409 # clear setuid/setgid 1410 mode &= 01777 1411 end 1412 if st.symlink? 1413 begin 1414 File.lchmod mode, path 1415 rescue NotImplementedError, Errno::EOPNOTSUPP 1416 end 1417 else 1418 File.chmod mode, path 1419 end 1420 end 1421 1422 def remove 1423 if directory? 1424 remove_dir1 1425 else 1426 remove_file 1427 end 1428 end 1429 1430 def remove_dir1 1431 platform_support { 1432 Dir.rmdir path().chomp(?/) 1433 } 1434 end 1435 1436 def remove_file 1437 platform_support { 1438 File.unlink path 1439 } 1440 end 1441 1442 def platform_support 1443 return yield unless fu_windows? 1444 first_time_p = true 1445 begin 1446 yield 1447 rescue Errno::ENOENT 1448 raise 1449 rescue => err 1450 if first_time_p 1451 first_time_p = false 1452 begin 1453 File.chmod 0700, path() # Windows does not have symlink 1454 retry 1455 rescue SystemCallError 1456 end 1457 end 1458 raise err 1459 end 1460 end 1461 1462 def preorder_traverse 1463 stack = [self] 1464 while ent = stack.pop 1465 yield ent 1466 stack.concat ent.entries.reverse if ent.directory? 1467 end 1468 end 1469 1470 alias traverse preorder_traverse 1471 1472 def postorder_traverse 1473 if directory? 1474 entries().each do |ent| 1475 ent.postorder_traverse do |e| 1476 yield e 1477 end 1478 end 1479 end 1480 ensure 1481 yield self 1482 end 1483 1484 def wrap_traverse(pre, post) 1485 pre.call self 1486 if directory? 1487 entries.each do |ent| 1488 ent.wrap_traverse pre, post 1489 end 1490 end 1491 post.call self 1492 end 1493 1494 private 1495 1496 $fileutils_rb_have_lchmod = nil 1497 1498 def have_lchmod? 1499 # This is not MT-safe, but it does not matter. 1500 if $fileutils_rb_have_lchmod == nil 1501 $fileutils_rb_have_lchmod = check_have_lchmod? 1502 end 1503 $fileutils_rb_have_lchmod 1504 end 1505 1506 def check_have_lchmod? 1507 return false unless File.respond_to?(:lchmod) 1508 File.lchmod 0 1509 return true 1510 rescue NotImplementedError 1511 return false 1512 end 1513 1514 $fileutils_rb_have_lchown = nil 1515 1516 def have_lchown? 1517 # This is not MT-safe, but it does not matter. 1518 if $fileutils_rb_have_lchown == nil 1519 $fileutils_rb_have_lchown = check_have_lchown? 1520 end 1521 $fileutils_rb_have_lchown 1522 end 1523 1524 def check_have_lchown? 1525 return false unless File.respond_to?(:lchown) 1526 File.lchown nil, nil 1527 return true 1528 rescue NotImplementedError 1529 return false 1530 end 1531 1532 def join(dir, base) 1533 return File.path(dir) if not base or base == '.' 1534 return File.path(base) if not dir or dir == '.' 1535 File.join(dir, base) 1536 end 1537 1538 if File::ALT_SEPARATOR 1539 DIRECTORY_TERM = "(?=[/#{Regexp.quote(File::ALT_SEPARATOR)}]|\\z)" 1540 else 1541 DIRECTORY_TERM = "(?=/|\\z)" 1542 end 1543 SYSCASE = File::FNM_SYSCASE.nonzero? ? "-i" : "" 1544 1545 def descendant_directory?(descendant, ascendant) 1546 /\A(?#{SYSCASE}:#{Regexp.quote(ascendant)})#{DIRECTORY_TERM}/ =~ File.dirname(descendant) 1547 end 1548 end # class Entry_ 1549 1550 def fu_list(arg) #:nodoc: 1551 [arg].flatten.map {|path| File.path(path) } 1552 end 1553 private_module_function :fu_list 1554 1555 def fu_each_src_dest(src, dest) #:nodoc: 1556 fu_each_src_dest0(src, dest) do |s, d| 1557 raise ArgumentError, "same file: #{s} and #{d}" if fu_same?(s, d) 1558 yield s, d 1559 end 1560 end 1561 private_module_function :fu_each_src_dest 1562 1563 def fu_each_src_dest0(src, dest) #:nodoc: 1564 if tmp = Array.try_convert(src) 1565 tmp.each do |s| 1566 s = File.path(s) 1567 yield s, File.join(dest, File.basename(s)) 1568 end 1569 else 1570 src = File.path(src) 1571 if File.directory?(dest) 1572 yield src, File.join(dest, File.basename(src)) 1573 else 1574 yield src, File.path(dest) 1575 end 1576 end 1577 end 1578 private_module_function :fu_each_src_dest0 1579 1580 def fu_same?(a, b) #:nodoc: 1581 File.identical?(a, b) 1582 end 1583 private_module_function :fu_same? 1584 1585 @fileutils_output = $stderr 1586 @fileutils_label = '' 1587 1588 def fu_output_message(msg) #:nodoc: 1589 @fileutils_output ||= $stderr 1590 @fileutils_label ||= '' 1591 @fileutils_output.puts @fileutils_label + msg 1592 end 1593 private_module_function :fu_output_message 1594 1595 # This hash table holds command options. 1596 OPT_TABLE = {} #:nodoc: internal use only 1597 (private_instance_methods & methods(false)).inject(OPT_TABLE) {|tbl, name| 1598 (tbl[name.to_s] = instance_method(name).parameters).map! {|t, n| n if t == :key}.compact! 1599 tbl 1600 } 1601 1602 # 1603 # Returns an Array of method names which have any options. 1604 # 1605 # p FileUtils.commands #=> ["chmod", "cp", "cp_r", "install", ...] 1606 # 1607 def self.commands 1608 OPT_TABLE.keys 1609 end 1610 1611 # 1612 # Returns an Array of option names. 1613 # 1614 # p FileUtils.options #=> ["noop", "force", "verbose", "preserve", "mode"] 1615 # 1616 def self.options 1617 OPT_TABLE.values.flatten.uniq.map {|sym| sym.to_s } 1618 end 1619 1620 # 1621 # Returns true if the method +mid+ have an option +opt+. 1622 # 1623 # p FileUtils.have_option?(:cp, :noop) #=> true 1624 # p FileUtils.have_option?(:rm, :force) #=> true 1625 # p FileUtils.have_option?(:rm, :preserve) #=> false 1626 # 1627 def self.have_option?(mid, opt) 1628 li = OPT_TABLE[mid.to_s] or raise ArgumentError, "no such method: #{mid}" 1629 li.include?(opt) 1630 end 1631 1632 # 1633 # Returns an Array of option names of the method +mid+. 1634 # 1635 # p FileUtils.options_of(:rm) #=> ["noop", "verbose", "force"] 1636 # 1637 def self.options_of(mid) 1638 OPT_TABLE[mid.to_s].map {|sym| sym.to_s } 1639 end 1640 1641 # 1642 # Returns an Array of method names which have the option +opt+. 1643 # 1644 # p FileUtils.collect_method(:preserve) #=> ["cp", "cp_r", "copy", "install"] 1645 # 1646 def self.collect_method(opt) 1647 OPT_TABLE.keys.select {|m| OPT_TABLE[m].include?(opt) } 1648 end 1649 1650 LOW_METHODS = singleton_methods(false) - collect_method(:noop).map(&:intern) 1651 module LowMethods 1652 private 1653 def _do_nothing(*)end 1654 ::FileUtils::LOW_METHODS.map {|name| alias_method name, :_do_nothing} 1655 end 1656 1657 METHODS = singleton_methods() - [:private_module_function, 1658 :commands, :options, :have_option?, :options_of, :collect_method] 1659 1660 # 1661 # This module has all methods of FileUtils module, but it outputs messages 1662 # before acting. This equates to passing the <tt>:verbose</tt> flag to 1663 # methods in FileUtils. 1664 # 1665 module Verbose 1666 include FileUtils 1667 @fileutils_output = $stderr 1668 @fileutils_label = '' 1669 names = ::FileUtils.collect_method(:verbose) 1670 names.each do |name| 1671 module_eval(<<-EOS, __FILE__, __LINE__ + 1) 1672 def #{name}(*args, **options) 1673 super(*args, **options, verbose: true) 1674 end 1675 EOS 1676 end 1677 private(*names) 1678 extend self 1679 class << self 1680 public(*::FileUtils::METHODS) 1681 end 1682 end 1683 1684 # 1685 # This module has all methods of FileUtils module, but never changes 1686 # files/directories. This equates to passing the <tt>:noop</tt> flag 1687 # to methods in FileUtils. 1688 # 1689 module NoWrite 1690 include FileUtils 1691 include LowMethods 1692 @fileutils_output = $stderr 1693 @fileutils_label = '' 1694 names = ::FileUtils.collect_method(:noop) 1695 names.each do |name| 1696 module_eval(<<-EOS, __FILE__, __LINE__ + 1) 1697 def #{name}(*args, **options) 1698 super(*args, **options, noop: true) 1699 end 1700 EOS 1701 end 1702 private(*names) 1703 extend self 1704 class << self 1705 public(*::FileUtils::METHODS) 1706 end 1707 end 1708 1709 # 1710 # This module has all methods of FileUtils module, but never changes 1711 # files/directories, with printing message before acting. 1712 # This equates to passing the <tt>:noop</tt> and <tt>:verbose</tt> flag 1713 # to methods in FileUtils. 1714 # 1715 module DryRun 1716 include FileUtils 1717 include LowMethods 1718 @fileutils_output = $stderr 1719 @fileutils_label = '' 1720 names = ::FileUtils.collect_method(:noop) 1721 names.each do |name| 1722 module_eval(<<-EOS, __FILE__, __LINE__ + 1) 1723 def #{name}(*args, **options) 1724 super(*args, **options, noop: true, verbose: true) 1725 end 1726 EOS 1727 end 1728 private(*names) 1729 extend self 1730 class << self 1731 public(*::FileUtils::METHODS) 1732 end 1733 end 1734 1735end 1736