#! /usr/bin/env ruby

#:title: RecursiveUtils
#:main:  RecursiveUtils
#
#= recursiveutils.rb -- Recursive Utils
#Authors::   Michimasa Koichi <michi[at]ep.sci.hokudai.ac.jp>
#Copyright:: Copyright (C) Michimasa Koichi 2006-. All rights reserved.
#Document::  http://www.ep.sci.hokudai.ac.jp/~michi/pc/ruby/doc/recursiveutils/
#Download::  http://www.ep.sci.hokudai.ac.jp/~michi/pc/ruby/src/recursiveutils/
#
#== Summary
#再帰処理機能を実装したツール群.
#
#== Methods
#=== Module Methods
#ファイル操作:: chmod, chown
#
#== Reference Tools
#rename.rb:: http://www.ep.sci.hokudai.ac.jp/~daktu32/DOC/ruby/works/ruby_works.htm
#rsed2.rb::  http://www.ep.sci.hokudai.ac.jp/~morikawa/ruby/rsed2/SIGEN_PUB.htm
#
#== Required Library
#===RecursiveUtils
#* localefilter ( >= Ver. 2.4.1 )
#  URL:: http://www.ep.sci.hokudai.ac.jp/~michi/pc/ruby/doc/localefilter/
#
#===RecursiveUtils::FullPathList
#* find (Standard Library)
#
#===RecursiveUtils::FrontEnd
#* optparse (Standard Library)
#
#== Acknowlege
#* 塚原氏 (rename.rb の作者)
#  URL:: http://www.ep.sci.hokudai.ac.jp/~daktu32/
#
#* 森川氏 (rsed2.rb の作者)
#  URL:: http://www.ep.sci.hokudai.ac.jp/~morikawa/
#
#==Future Plans
#* 再帰処理できると便利なメソッドを思いついたら追加する.
#
#== Recent History
#* Ver. 3.2.0, 2007/10/05 by michi
#  FrontEnd#rename:: ヘルプを改訂
#  FrontEnd#sed::    ヘルプを改訂
#  FrontEnd#chmod::  実装
#  FrontEnd#chown::  実装
#
#* Ver. 3.1.0, 2007/05/27 by michi
#  Rename::          force, quiet モードの処理を調整, 他
#  Sed::             @protect を実装, 他
#  FrontEnd#rename:: [-h] の改訂, [-q] の実装
#  FrontEnd#sed::    [-h] の改訂, [-p] の実装
#
#* Ver. 3.0.1, 2007/05/23 by michi
#  FullPathList::    コードの最適化
#  Rename::          コードの最適化
#  FrontEnd#rename:: ヘルプの改訂
#  FrontEnd#sed::    ヘルプの改訂
#
#* Ver. 3.0.0, 2007/05/09 by michi
#  FrontEnd::        新規作成
#  FrontEnd#rename:: rename.rb から移植
#  FrontEnd#sed::    rsed.rb から移植
#
#--
#* Ver. 2.3.2, 2006/11/21 by michi
#  Sed:: バグ修正
#
#* Ver. 2.3.1, 2006/11/18 by michi
#  FullPathList:: バグ修正
#  Sed::          バグ修正
#
#* Ver. 2.3.0, 2006/11/13 by michi
#  RecursiveUtils:: 各クラスの initialize にオプションを渡せるよう改訂
#  FullPathList::   オプション, メソッドを追加
#
#* Ver. 2.2.0, 2006/11/12 by michi
#  chmod:: 新規作成
#  chown:: 新規作成
#
#* Ver. 2.1.0, 2006/11/07 by michi
#  RecursiveUtils:: FullPathList, Rename, Sed をモジュール化
#  FullPathList::   Rename, Sed のスーパークラスとして実装
#++
#
module RecursiveUtils
  require 'localefilter'

  # RecursiveUtils のバージョン
  VERSION = '3.2.0'

  # RecursiveUtils の最終更新日
  UPDATE  = '2007/10/05'

  # RecursiveUtils の公開アドレス
  URL     = 'http://www.ep.sci.hokudai.ac.jp/~michi/pc/ruby/'

  #
  #== Summary
  #重複無しのフルパスのリストを作成する.
  #
  #== Methods
  #===Public Methods
  #初期化:: new
  #リスト取得:: get, get_exist, get_dir, get_file
  #
  #===Private Methods
  #その他:: option_filter
  #
  #==Future Plans
  #* 配列の演算子メソッドを実装してみる？
  #
  class FullPathList
    require 'find'

    #
    #新規オブジェクトを作成する. 各オプションは +Attributes+ を参照する事.
    #
    def initialize(options = {}, *path)
      @lf = LocaleFilter.new
      @recursive = options[:recursive]
      @dir       = options[:dir]
      @owner     = options[:owner]
      @readable  = options[:readable]
      @writable  = options[:writable]
      @warn      = options[:warn]
      @debug     = options[:debug]
      @pathlist  = get(path)  unless path.to_s.empty?
    end

    # 再帰処理を行う. デフォルトは *false*.
    attr_accessor :recursive

    # 処理対象にディレクトリを追加する. デフォルトは *false*.
    attr_accessor :dir

    # 所有者のチェックを行う. デフォルトは *false*.
    attr_accessor :owner

    # 読み込み権のチェックを行う. デフォルトは *false*.
    attr_accessor :readable

    # 書き込み権のチェックを行う. デフォルトは *false*.
    attr_accessor :writable

    # パスが存在しなかった場合に警告を出力する. デフォルトは *false*.
    attr_accessor :warn

    # 開発者用のメッセージを出力する. デフォルトは *false*.
    attr_accessor :debug

    # new に _path_ を与えた際に作成されるリスト.
    attr_reader :pathlist

    #
    #配列 _list_ をオプションでフィルタリングして返す.
    #
    def option_filter(list, dir = false)
      return false  if list.to_s.empty?

      unless dir || @dir
        list = list.partition { |i| File.directory?(i) }
        list[0].each { |i| @lf.warn "\"#{i}\" is directory." }  if @warn
        list = list[1]
      end

      if @owner
        list = list.partition { |i| File.owned?(i) }
        list[1].each { |i| @lf.warn "#{i}: Not owner." }  if @warn
        list = list[0]
      end

      if @readable
        list = list.partition { |i| File.readable?(i) }
        list[1].each { |i| @lf.warn "#{i}: Read permission denied." }  if @warn
        list = list[0]
      end

      if @writable
        list = list.partition { |i| File.writable?(i) }
        list[1].each { |i| @lf.warn "#{i}: Write permission denied." }  if @warn
        list = list[0]
      end

      return list
    end
    private :option_filter

    #
    #_path_ を元に存在するファイルのリストを配列で返す.
    #
    def get_exist(*path)
      return false  if path.to_s.empty?
      list = Array.new

      path.flatten.uniq.each { |i|
        unless File.exist?(i)
          @lf.warn "#{i}: No such file or direcotry."  if @warn
          next
        end

        fullpath = File.expand_path(i)
        unless list.include?(fullpath)
          list << fullpath

          if @recursive && File.directory?(fullpath)
            Find.find(fullpath) { |j| list << j }
          end
        end
      }
      list.uniq
    end

    #
    #_path_ を元にオプションに対応するリストを配列で返す.
    #
    def get(*path)
      option_filter(get_exist(path))
    end

    #
    #_path_ を元にディレクトリのリストを配列で返す.
    #
    def get_dir(*path)
      return  unless list = get_exist(path)
      list = list.partition { |i| File.directory?(i) }
      list[1].each { |i| @lf.warn "#{i}: Not directory." }  if @warn
      option_filter(list[0], true)
    end

    #
    #_path_ を元に通常ファイルのリストを配列で返す.
    #
    def get_file(*path)
      return  unless list = get_exist(path)
      list = list.partition { |i| File.file?(i) }
      list[1].each { |i| @lf.warn "#{i}: Not regular file." }  if @warn
      option_filter(list[0], true)
    end
  end

  #
  #== Summary
  #再帰処理機能, 文字コード変換機能を実装した Ruby 版 rename.
  #
  #== Example
  #  require 'recursiveutils'
  #
  #  obj = RecursiveUtils::Rename.new
  #  obj.rename('f..', 'bar', 'foo.txt')
  #  # => 対象ファイルの /f../ を "bar" に変換する
  #
  #  obj.recursive = true
  #  obj.upcase('/usr/local/src/')
  #  # => 対象ディレクトリ以下のファイルを全て大文字に変換
  #
  #  obj.recursive = false
  #  obj.extention = true
  #  obj.downcase
  #  # => カレントディレクト中のファイルの拡張子を小文字に変換
  #
  #  obj.extention = false
  #  obj.convert('euc', Dir.glob('./*.txt'))
  #  # => 対象ファイルの文字コードを euc に変換
  #
  #  obj.exec_rename(Dir.glob('./*.txt')) { |i| i.swapcase.gsub('X', '_') }
  #  # => 対象ファイルの大文字と小文字を変換後, "X" を "_" に置換
  #
  #== Methods
  #=== Public Methods
  #初期化:: new
  #ファイル操作:: rename, upcase, downcase, convert, exec_rename
  #
  #=== Private Methods
  #その他:: sort_list, confirm
  #
  #== Reference Tool
  #rename.rb:: http://www.ep.sci.hokudai.ac.jp/~daktu32/DOC/ruby/works/ruby_works.htm
  #
  #==Future Plans
  #* 汎用性の高いファイル名変換メソッドを思いついたら追加する.
  #
  class Rename < FullPathList
    #
    #新規オブジェクトを作成する.
    #オプションの意味は FullPathList と Rename の +Attributes+ を参照する事.
    #
    def initialize(options = {})
      @lf = LocaleFilter.new
      @recursive = options[:recursive]
      @dir       = options[:dir]
      @owner     = options[:owner]
      @readable  = options[:readable]
      @writable  = options[:writable]
      @warn      = options[:warn]
      @debug     = options[:debug]
      @noop      = options[:noop]
      @force     = options[:force]
      @extention = options[:extention]
      @quiet     = options[:quiet]
    end

    # 使用しないオプション
    undef   :pathlist

    # 使用しないメソッド
    undef   :get_dir, :get_file
    private :get, :get_exist

    # 変換処理を行わない. デフォルトは *false*.
    attr_accessor :noop

    # 確認無しで処理を実行する. デフォルトは *false*.
    attr_accessor :force

    # 処理対象を拡張子のみに限定する. デフォルトは *false*.
    attr_accessor :extention

    # メッセージを抑制する. デフォルトは *false*.
    attr_accessor :quiet

    #
    #Hash _list_ を文字長の長い順にソートした配列で返す.
    #
    def sort_list(list)
      list.to_a.sort { |a, b|
        (b[0].length <=> a[0].length) * 2 + (a[1] <=> b[1])
      }
    end
    private :sort_list

    #
    #標準入力から *Yes* or *No* の回答を得る.
    #
    def confirm(message)
      STDERR.print "#{message} [y/n] : "
      loop{
        case STDIN.gets.chomp
        when /^y(es)??\b/i
          return true

        when /^n(o)??\b/i
          STDERR.puts 'quit.'
          return false

        else
          STDERR.print '  Please answer yes or no. [y/n] : '
        end
      }
    end
    private :confirm

    #
    #配列 _list_ を元に作成した処理対象を _block_ で変換する.
    #
    def exec_rename(list, message = nil, &block)
      list = Dir.entries(Dir.pwd)  if list.to_s.empty?
      pathlist = get(list)
      raise ArgumentError,  'Error: No entry.'        unless pathlist
      raise LocalJumpError, 'Error: No block given.'  unless block
      src_list = Hash.new { |h, k| h[k] = Array.new }
      new_list = src_list.dup
      new_num  = 0

      pathlist.each { |path|
        dir, before = File.split(path)
        if @extention
          target = File.extname(before)
          after  = File.basename(before, ".*") << block.call(target)

        else
          after = block.call(before)
        end

        unless before == after
          src_list[dir] << before
          new_list[dir] << after
          new_num += 1
        end
      }

      if new_list.empty?
        STDERR.puts 'No such files which will be converted to that.'
        return false
      end
      src_list = sort_list(src_list)
      new_list = sort_list(new_list)

      unless @quiet
        src_list.each_index { |m|
          next  unless new_list[m]
          @lf.puts "\n#{src_list[m][0]}:"

          max = src_list[m][1].map{ |i| i.size }.max
          src_list[m][1].each_index { |n|
            print '    ', @lf.kconv(src_list[m][1][n]).to_s.ljust(max)
            if message
              puts message

            else
              @lf.puts "  =>  #{new_list[m][1][n]}"
            end
          }
        }
        print "\n"
      end
      return true  if @noop

      unless @force
        STDERR.print "#{new_num} files will be renamed. "  if @debug
        return false  unless confirm('Do you sure rename?')
      end

      src_list.each_index { |m|
        next  unless new_list[m]
        src_list[m][1].each_index { |n|
          src_path = File.join(src_list[m][0], src_list[m][1][n])
          new_path = File.join(src_list[m][0], new_list[m][1][n])

          if ! @force && File.exist?(new_path)
            STDERR.print %Q("#{new_path}" is already exist. )
            next  unless confirm('Overwrite now?')
          end

          File.rename(src_path, new_path)  rescue STDERR.puts $!
        }
      }
      print "\n", "done.\n"  unless @quiet
    end

    #
    #_path_ 中の正規表現 _from_ に一致するファイルを _to_ に変換する.
    #
    def rename(from, to, *path)
      from = /#{from}/
      exec_rename(path) { |i| i.gsub(from, to) }
    end

    #
    #_path_ 中のファイルを大文字に変換する.
    #
    def upcase(*path)
      exec_rename(path) { |i| i.upcase }
    end

    #
    #_path_ 中のファイルを小文字に変換する.
    #
    def downcase(*path)
      exec_rename(path) { |i| i.downcase }
    end

    #
    #_path_ 中のファイルを文字コード _code_ に変換する.
    #
    def convert(code = @lf.locale, *path)
      to = @lf.txt2num(code)

      case to
      when Kconv::ASCII, Kconv::BINARY, Kconv::UNKNOWN
        raise ArgumentError, "#{code}: Invalide character code."

      else
        puts %Q(Convert file name encode to "#{@lf.num2txt(to)}".)  unless @quiet
      end
      message = %Q[  (Convert to "#{@lf.num2txt(to)}")]

      exec_rename(path, message) { |i| Kconv.kconv(i, to) }
    end
  end

  #
  #== Summary
  #再帰処理機能を実装した Ruby 版 sed.
  #
  #== Example
  #  require 'recursiveutils'
  #
  #  obj = RecursiveUtils::Sed.new
  #  obj.sed('f..', 'bar', 'foo.txt')
  #  # => 対象ファイル中の文字列 /f../ を "bar" に置換する
  #
  #  obj.recursive = true
  #  obj.sed('text', 'テキスト', '/usr/local/src/')
  #  # => 対象ディレクトリ以下のファイルを全てに対して実行する
  #
  #  obj.recursive = false
  #  obj.code = 'euc'
  #  obj.sed('ほげ', 'hoge', 'hoge.txt')
  #  # => 出力文字コードを "EUC" に設定して実行する
  #
  #== Methods
  #=== Public Methods
  #初期化:: new
  #ファイル操作:: sed
  #設定:: size=
  #
  #=== Private Methods
  #ファイル操作:: exec_sed, change_file_stat
  #その他:: set_code
  #
  #== Reference Tool
  #rsed2.rb::  http://www.ep.sci.hokudai.ac.jp/~morikawa/ruby/rsed2/SIGEN_PUB.htm
  #
  #==Future Plans
  #* バックアップファイルの拡張子を指定できるようにする.
  #
  #* 複雑な置換処理も実行できるようにする.
  #  * Rename#exec_rename みたいな形で実装するのが目標.
  #
  class Sed < FullPathList
    include LocaleFilter::CodeConverter

    # 文字コード判定用の読み込みサイズの初期値 (byte).
    DEF_SIZE = 1024

    # 文字コード判定用の読み込みサイズの上限値 (byte).
    MAX_SIZE = 1024 * 1024

    # 凡例
    SEP_EXPLAIN = ' | --- : before, +++ : after |'

    # path 間のセパレータ
    SEP_PATH    = ' +' << ( '-' * ( SEP_EXPLAIN.length - 3 ) ) << '+'

    # list 間のセパレータ
    SEP_LIST    = SEP_PATH.gsub('-', '=')

    #
    #新規オブジェクトを作成する.
    #オプションの意味は FullPathList と Sed の +Attributes+ を参照する事.
    #
    def initialize(options = {})
      @lf = LocaleFilter.new
      @recursive = options[:recursive]
      @dir       = false
      @owner     = options[:owner]
      @readable  = true
      @writable  = true
      @warn      = options[:warn]
      @debug     = options[:debug]
      @backup    = options[:backup]
      @code      = options[:code]
      @force     = options[:force]
      @noop      = options[:noop]
      @protect   = options[:protect]
      @verbose   = options[:verbose]
      if options[:size]
        self.size=(options[:size])

      else
        @size    = DEF_SIZE
      end
    end

    # 使用しないオプション
    undef :pathlist, :dir, :dir=, :readable, :readable=, :writable, :writable=

    # 使用しないメソッド
    undef   :get, :get_dir, :get_env
    private :get_exist, :get_file, :txt2num, :num2txt

    #
    #元ファイルを <b>*.bk</b> という名前で保存し, 元ファイルを上書きする.
    #デフォルトは *false*.
    #
    attr_accessor :backup

    #
    #文字コード名. 設定されていると文字コード判定結果を上書きする.
    #デフォルトは *nil*.
    #
    attr_accessor :code

    # ファイルを上書きする (<b>@backup</b> より優先される). デフォルトは *false*.
    attr_accessor :force

    # 変換処理を行わない. デフォルトは *false*.
    attr_accessor :noop

    #
    #一時ファイルやバックアップファイルを上書きせずに別名で出力する.
    #デフォルトは *false*.
    #
    attr_accessor :protect

    # 冗長なメッセージを表示する. デフォルトは *false*.
    attr_accessor :verbose

    #
    #文字コード判定用の読み込みサイズ(byte).
    #デフォルトは *DEF_SIZE*. 上限は *MAX_SIZE*.
    #
    attr_reader :size

    #
    #<b>@size</b> の設定用メソッド.
    #引数 _size_ に 1 以下か *MAX_SIZE* 以上を与えると例外 *ArgumentError* を返す.
    #
    def size=(size)
      size = size.to_i
      if size < 1
        raise ArgumentError, 'Invalid size.'

      elsif size > MAX_SIZE
        raise ArgumentError, "Too big (Max: #{MAX_SIZE} byte)."

      else
        @size = size
        puts %Q(set guess size: "#{@size}")  if @verbose
      end
    end

    #
    #_path_ (<b>@io</b>) の文字コードを判定し, 出力文字コードを決定する.
    #
    def set_code(path, code = nil)
      begin
        guess_code = Kconv.guess(@io.read(@size).to_s)
        if guess_code == Kconv::BINARY
          @lf.warn "Warning: #{path}: Binary files are not supported."  if @warn
          return false

        elsif @code
          return txt2num(@code)

        elsif guess_code == Kconv::ASCII && code
          return txt2num(code)

        else
          return guess_code
        end

      rescue
        STDERR.puts $!
        return false

      ensure
        @io.rewind
      end
    end
    private :set_code

    #
    #ファイル _to_ の mode, owner を _from_ の値に変更する.
    #<b>@backup</b> か <b>@force</b> が *true* の場合はファイル名の変更も行う.
    #
    def change_file_stat(from, to)
      stat = File::Stat.new(from)
      File.chmod(stat.mode, to)
      begin
        File.chown(stat.uid, stat.gid, to)

      rescue
        if @warn
          if @backup || @force
            file = from

          else
            file = to
          end
          @lf.warn %Q( + Warning: Owner and group of "#{file}" is not preserved.)
        end
      end

      if @force
        File.rename(to, from)
        @lf.puts ' |', " |rename: #{to} => #{File.basename(from)}"  if @verbose

      elsif @backup
        ext = '.bk'
        if @protect && File.exist?(from + ext)
          @lf.warn " + Warning: #{from}#{ext}: Already exists."  if @warn
          num = 1
          while File.exist?(from + '_' + num.to_s + ext)
            @lf.warn " + Warning: #{from}_#{num}#{ext}: Already exists."  if @warn
            num += 1
          end
          bk_name = '_' << num.to_s << ext

        else
          bk_name = ext
        end

        File.rename(from, from + bk_name)
        File.rename(to, from)
        @lf.puts ' |', " |backup: #{from} => #{File.basename(from)}#{bk_name}"  if @verbose
      end
    end
    private :change_file_stat

    #
    #_path_ (<b>@io</b>) 中の _from_ を _to_ で置換する.
    #<b>@force</b>, <b>@backup</b>, <b>@noop</b> の値が全て *false* の場合は
    #元の名前にアンダースコアを付加したバックアップファイルを作成する.
    #
    def exec_sed(from, to, path, code)
      return false  unless code
      code_name = num2txt(code)
      puts "code : #{code_name}"  if @verbose

      unless @io.any? { |line| Kconv.toeuc(line) =~ from }
        @lf.warn "#{path}: Nothing to match it."  if @verbose
        return false
      end
      @io.rewind
      @lf.puts "\n#{path}: #{code_name}", SEP_LIST

      line_length = @io.read.scan(/$/).size.to_s.length
      @io.rewind

      unless @noop
        num = ''
        begin
          output_path = path + '_'
          if @protect
            while File.exist?(output_path + num.to_s)
              @lf.warn " + Warning: #{output_path}#{num}: Already exists."  if @warn
              num = num.to_i + 1
            end
            open_flag = File::WRONLY | File::CREAT | File::EXCL

          else
            open_flag = 'w'
          end
          output_io = open(output_path << num.to_s, open_flag)

        rescue Errno::EEXIST
          @lf.warn " + Warning: #{output_path}: Already exists."  if @warn
          num = num.to_i + 1
          retry

        rescue
          @lf.warn " + Warning: #{output_path}: Permission denied."  if @warn
          num = num.to_i + 1
          retry
        end
        output_lf = LocaleFilter.new(code_name, output_io)
        @lf.puts " |open: #{output_path}"  if @debug
      end

      @io.each { |line|
        line = Kconv.toeuc(line)
        n = sprintf("%*d", line_length, @io.lineno)
        if line =~ from
          @lf.puts " |#{n}: --- #{line}"
          @lf.puts " |#{n}: +++ #{line.gsub!(from, to)}"
          puts " *#{n}:#{code_name}"  if @debug

        elsif @debug
          puts " |#{n}:#{code_name}"
        end
        output_lf.print line  unless @noop
      }
      @io.close

      unless @noop
        output_io.close
        @lf.puts " |close: #{output_path}"  if @debug
        change_file_stat(path, output_path)
      end
      puts SEP_PATH
    end
    private :exec_sed

    #
    #_path_ 中の正規表現 _from_ に一致する文字列を _to_ に置換する.
    #個別ファイルの置換処理は exec_sed で行う.
    #
    def sed(from, to, *path)
      path     = Dir.entries(Dir.pwd)  if path.to_s.empty?
      pathlist = get_file(path)
      raise ArgumentError,  'Error: No entry.'  unless pathlist

      guess_code = Kconv.guess(to.to_s)
      case guess_code
      when Kconv::ASCII, Kconv::BINARY, Kconv::UNKNOWN
        to_code = nil
      else
        to_code = guess_code
      end
      from = /#{Kconv.toeuc(from.to_s)}/e
      to   = Kconv.toeuc(to.to_s)

      puts %Q(set locale : "#{@lf.locale}")  if @verbose
      puts SEP_PATH, SEP_EXPLAIN, SEP_PATH

      pathlist.each { |path|
        @io = open(path)
        begin
          exec_sed(from, to, path, set_code(path, to_code))

        rescue
          STDERR.puts $!
          false

        ensure
          @io.close  unless @io.closed?
        end
      }
    end
  end

  #
  #再帰処理機能を付加した File#chmod.
  #オプションの種類が増えている事を除けば FileUtils#chmod_R とほぼ同じ.
  #
  #=== Options
  #FullPathList のオプションも参照する事.
  #verbose:: 処理を行う前に内容を表示する.
  #noop::    実際の処理は行わない.
  #
  #===Future Plans
  #* 8進数を使った指定もできると便利かな？
  #
  def chmod(mode, list, options = {})
    raise ArgumentError, 'Error: No entry.'  if list.to_s.empty?
    options[:recursive] = true
    options[:warn]      = false
    begin
      pl = FullPathList.new(options, list)
    rescue
      STDERR.puts $!
      return false
    end
    return  unless pl.pathlist
    lf = LocaleFilter.new
    message = %Q(: Change to "#{mode}".)

    pl.pathlist.each { |path|
      lf.print path, message, "\n"    if options[:verbose]
      puts File::Stat.new(path).mode  if options[:debug]

      unless options[:noop]
        File.chmod(mode, path)  rescue STDERR.puts $!
      end
    }
  end
  module_function :chmod

  #
  #再帰処理機能を付加した File#chown.
  #オプションの種類が増えている事を除けば FileUtils#chown_R とほぼ同じ.
  #
  #=== Options
  #FullPathList のオプションも参照する事.
  #verbose:: 処理を行う前に内容を表示する.
  #noop::    実際の処理は行わない.
  #
  #===Future Plans
  #* ユーザ名を使った指定も可能にする？
  #  * Etc#getpwnam を使えばできそう
  #
  def chown(owner, group, list, options = {})
    raise ArgumentError, 'Error: No entry.'  if list.to_s.empty?
    options[:recursive] = true
    options[:warn]      = false
    begin
      pl = FullPathList.new(options, list)
    rescue
      STDERR.puts $!
      return false
    end
    return  unless pl.pathlist
    lf = LocaleFilter.new
    message = %Q(: Change to owner: "#{owner}", group: "#{group}".)

    pl.pathlist.each { |path|
      lf.print path, message, "\n"  if options[:verbose]

      unless options[:noop]
        File.chown(owner, group, path)  rescue STDERR.puts $!
      end
    }
  end
  module_function :chown

  #
  #== Summary
  #RecursiveUtils::Rename, RecursiveUtils::Sed,
  #RecursiveUtils#chmod, RecursiveUtils#chown のフロントエンド
  #
  #== How to Use
  #  1) recursiveutils.rb を実効権限を付加して
  #     Ruby のライブラリパスに置く.
  #
  #  2) パスの通った場所に rename.rb, rsed.rb, chmod.rb, chown.rb
  #     という名前でシンボリックリンクを作成する.
  #
  #  3) シンボリックリンクを実行すると名前に対応したメソッドが
  #     実行される.
  #
  #== Methods
  #===Public Methods
  #フロントエンド:: rename, sed
  #
  #===Private Methods
  #共通:: check_ver, usage, help, version, error, check_code
  #
  #===Future Plans
  #* locale を見てヘルプの言語を切り替える
  #
  module FrontEnd
    require 'optparse'

    #
    #ライブラリのバージョンチェック用メソッド.
    #*now* よりも *req* が低いと *false* を返す.
    #
    def check_ver(now, req)
      a_now = now.split(/[\.| ]/).map { |i| i.to_i }
      a_req = req.split(/[\.| ]/).map { |i| i.to_i }

      return true   if a_now[0] > a_req[0]
      return false  if a_now[0] < a_req[0]

      return true   if a_now[1] > a_req[1]
      return false  if a_now[1] < a_req[1]

      return true   if a_now[2] > a_req[2]
      return true   if ! a_now[3] && a_now[2] == a_req[2]

      return false
    end
    private :check_ver

    #
    #USAGE 表示用メソッド.
    #
    def usage(option)
      @stdout.puts "  USAGE:#{@usage}\n\n  #{option}"
      exit true
    end
    private :usage

    #
    #ヘルプ表示用メソッド.
    #
    def help(option)
      message = "  NAME:
      #{@scriptname} - #{@summary}

  SYNOPSIS:#{@usage}

  DESCRIPTION:#{@description}

  #{option}"

      message << "\n  EXAMPLE:#{@example}\n"  if @example

      message << "\n  ENVIRONMENT:#{@env}\n"  if @env

      message << "
  VERSION:
      #{@scriptname} Version #{@version}, Last Update: #{@update}

  LIBRARY VERSION:
      RecursiveUtils Version #{RecursiveUtils::VERSION}, Last Update: #{RecursiveUtils::UPDATE}
      LocaleFilter   Version #{LocaleFilter::VERSION}, Last Update: #{LocaleFilter::UPDATE}

  DEVELOPERS:#{@developers}
        All Rights Reserved.

  URL:
      #{RecursiveUtils::URL}

  HISTORY:#{@history}"

      message << "\n\n  TODO:#{@todo}"  if @todo

      @stdout.puts message
      exit true
    end
    private :help

    #
    #Version 情報表示用メソッド.
    #
    def version
      puts "#{@scriptname} Ver. #{@version}, Last Update: #{@update}"
      exit true
    end
    private :version

    #
    #エラーメッセージ表示用メソッド.
    #
    def error(message)
      STDERR.puts "#{@scriptname}: #{message}", "  #{@usage}",
      %Q(\nType "#{@scriptname} --help" for advanced help.)
      exit false
    end
    private :error

    #
    #文字コードチェック用メソッド.
    #
    def check_code(code)
      num = LocaleFilter::CodeConverter.txt2num(code)
      case num
      when Kconv::ASCII, Kconv::BINARY, Kconv::UNKNOWN
        raise ArgumentError, "#{code}: Invalide character code."

      else
        return LocaleFilter::CodeConverter.num2txt(num)
      end
    end
    private :check_code

    #
    #===Summary
    #RecursiveUtils::Rename のフロントエンド.
    #
    #===Synopsis
    #  rename.rb [OPTIONS] from to file [file...]
    #  rename.rb --code CODE file [file...]
    #
    #===Options
    #  -c, --code CODE    convert character code of filename
    #                     (sjis, euc, jis, utf8, utf16)
    #  -d, --down         convert to small letter
    #  -D, --debug        execute with debug mode (for developer)
    #  -e, --extension    execute only extension
    #  -f, --force        rename without confirmations
    #  -h, --usage        display usage
    #  -H, --help         display detailed help
    #  -n, --no-exec      do not rename, display converted list only
    #  -q, --quiet        execute with quiet mode
    #  -r, --recursive    execute recurrently
    #  -u, --up           convert to capital letter
    #  -v, --version      display version information
    #
    #===Future Plans
    #* 日本語のヘルプを作成する
    #
    def rename(argv = ARGV)
      extend FrontEnd

      @scriptname  = 'rename.rb'

      @version     = '3.3'

      @update      = '2007/10/05'

      @summary     = 'rename multiple files'

      @description = '
      Rename multiple files at a time by regular expression.
      Convert character code of multiple filenames at a time.'

      @usage       = "
      #{@scriptname} [OPTIONS] from to file [file...]
      #{@scriptname} --code CODE file [file...]"

      @developers  = '
      Yasuhiro Morikawa  <morikawa@ep.sci.hokudai.ac.jp>
      Koichi   Michimasa <michi@ep.sci.hokudai.ac.jp>
      Daisuke  Tsukahara <daktu32@ep.sci.hokudai.ac.jp>'

      @history     = '
      2007/10/05 modified by michi,    Ver. 3.3
      2007/05/27 modified by michi,    Ver. 3.2
      2007/05/23 modified by michi,    Ver. 3.1
      2007/05/19 modified by michi,    Ver. 3.0.1
      2007/05/09 modified by michi,    Ver. 3.0
      2007/01/19 modified by michi,    Ver. 2.3.1
      2007/01/08 modified by michi,    Ver. 2.3
      2006/11/17 modified by michi,    Ver. 2.2
      2006/11/13 modified by michi,    Ver. 2.1
      2006/11/04 modified by michi,    Ver. 2.0
      2006/10/30 modified by michi,    Ver. 1.3
      2006/10/25 modified by michi,    Ver. 1.2
      2006/10/24 modified by morikawa, Ver. 1.1
      2006/10/24 modified by michi,    Ver. 1.0
      2003/12/20 modified by daktu32,  Ver. 0.2
      2003/11/18 modified by daktu32,  Ver. 0.1.1
      2003/09/?? created  by daktu32,  Ver. 0.1'

      @todo       = '
      make japanese help'

      lf_ver = '2.4.1'
      unless check_ver(LocaleFilter::VERSION, lf_ver)
        STDERR.puts %Q(error: "#{@scriptname}" required "LocaleFilter" Version #{lf_ver} or later.)
        return false
      end

      op      = OptionParser.new('OPTIONS:', 18, ' ' * 6)
      opts    = Hash.new
      @stdout = STDOUT

      op.on('-c', '--code CODE', String,
            'convert character code of filename', '(sjis, euc, jis, utf8, utf16)'
            ) { |i| opts[:code] = check_code(i) }

      op.on('-d', '--down',
            'convert to small letter'
            ) { opts[:downcase] = true }

      op.on('-D', '--debug',
            'execute with debug mode (for developer)'
            ) { opts[:debug] = true }

      op.on('-e', '--extension',
            'execute only extension'
            ) { opts[:extention] = true }

      op.on('-f', '--force',
            'rename without confirmations'
            ) { opts[:force] = true }

      op.on('-h', '--usage',
            'display usage'
            ) { usage(op.help) }

      op.on('-H', '--help',
            'display detailed help'
            ) { help(op.help) }

      op.on('-n', '--no-exec',
            'do not rename, display converted list only'
            ) { opts[:noop] = true }

      op.on('-q', '--quiet',
            'execute with quiet mode'
            ) { opts[:quiet] = true }

      op.on('-r', '--recursive',
            'execute recurrently'
            ) { opts[:recursive] = true }

      op.on('-u', '--up',
            'convert to capital letter'
            ) { opts[:upcase] = true }

      op.on('-v', '--version',
            'display version information'
            ) { version }

      op.parse!(argv)  rescue error($!)

      if opts[:code] || opts[:upcase] || opts[:downcase]
        error('No argument.')  if argv.empty?

      elsif argv.size < 3
        error('No argument.')
      end

      rf = RecursiveUtils::Rename.new(opts)
      rf.dir = true

      if opts[:debug]
        lf = LocaleFilter.new
        puts "set locale : #{lf.locale}"
      end

      begin
        if opts[:code]
          rf.convert(opts[:code], argv)

        elsif opts[:upcase]
          rf.upcase(argv)

        elsif opts[:downcase]
          rf.downcase(argv)

        else
          rf.rename(argv[0], argv[1], argv[2..-1])
        end

      rescue => message
        error(message)
      end
    end
    module_function :rename

    #
    #===Summary
    #RecursiveUtils::Sed のフロントエンド.
    #
    #===Synopsis
    #   rsed.rb [OPTIONS] from to files [file...]
    #
    #===Options
    #  -b, --backup       元ファイルを *.bk という名前で保存し,
    #                     オリジナルのファイルを上書きする.
    #  -c, --code CODE    ファイルの文字コードを指定する.
    #                     (sjis, euc, jis, utf8, utf16)
    #  -f, --force        オリジナルのファイルを上書きする.
    #  -D, --debug        デバッグ用メッセージを出力する. (開発者用)
    #  -h, --usage        Usage とオプションを出力する.
    #  -H, --help         詳細なヘルプを出力する.
    #  -n, --no-exec      実行結果のみ標準出力に出力し, 実行はしない.
    #  -p, --protect      一時ファイルやバックアップファイルの作成時に
    #                     既存のファイルを上書きしない.
    #  -r, --recursive    再帰的に実行する.
    #  -s, --size BYTES   ファイルの文字コード判定時に読み込む
    #                     ファイルサイズを指定する.
    #                     (default : 1024 byte, max: 1048576 byte)
    #  -v, --version      バージョン情報を出力する.
    #  -V, --verbose      冗長なメッセージを出力する.
    #
    #===Future Plans
    #
    #* バックアップファイルの拡張子を指定できるようにする
    #* 英語のヘルプを作成する
    #
    def sed(argv = ARGV)
      extend FrontEnd

      @scriptname = 'rsed.rb'

      @version     = '5.4'

      @update      = '2007/10/05'

      @summary     = '再帰処理機能付きの Ruby 版 sed'

      @description = %Q(
      #{@scriptname} は 再帰処理機能を実装した Ruby 版 sed です。
      default では、「file」で指定したパスのファイルに対して
      「from」で指定した正規表現パターンを検索し、「to」に
      置換した後、 "file_" のようにオリジナルのファイル名の
      最後にアンダーバーを付加したファイルへ出力します。)

      @usage      = "
      #{@scriptname} [OPTIONS] from to file [file...]"

      @example    = %Q(
      #{@scriptname} -f text2002.html text2004.html ./html/200?.html

      ./html/200?.html に該当するファイル中の "text2002.html" という
      文字列を "text2004.html" に変換します。)

      @developers = '
      Koichi   Michimasa <michi@ep.sci.hokudai.ac.jp>
      Yasuhiro Morikawa  <morikawa@ep.sci.hokudai.ac.jp>
      Daisuke  Tsukahara <daktu32@ep.sci.hokudai.ac.jp>'

      @history    = '
      2007/10/05  道政  Ver. 5.4
      2007/05/27  道政  Ver. 5.3
      2007/05/26  道政  Ver. 5.2
      2007/05/23  道政  Ver. 5.1
      2007/05/09  道政  Ver. 5.0
      2007/01/19  道政  Ver. 4.3
      2006/11/21  道政  Ver. 4.2
      2006/11/17  道政  Ver. 4.1
      2006/11/13  道政  Ver. 4.0
      2006/11/09  塚原  Ver. 3.7
      2006/10/30  道政  Ver. 3.6
      2006/10/20  道政  Ver. 3.5
      2006/10/14  道政  Ver. 3.4
      2006/10/13  道政  Ver. 3.3
      2006/10/02  森川  Ver. 3.2
      2006/10/02  森川  Ver. 3.1
      2006/10/01  道政  Ver. 3.0
      2006/04/15  森川  Ver. 2.5
      2004/12/23  森川  更新
      2004/10/26  森川  更新
      2004/08/06  森川  更新
      2004/02/12  森川  更新
      2004/02/10  森川  更新
      2004/01/11  塚原  作成'

      @todo       = '
      バックアップファイルの拡張子を指定できるようにする。
      英語のヘルプを作成する。'

      lf_ver = '2.4.1'
      unless check_ver(LocaleFilter::VERSION, lf_ver)
        STDERR.puts %Q(error: "#{@scriptname}" required "LocaleFilter" Version #{lf_ver} or later.)
        return false
      end

      # オプション解析 (banner, width, indent)
      op      = OptionParser.new('OPTIONS:', 18, ' ' * 6)
      opts    = Hash.new
      lf      = LocaleFilter.new
      @stdout = lf

      op.on('-b', '--backup',
            '元ファイルを *.bk という名前で保存し、',
            'オリジナルのファイルを上書きする。'
            ) { opts[:backup] = true }

      op.on('-c', '--code CODE', String,
            'ファイルの文字コードを指定する。',
            '(sjis, euc, jis, utf8, utf16)'
            ) { |i| opts[:code] = check_code(i) }

      op.on('-f', '--force',
            'オリジナルのファイルを上書きする。'
            ) { opts[:force] = true }

      op.on('-D', '--debug',
            'デバッグ用メッセージを出力する。 (開発者用)'
            ) { opts[:debug] = true }

      op.on('-h', '--usage',
            'Usage とオプションを出力する。'
            ) { usage(op.help) }

      op.on('-H', '--help',
            '詳細なヘルプを出力する。'
            ) { help(op.help) }

      op.on('-n', '--no-exec',
            '実行結果のみ標準出力に出力し、実行はしない。'
            ) { opts[:noop] = true }

      op.on('-p', '--protect',
            '一時ファイルやバックアップファイルの作成時に',
            '既存のファイルを上書きしない。'
            ) { opts[:protect] = true }

      op.on('-r', '--recursive',
            '再帰的に実行する。'
            ) { opts[:recursive] = true }

      op.on('-s', '--size BYTES', Integer,
            'ファイルの文字コード判定時に読み込む',
            'ファイルサイズを指定する。',
            "(default : #{Sed::DEF_SIZE} byte, max: #{Sed::MAX_SIZE} byte)"
            ) { |i| opts[:size] = i }

      op.on('-v', '--version',
            'バージョン情報を出力する。'
            ) { version }

      op.on('-V', '--verbose',
            '冗長なメッセージを出力する。'
            ) { opts[:verbose] = true, opts[:warn] = true }

      op.parse!(argv)  rescue error($!)
      error('No argument.')  if argv.size < 3

      begin
        rs = Sed.new(opts)
        rs.sed(argv[0], argv[1], argv[2..-1])

      rescue => message
        if message.to_s =~ /Invalid size|Too big/
          message = "#{opts[:size]}: #{message}"
          @usage  = "\n"
          flag    = false
          op.to_a.each { |i|
            case i
            when /^\s*\-s/
              flag = true

            when /^\s*\-v/
              break
            end
            @usage << lf.kconv(i).to_s.chomp << "\n" if flag
          }
        end
        error(message)
      end
    end
    module_function :sed

    #
    #===Summary
    #RecursiveUtils#chmod のフロントエンド.
    #
    #===Synopsis
    #  chmod.rb [OPTIONS] mode file [file...]
    #
    #===Options
    #  -d, --dir          ディレクトリを対象に含める。
    #  -D, --debug        デバッグ用メッセージを出力する。 (開発者用)
    #  -h, --usage        Usage とオプションを出力する。
    #  -H, --help         詳細なヘルプを出力する。
    #  -n, --no-exec      実行結果のみ標準出力に出力し、実行はしない。
    #  -o, --owner        所有権を持たないファイルを除外する。
    #  -r, --readable     読み込み権を持たないファイルを除外する。
    #  -v, --version      バージョン情報を出力する。
    #  -V, --verbose      冗長なメッセージを出力する。
    #
    #===Future Plans
    #* 8進数を使った指定もできるようにする
    #* 英語のヘルプを作成する
    #
    def chmod(argv = ARGV)
      extend FrontEnd

      @scriptname  = 'chmod.rb'

      @version     = '0.1'

      @update      = '2007/10/05'

      @summary     = '再帰処理機能付きの Ruby 版 chmod'

      @description = %Q(
      #{@scriptname} は 再帰処理を追加した Ruby 版 chmod です。
      File#chmod を RecursiveUtils::FullPathList を介して
      再帰的に処理しています。
      FullPathList 独自のオプションが追加されている事を除けば
      FileUtils#chmod_R とほぼ同じです。
      UNIX コマンドの chmod とは mode の指定の仕方が違うので
      注意して下さい。)

      @usage       = "
      #{@scriptname} [OPTIONS] mode file [file...]"

      @developers  = '
      Koichi   Michimasa <michi@ep.sci.hokudai.ac.jp>'

      @history     = '
      2007/10/05 created  by michi, Ver. 0.1'

      @todo       = '
      8進数を使った指定もできるようにする。
      英語のヘルプを作成する。'

      lf_ver = '2.4.1'
      unless check_ver(LocaleFilter::VERSION, lf_ver)
        STDERR.puts %Q(error: "#{@scriptname}" required "LocaleFilter" Version #{lf_ver} or later.)
        return false
      end

      op      = OptionParser.new('OPTIONS:', 18, ' ' * 6)
      opts    = Hash.new
      lf      = LocaleFilter.new
      @stdout = lf

      op.on('-d', '--dir',
          'ディレクトリを対象に含める。'
          ) { opts[:dir] = true }

      op.on('-D', '--debug',
            'デバッグ用メッセージを出力する。 (開発者用)'
            ) { opts[:debug] = true }

      op.on('-h', '--usage',
            'Usage とオプションを出力する。'
            ) { usage(op.help) }

      op.on('-H', '--help',
            '詳細なヘルプを出力する。'
            ) { help(op.help) }

      op.on('-n', '--no-exec',
            '実行結果のみ標準出力に出力し、実行はしない。'
            ) { opts[:noop] = true }

      op.on('-o', '--owner',
          '所有権を持たないファイルを除外する。'
          ) { opts[:owner] = true }

      op.on('-r', '--readable',
          '読み込み権を持たないファイルを除外する。'
          ) { opts[:readable] = true }

      op.on('-v', '--version',
            'バージョン情報を出力する。'
            ) { version }

      op.on('-V', '--verbose',
            '冗長なメッセージを出力する。'
            ) { opts[:verbose] = true, opts[:warn] = true }

      op.parse!(argv)  rescue error($!)

      error('No argument.') if argv.size < 2

      puts "set locale : #{lf.locale}"  if opts[:debug]

      begin
        RecursiveUtils.chmod(argv[0], argv[1..-1], opts)

      rescue => message
        error(message)
      end
    end
    module_function :chmod

    #
    #===Summary
    #RecursiveUtils#chown のフロントエンド.
    #
    #===Synopsis
    #  chown.rb [OPTIONS] mode file [file...]
    #
    #===Options
    #  -d, --dir          ディレクトリを対象に含める。
    #  -D, --debug        デバッグ用メッセージを出力する。 (開発者用)
    #  -h, --usage        Usage とオプションを出力する。
    #  -H, --help         詳細なヘルプを出力する。
    #  -n, --no-exec      実行結果のみ標準出力に出力し、実行はしない。
    #  -o, --owner        所有権を持たないファイルを除外する。
    #  -r, --readable     読み込み権を持たないファイルを除外する。
    #  -v, --version      バージョン情報を出力する。
    #  -V, --verbose      冗長なメッセージを出力する。
    #
    #===Future Plans
    #* 英語のヘルプを作成する
    #
    def chown(argv = ARGV)
      extend FrontEnd

      @scriptname  = 'chown.rb'

      @version     = '0.1'

      @update      = '2007/10/05'

      @summary     = '再帰処理機能付きの Ruby 版 chown'

      @description = %Q(
      #{@scriptname} は 再帰処理を追加した Ruby 版 chown です。
      File#chown を RecursiveUtils::FullPathList を介して
      再帰的に処理しています。
      FullPathList 独自のオプションが追加されている事を除けば
      FileUtils#chown_R とほぼ同じです。
      UNIX コマンドの chown とは owner, group の指定の仕方が
      違うので注意して下さい。)

      @usage       = "
      #{@scriptname} [OPTIONS] owner group file [file...]"

      @developers  = '
      Koichi   Michimasa <michi@ep.sci.hokudai.ac.jp>'

      @history     = '
      2007/10/05 created  by michi, Ver. 0.1'

      @todo       = '
      英語のヘルプを作成する。'

      lf_ver = '2.4.1'
      unless check_ver(LocaleFilter::VERSION, lf_ver)
        STDERR.puts %Q(error: "#{@scriptname}" required "LocaleFilter" Version #{lf_ver} or later.)
        return false
      end

      op      = OptionParser.new('OPTIONS:', 18, ' ' * 6)
      opts    = Hash.new
      lf      = LocaleFilter.new
      @stdout = lf

      op.on('-d', '--dir',
          'ディレクトリを対象に含める。'
          ) { opts[:dir] = true }

      op.on('-D', '--debug',
            'デバッグ用メッセージを出力する。 (開発者用)'
            ) { opts[:debug] = true }

      op.on('-h', '--usage',
            'Usage とオプションを出力する。'
            ) { usage(op.help) }

      op.on('-H', '--help',
            '詳細なヘルプを出力する。'
            ) { help(op.help) }

      op.on('-n', '--no-exec',
            '実行結果のみ標準出力に出力し、実行はしない。'
            ) { opts[:noop] = true }

      op.on('-o', '--owner',
          '所有権を持たないファイルを除外する。'
          ) { opts[:owner] = true }

      op.on('-r', '--readable',
          '読み込み権を持たないファイルを除外する。'
          ) { opts[:readable] = true }

      op.on('-v', '--version',
            'バージョン情報を出力する。'
            ) { version }

      op.on('-V', '--verbose',
            '冗長なメッセージを出力する。'
            ) { opts[:verbose] = true, opts[:warn] = true }

      op.parse!(argv)  rescue error($!)

      error('No argument.') if argv.size < 3

      puts "set locale : #{lf.locale}"  if opts[:debug]

      begin
        RecursiveUtils.chown(argv[0], argv[1], argv[2..-1], opts)

      rescue => message
        error(message)
      end
    end
    module_function :chown
  end
end

# exit  unless $0 == __FILE__
case File.basename($0)
when /rename/
  RecursiveUtils::FrontEnd.rename

when /sed/
  RecursiveUtils::FrontEnd.sed

when /chmod/
  RecursiveUtils::FrontEnd.chmod

when /chown/
  RecursiveUtils::FrontEnd.chown

else
  puts "recursiveutils.rb Ver. #{RecursiveUtils::VERSION}, Last Update: #{RecursiveUtils::UPDATE}"
end
