#!/usr/bin/env ruby
# -*- coding: euc-jp -*-
#= lndir.rb -- Create a symlink tree.
#
#== SYNOPSIS
SYNOPSIS = <<-"SYNOP"

      dcreal-lndir.rb は X11 に付随する lndir を Ruby で模写したツール
      です.  X11 版 lndir は引数に与えたディレクトリ以下のツリー構造を
      反映したシンボリックリンク群を再帰的に作成します.

      dcreal-lndir.rb はこの機能に加え, ignore オプションによって特定
      のファイルまたはディレクトリを作成しないことが可能です. 詳細は下
      記の --ignore オプション及び --ignore-regexp オプションを参照く
      ださい.  なお, 常にある特定のファイルを無視するようにしたい場合
      は, 本スクリプトの IGNORE_DEFAULT および IGNORE_REGEXP_DEFAULT 
      変数を編集してください.

      --delete オプションを指定することで, dcreal-lndir.rb によって作
      成したディレクトリツリーの削除を行うことも可能です. ディレクトリ
      とシンボリックリンク以外のファイルを削除しないようになっているた
      め, 誤って通常ファイルを削除する恐れがありません.

SYNOP

#== MAINTAINERS
MAINTAINERS="Yasuhiro MORIKAWA, Daisuke TSUKAHARA"

#== USAGE
USAGE = "
      dcreal-lndir.rb [options] srcdir

        srcdir が相対パスの場合は相対パスでシンボリックリンクを作成します.
        絶対パスの場合には絶対パスでシンボリックリンクを作成します.

        If \"srcdir\" is relative path, relative symbolic links are created.
        Otherwise, absolute symbolic links are created."

#== OPTIONS
#
#please read output of 
#  % lndir.rb --help

#== VERSION
PACKAGE_NAME="dcreal"
URL='http://www.gfd-dennou.org/library/cc-env/dcreal'

CVSTAGNAME='$Name: dcreal-0_3_5-4 $'
CVSDATE='$Date: 2012-11-21 06:23:57 $'
CVSREVISION='$Revision: 1.4 $'

#== LICENCE
#
#This library is distributed under the terms of the Ruby license.
#You can freely distribute/modify this library.
#
#==COPYRIGHT 
#
COPYRIGHT =
  "Copyright (C) 2006 GFD Dennou Club"

#
#== FUTURE PLANS
#
#* force オプションと dry-run オプションは実質競合するので dry-run
#  を優先する.
#

#== HISTORY
#
#These entries is generated by CVS automatically.
#So don't add new information manually.
#(But please adjust old log format to latest log format manually,
#if format gap between them causes).
#
#$Log: dcreal-lndir.rb,v $
#Revision 1.4  2012-11-21 06:23:57  murashin
#Ruby 1.9対応のためのマジックコメントの付加
#
#Revision 1.3  2006-04-11 16:26:18  morikawa
#* オプションの先頭に付加される "=" を除去する処理を追加.
#
#Revision 1.2  2006/04/11 15:47:16  morikawa
#* 設定ファイルの雛形出力と, その設定ファイルを読み込んで動作するよう
#  改良.
#
#Revision 1.1  2006/04/11 14:50:28  morikawa
#* 再帰的にシンボリックリンクを作成する lndir.rb を dcreal のツール群
#  へ吸収.
#
#
#2005-12-28  morikawa
#* IGNORE および IGNORE_REGEXP のデフォルト値を修正.
#
#2005-12-28  morikawa
#* synopsis を修正. ignore-regexp オプションの例を修正.
#
#2005-11-29  morikawa
#* Future Plans を追加.
#
#2005-11-29  morikawa
#* 正規表現での無視の指定を --ignore-regexp で,
#  通常表記での指定を --ignore オプションで
#  行うよう仕様を変更.
#* --delete オプションを追加
#* 上書きオプション --force を追加.
#* 動作チェック用の --dry-run を追加.
#* 詳細表示用の --verbose を追加.
#
#2005-11-28  morikawa
#* optparse を利用するように変更
#
#2004-04-13  daktu32
#* 祖先のディレクトリを指定できないように修正
#
#2004-04-13  daktu32
#* 相対パスで指定すると失敗するバグを修正
#
#2004-04-06  daktu32
#* X 版 lndir を元に作成.
#
############################################################


############################################################
## config part

# default ignored patterns
IGNORE_DEFAULT         = %w(./ ../ . ..)
IGNORE_REGEXP_DEFAULT  = ''

SAMPLE_CONF = "dcreal-lndir.conf"

