#!/usr/bin/env ruby # #= Fortran 90/95 dependency lister # # Authors:: Yasuhiro MORIKAWA # Version:: $Id: f90depend.rb,v 1.2 2007-06-19 15:20:04 morikawa Exp $ # Tag Name:: $Name: gt4f90io-20080812 $ # Copyright:: Copyright (C) GFD Dennou Club, 2006. All rights reserved. # License:: See COPYRIGHT[link:../../COPYRIGHT] # # #引数として受け取る Fortran 90/95 ファイル内の use 文および #module 文を解析し, 依存関係を記したファイルを Makefile 形式 #で標準出力に書き出す. # require "optparse" class Fortran90Dependency def initialize(quiet=nil, verbose=nil, strictly=nil) @entries = [] @quiet = quiet @verbose = verbose @strictly = strictly @progress = $stderr unless quiet end # #探査するファイルを追加する. #引数 files には Fortran90/95 ファイル名を与える. #ファイルが存在しない場合, エラーを返す. # def add_files(*files) files.flatten! progress " Adding " files.each{|file| progress "\n #{file}", '.' entry = Fortran90DependencyEntry.new(file, @strictly) progress "\n Modules: ", '' entry.modules.each{ |m| progress "\n #{m}", '' } progress "\n Uses: ", '' entry.uses.each{ |u| progress "\n #{u}", '' } @entries << entry if entry.modules } progress "\n" @entries end # #探査するファイルリストから, ファイルを削除する. #引数 files には Fortran90/95 ファイル名を与える. # def del_files(*files) files.flatten! progress " Deleting " files.each{|file| @entries.collect!{|entry| if File.expand_path(file) == File.expand_path(entry.file) progress "\n #{file}", '.' next nil else next entry end } @entries.delete_if{|entry| !entry } } progress "\n" @entries end # #依存関係リストを作成する # def gen_list dep_list = [] progress " Generating " @entries.each{|entry| progress "." dep_line = entry.file.sub(/\.f9(0|5)$/i, '.o') + ': ' entry.uses.each{|use_item| @entries.each{|checked_entry| if checked_entry.modules.include?(use_item) dep_line << checked_entry.file.sub(/\.f9(0|5)$/i, '.o') + ' ' break end } } dep_list << dep_line } progress "\n" dep_list.join("\n") end private # # 引数 *char* に与えられたメッセージを表示する. # @quiet が true の場合は何も表示しない. # 第 2 引数 *clipto* に String クラスが与えられ, 且つ # @varbose が true ではない場合, *char* に与えられた文字列は *clipto* # に置き換えられます. # def progress(char, clipto=nil) unless @quiet char = clipto if clipto && !(@verbose) && clipto.class.to_s == 'String' @progress.print(char) @progress.flush end end class Fortran90DependencyEntry attr_reader :file attr_reader :modules attr_reader :uses #引数 file には Fortran 90/95 ファイル名を与える. #ファイルが存在しない場合, エラーを返す. # #strictly に true を与えると Fortran 90/95 ファイル内の #継続行マーカ "&" や 改行マーカ ";" に関しても正しく #解析しますが, 実行時間が大幅に増えます. # def initialize(file, strictly=nil) @file = file if !(file =~ /\.f9(0|5)$/i) $stderr.print "\n Warning: \"#{file}\" is not recognized as a Fortran 90/95 file.\n" + " Please rename a suffix of this file to .f90 etc.\n" return nil end body = File.open(@file, 'r') {|f| f.read} if strictly body = united_to_one_line(body) body = semicolon_to_linefeed(body) end @modules = find_modules(body) @uses = find_uses(body) @uses.collect!{|use_item| if @modules.include?(use_item) nil else use_item end } @uses.delete_if{|use_item| !use_item } end private # #引数 body で与えられる Fortran90/95 ソースコード内で #定義される module 名を取り出す. # def find_modules(body) modules = [] body.split("\n").each{ |line| if line =~ /^\s*?module\s+(\w+)\s*?(!.*?)?$/i modules << $1 end } return modules end # #引数 body で与えられる Fortran90/95 ソースコード内で #定義される use 文を取り出す. # def find_uses(body) uses = [] body.split("\n").each{ |line| if line =~ /^\s*?use\s+(\w+)(.*?)(!.*?)?$/i uses << $1 end } return uses.uniq end # # {RDoc Fortran 90/95 解析機能強化版}[http://www.gfd-dennou.org/library/dcmodel] # の Fortran 95 パーサから移植 # したメソッド. # # # Semicolons are replaced to line feed. # def semicolon_to_linefeed(text) return "" unless text lines = text.split("\n") lines.collect!{ |line| indent_space = "" if line =~ /^(\s+)/ indent_space = $1 end words = line.split("") commentout = false squote = false ; dquote = false words.collect! { |char| if !(squote) && !(dquote) && !(commentout) case char when "!" ; commentout = true ; next char when "\""; dquote = true ; next char when "\'"; squote = true ; next char when ";" ; "\n"+indent_space else next char end elsif commentout next char elsif squote case char when "\'"; squote = false ; next char else next char end elsif dquote case char when "\""; dquote = false ; next char else next char end end } words.join("") } return lines.join("\n") end # # {RDoc Fortran 90/95 解析機能強化版}[http://www.gfd-dennou.org/library/dcmodel] # の Fortran 95 パーサから移植 # したメソッド. # # Continuous lines are united. # # Comments in continuous lines are removed. # If delete_space=false, spaces around "&" are not deleted. # # Example # # before # # subroutine func(a, b, c, d, e, & ! ignored comments # & f, g, h) ! valid comments # # after # # subroutine func(a, b, c, d, e, f, g, h) ! valid comments # def united_to_one_line(f90src, delete_space=true) return "" unless f90src lines = f90src.split("\n") previous_continuing = false now_continuing = false body = "" lines.each{ |line| words = line.split("") next if words.empty? && previous_continuing commentout = false brank_flag = true ; brank_char = "" squote = false ; dquote = false ignore = false words.collect! { |char| if previous_continuing && brank_flag now_continuing = true ignore = true case char when "!" ; break when " " ; brank_char << char ; next "" when "&" brank_flag = false now_continuing = false next "" else brank_flag = false now_continuing = false ignore = false next brank_char + char end end ignore = false if now_continuing next "" elsif !(squote) && !(dquote) && !(commentout) case char when "!" ; commentout = true ; next char when "\""; dquote = true ; next char when "\'"; squote = true ; next char when "&" ; now_continuing = true ; next "" else next char end elsif commentout next char elsif squote case char when "\'"; squote = false ; next char else next char end elsif dquote case char when "\""; dquote = false ; next char else next char end end } if !ignore && !previous_continuing || !brank_flag if previous_continuing if delete_space joined_words = words.join("") body = body.rstrip + " " + joined_words.lstrip else body << words.join("") end else body << "\n" + words.join("") end end previous_continuing = now_continuing ? true : nil now_continuing = nil } return body end end end # # 以下はこのファイルを実行プログラムとして動かした際の動作. # if $0 == __FILE__ opt = OptionParser.new OPTS = {} ARGV.options{|opt| opt.on( '-X=VAL', '--exclude=VAL', "Exclude files (delimiter is \",\")" ){|v| OPTS[:exclude] = v.gsub(/^=/, '')} opt.on_tail('-q', '--quiet', "Progress message is not displayed" ){|v| OPTS[:quiet] = v} opt.on_tail('-v', '--verbose', "Verbose message is displayed" ){|v| OPTS[:verbose] = v} opt.on_tail('-S', '--strict', "Fortran 90/95 files are strictly parsed. (But take a long time.)" ){|v| OPTS[:strict] = v} opt.on_tail('-h', '-H', '--help', "This help message is output" ){|v| OPTS[:help] = v} opt.parse! } if OPTS[:help] || ARGV.size < 1 print <<-"EOF" #{File.basename($0.to_s)}: USAGE: #{File.basename($0.to_s)} f90files [-X=exclude_file,...] [options] OPTION: \n#{opt.to_a[1..-1].join("")} EOF exit end f90dep = Fortran90Dependency.new(OPTS[:quiet], OPTS[:verbose], OPTS[:strict]) f90dep.add_files(ARGV) f90dep.del_files(OPTS[:exclude].split(',')) if OPTS[:exclude] print f90dep.gen_list end