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