## config part end
############################################################

## required libraries

require 'pathname'
require "optparse"
require 'date'

## バージョンナンバー整形
UPDATE=CVSDATE.sub(/\s*\$\w+: /, '').sub(/\s*\$$/, '')
FILE_VERSION=CVSREVISION.sub(/\s*\$\w+: /, '').sub(/\s*\$$/, '')
pver_tmp=CVSTAGNAME.sub(/\s*\$\w+: /, '').sub(/\s*\$$/, '').sub(/#{PACKAGE_NAME}\-/, '').gsub(/_/, '.').chomp.strip
PACKAGE_VERSION = pver_tmp == "" ? '(Latest CVS version)' : pver_tmp

## options are parsed

opt = OptionParser.new
OPTS = {}
ARGV.options{|opt|

  opt.summary_width = 23
  opt.summary_indent = ' '*6

  opt.on( '-i=VAL', '--ignore=VAL',
                             "シンボリックリンクを作成しないファイル名
                              を指定します. 複数指定する場合は, それぞれ
                              の間に \",\" を挿入してください.
                              例: src, pub_real といったディレクトリを無視
                                  % lndir.rb --ignore src,pub_real ~/lib\n"
         ){|v| OPTS[:ignore] = v.gsub(/^=/, '').split(",")}

  opt.on( '-I=VAL', '--ignore-regexp=VAL',
                             "シンボリックリンクを作成しない
                              ファイル名を正規表現で指定します.
                              複数のサフィックスを指定する場合は,
                              それぞれの間に \\| を挿入してください.
                              例: 拡張子 rb, txt のファイルを無視
                                  % lndir.rb --ignore-regexp \\.\\(rb\\|txt\\)$ ~/lib\n"
         ){|v| OPTS[:ignore_regexp] = v.gsub(/^=/, '')}

  opt.on( '-d', '--delete',
                             "srcdir を削除する. ただし, lndir.rb 
                              によって作成されたディレクトリツリー,
                              すなわちディレクトリまたはシンボリックリンク
                              のみで構成されるものを削除する. 
                              通常のファイルが存在する場合, エラーを返す.\n"
         ){|v| OPTS[:delete] = v}

  opt.on( '-f', '--force',
                             "既に srcdir が作成されている場合, その
                              ディレクトリツリーを削除した後, 新たに
                              ディレクトリツリーを作成します..\n"
         ){|v| OPTS[:force] = v}

#  opt.on('--relative',
#                           " シンボリックリンクを相対パスで指定する.\n"
#         ){|v| OPTS[:relative] = v}

  opt.on('-v', '--verbose',
                             "作成するディレクトリとシンボリックリンクの
                              詳細を表示する. --delete オプションと同時に
                              指定される場合には, 削除するリストを表示する\n"
         ){|v| OPTS[:verbose] = v}

  opt.on('-n', '--dry-run',
                             "作成または削除するディレクトリとシンボリック
                              リンクのリストが表示されます. 自動的に
                              --verbose オプションが指定されます.\n"
         ){|v| OPTS[:dryrun] = v; OPTS[:verbose] = v}

  opt.on( '-c=VAL', '--config=VAL',
                             "オプション ---ignore および --ignore-regex
                              に与えられる文字列を, 設定ファイルから読み
                              込みます. 設定ファイルの雛形は
                              --output-sample によって出力されます.\n"
         ){|v| OPTS[:config] = v}

  opt.on( '-o', '--output-sample',
                             "オプション --config で読み込むファイルの
                              雛形を出力します. ファイル名は
                              #{SAMPLE_CONF} です.\n"
         ){|v| OPTS[:output_sample] = v}

  opt.on_tail('-h', '-H', '--help', 
                             "詳細なヘルプを出力します.\n"
              ){|v| OPTS[:help] = v}

  opt.parse!
}
n = nil

## help
def help(opt)
  print <<-"EOF"

  #{File.basename($0.to_s)}: #{PACKAGE_NAME} #{PACKAGE_VERSION}

    USAGE: #{USAGE}

    OPTION: \n#{opt.to_a[1..-1].join("")}
    VERSION: 
      Version: #{FILE_VERSION}, Last Update: #{UPDATE}

    #{COPYRIGHT}

  EOF
end
private :help

def help_detail(opt)
  print <<-"EOF"

  #{File.basename($0.to_s)}: #{PACKAGE_NAME} #{PACKAGE_VERSION}

    SYNOPSTIC: #{SYNOPSIS}
    USAGE: #{USAGE}

    OPTION: \n#{opt.to_a[1..-1].join("")}
    VERSION: 
      Version: #{FILE_VERSION}, Last Update: #{UPDATE}

    REFERENCE:
      まつもとゆきひろ,石塚圭樹, 1999, 
      オブジェクト指向スクリプト言語 Ruby , アスキー出版局, 135-136.

    CAUTION:
      REFERENCE の例題をほぼぱくりました(;^^).

    #{COPYRIGHT}

    MAINTAINERS: #{MAINTAINERS}

EOF
end


def sample_conf
  filename = SAMPLE_CONF

  print "#{File.basename($0.to_s)}: #{PACKAGE_NAME} #{PACKAGE_VERSION}\n"
  print "  Creating #{filename} ... "
  
  sfile = open(filename, "w")
  sfile.print <<-EOF
#= #{File.basename($0.to_s)}: #{PACKAGE_NAME} #{PACKAGE_VERSION} configure file
#
#  Created:: #{Time.now.strftime("%Y/%m/%d %H:%M:%S")}
#
#== Overview
#
#This file is generate by following ruby script automatically.
#
#      #{File.expand_path($0.to_s)}
#      version "#{FILE_VERSION}"
#
#Please edit this file according to your purpose.
#And Enter following command.
#
#      #{File.basename($0.to_s)} -c #{filename}
#

# Ignored file or directory names
IGNORE         = ['./', '../', '.', '..', 'CVS', 'RCS']

# Ignored file or directory names by regular expression
IGNORE_REGEXP  = '~$|\.(bak|orig)$|^SIGEN.*html?$'

EOF
  sfile.close

  print "  done.\n"

end

## methods list

#
# 引数 srcdir がカレントディレクトリと同じか, もしくは
# カレントディレクトリの上位ディレクトリの場合, 真を返す.
#
def include_pwd?(srcdir)
  pwdpath = Pathname.new(Dir.pwd).expand_path
  srcpath = Pathname.new(srcdir).expand_path

  pwd_array = pwdpath.to_s.split("/")

  while pwd_array.size > 0
    pwd_array[0] = "/" if pwd_array.size == 1
    return true if srcpath.to_s == File.join(pwd_array)
    pwd_array.pop
  end
  false

  # 低速だが厳密にチェックする方法 (時間がかかりすぎて却下)
#  Pathname.glob(srcpath.join("**/")){ |dir|
#    return true if dir.expand_path == pwdpath
#  }
#  false

  # 高速だがいい加減にチェックする方法 (気に食わなかったので却下)
#  return true if pwdpath == srcpath
#  return true if pwdpath.to_s =~ /^#{srcpath.to_s}/

end

#
# 引数 dstdir 内にシンボリックリンクとディレクトリ以外がないかを
# チェックする. もしもそれ以外があった場合, エラーを返して終了する.
#
def only_sym_or_dir?(dstdir)
  ndir = Pathname.new(dstdir.strip.chomp)

  ndir.each_entry{ |file|
    next if file.to_s == ".." || file.to_s == "."
    fullpath = ndir.join(file)
    if fullpath.directory?
      only_sym_or_dir?(fullpath.to_s)
    elsif !fullpath.symlink?
      raise ArgumentError, "\n\n  Error : \"#{fullpath.to_s}\" is not directory or symbolic link.\n\n"
    end
  }
  true
end


#
# 引数 dstdir のディレクトリを削除する. ただし, 
# ディレクトリかシンボリックリンク以外のファイルが現れた場合,
# エラーを返して終了する.
#
def deldir(dstdir, origdir)
  @@depth += 1
  ndir = Pathname.new(dstdir.strip.chomp)

  # check
  unless ndir.exist?
    raise ArgumentError, "\n\n  Error : \"#{ndir.to_s}\" is not found.\n\n"
  end

  unless ndir.directory?
    raise ArgumentError, "\n\n  Error : \"#{ndir.to_s}\" is not directory.\n\n"
  end

  # ディレクトリ内を探査し, 中にあるのがシンボリックリンクならば削除.
  # ディレクトリならば再帰的にメソッド deldir を呼び出す. それ以外
  # のファイルが見つかったらエラーを返して終了. (上でチェックしているが念のため)
  ndir.each_entry{ |f_or_d|
    next if f_or_d.to_s == ".." || f_or_d.to_s == "."
    fullpath = ndir.join(f_or_d)
    if fullpath.directory?
      deldir(fullpath.to_s, origdir)
    elsif fullpath.symlink?
      fullpath.unlink unless OPTS[:dryrun]
      print "  deleting #{fullpath}\n" if OPTS[:verbose]
    else
      raise ArgumentError, "\n\n  Error : \"#{dir.to_s}\" is not directory or symbolic link.\n\n"
    end
  }

  # 脱出
  @@depth -= 1
  print "  deleting #{ndir.to_s}\n" if OPTS[:verbose]
  Dir.rmdir(ndir.to_s) unless OPTS[:dryrun]
end


#
# 引数 srcdir のディレクトリ構成をカレントディレクトリに作成する.
#
def lndir(srcdir, origdir)
  @@depth += 1
  ndir = Pathname.new(srcdir.strip.chomp)
  relpath2dst = Pathname.pwd.relative_path_from(origdir)
  if ndir.relative?
    path2src = ndir.relative_path_from(Pathname.new("../"*@@depth))
  else
    path2src = Pathname.new(ndir)
  end

  # check
  unless ndir.exist?
    raise ArgumentError, "\n\n  Error : \"#{ndir.to_s}\" is not found.\n\n"
  end

  unless ndir.directory?
    raise ArgumentError, "\n\n  Error : \"#{ndir.to_s}\" is not directory.\n\n"
  end

  if include_pwd?(srcdir)
    raise ArgumentError, "\n\n  Error : \"#{srcdir.to_s}\" include current directory.\n\n"
  end

  # create srcdir name
  dirname = ndir.expand_path.basename
  if dirname.exist?
    raise IOError, "\n\n  Error : \"#{dirname.to_s}\" is already exist.\n\n"
  end

  print "  Directory: #{relpath2dst.join(dirname)} is created\n" if OPTS[:verbose]
  dirname.mkdir
  Dir.chdir(dirname.to_s)

  # 相対パスと絶対パスで場合分け
  if ndir.absolute?
    dir = Pathname.new(ndir)
  else
    dir = Pathname.new(File.join("..", ndir.to_s))
  end

  # ディレクトリ内を探査し, 中にあるのがファイルならシンボリックリンク
  # を作成, ディレクトリならば再帰的にメソッド lndir を呼び出す.
  dir.each_entry{ |f_or_d|
    fullpath = dir.join(f_or_d.to_s)
    next if IGNORE.include?(f_or_d.to_s)
    next if !IGNORE_REGEXP.strip.chomp.empty? && /#{IGNORE_REGEXP}/ =~ f_or_d.to_s
    next if OPTS[:ignore] && OPTS[:ignore].include?(f_or_d.to_s)
    next if OPTS[:ignore_regexp] && /#{OPTS[:ignore_regexp]}/ =~ f_or_d.to_s

    if fullpath.directory?
      fname = f_or_d.basename
      lndir(dir.join(fname.to_s).to_s, origdir)
    else 
      if f_or_d.exist?
        raise IOError, "\n\n  Error : \"#{f_or_d.to_s}\" is alread exist.\n\n"
      end
      f_or_d.make_symlink(dir.join(f_or_d)) unless OPTS[:dryrun]
      print "  SymLink  : #{path2src.join(f_or_d.basename)} => #{relpath2dst.join(dirname, f_or_d)}\n" if OPTS[:verbose]
    end
  }

  # 脱出
  Dir.chdir("../")
  @@depth -= 1
  Dir.rmdir(dirname.to_s) if OPTS[:dryrun]

end

## main
if OPTS[:help] then
  help_detail(opt)
  exit
end

if OPTS[:output_sample] then
  sample_conf
  exit
end

if ARGV.size == 0 then
  help(opt)
  exit
end

if OPTS[:config] then
  print "#{File.basename($0.to_s)}: #{PACKAGE_NAME} #{PACKAGE_VERSION}\n"
  print "  Loading #{OPTS[:config]} ... \n"
  $stdout.flush

  load OPTS[:config]

  print "  .. done.\n"
else
  IGNORE         = IGNORE_DEFAULT
  IGNORE_REGEXP  = IGNORE_REGEXP_DEFAULT
end


srcdir = ARGV[0]
origdir = Pathname.new(Dir.pwd)
if OPTS[:delete]
  only_sym_or_dir?(srcdir)
  @@depth = -1
  deldir(srcdir, origdir)
end
if OPTS[:force]
  only_sym_or_dir?(File.basename(srcdir))
  @@depth = -1
  deldir(File.basename(srcdir), origdir)
end
@@depth = -1
lndir(srcdir, origdir) unless OPTS[:delete]
exit 0
