require 'optparse' require 'shellwords' module QED def self.main(*argv) Command.main(*argv) end # = QED Commandline Tool # # TODO: Merge Command with Session ? class Command # Configuration directory `.qed`, `.config/qed` or `config/qed`. # In this directory special configuration files can be placed # to autmatically effect qed execution. In particular you can # add a `profiles.yml` file to setup convenient execution # scenarios. CONFIG_PATTERN = "{.,.config/,config/}qed" # Default location of demonstrations if no specific files # or locations given. This is use in Dir.glob. The default # locations are qed/, demo/ or demos/, searched for in that # order relative to the root directory. DEMO_LOCATION = '{qed,demo,demos}' # Glob pattern used to search for project's root directory. ROOT_PATTERN = '{.root,.git,.hg,_darcs}/' # Home directory. HOME = File.expand_path('~') # Instantiate a new Command object and call #execute. def self.main(*argv) new.execute(argv) end # Ouput format. attr :format # Make sure format is a symbol. def format=(type) @format = type.to_sym end # Trace execution? attr :trace # Options defined by selected profile. attr :profile # Command-line options. attr :options # Files to be run. attr :files # Ensure files are in a flat list. def files=(globs) @files = [globs].flatten end # Paths to be added to $LOAD_PATH. attr_accessor :loadpath # Libraries to be required. attr_accessor :requires # ? attr_accessor :extension # Move to root directory? attr_accessor :root # Parse mode. attr_accessor :mode # # TODO: Should extension and profile have a common reference? def initialize @format = :dotprogress @extension = :default @profile = :default @requires = [] @loadpath = [] @files = [] @options = {} end # Instance of OptionParser def opts @opts ||= OptionParser.new do |opt| opt.separator("Custom Profiles:") unless profiles.empty? profiles.each do |name, value| o = "--#{name}" opt.on(o, "#{name} custom profile") do @profile = name end end opt.separator("Report Formats (pick one):") opt.on('--dotprogress', '-d', "use dot-progress reporter [default]") do @options[:format] = :dotprogress end opt.on('--verbatim', '-v', "use verbatim reporter") do @options[:format] = :verbatim end opt.on('--bullet', '-b', "use bullet-point reporter") do @options[:format] = :bullet end opt.on('--html', '-h', "use underlying HTML reporter") do @options[:format] = :html end opt.on('--format', '-f FORMAT', "use custom reporter") do |format| @options[:format] = format end #opt.on('--script', "psuedo-reporter") do # @options[:format] = :script # psuedo-reporter #end opt.separator("Control Options:") opt.on('--root', '-R', "run command from project's root directory") do @options[:root] = true end opt.on('--comment', '-c', "Run comment code.") do @options[:mode] = :comment end opt.on('--ext', '-e NAME', "runtime extension [default]") do |name| @options[:extension] = name end opt.on('--loadpath', "-I PATH", "add paths to $LOAD_PATH") do |arg| @options[:loadpath] ||= [] @options[:loadpath].concat(arg.split(/[:;]/).map{ |dir| File.expand_path(dir) }) end opt.on('--require', "-r LIB", "require library") do |arg| @options[:requires] ||= [] @options[:requires].concat(arg.split(/[:;]/)) #.map{ |dir| File.expand_path(dir) }) end opt.on('--trace', '-t', "show full backtraces for exceptions") do @options[:trace] = true end opt.on('--debug', "exit immediately upon raised exception") do $VERBOSE = true # wish this were called $WARN $DEBUG = true end opt.separator("Optional Commands:") opt.on_tail('--version', "display version") do puts "QED #{VERSION}" exit end opt.on_tail('--copyright', "display copyrights") do puts "Copyright (c) 2008, 2009 Thomas Sawyer, GPL License" exit end opt.on_tail('--help', '-h', "display this help message") do puts opt exit end end end # Default recognized demos file types. DEMO_TYPES = %w{qed rdoc md markdown} CODE_TYPES = %w{rb} # Returns a list of demo files. The files returned depends on the # +files+ attribute and if none given, then the current run mode. def demos @demos ||= ( if mode == :comment demos_in_comment_mode else demos_in_normal_mode end ) end # Collect default files to process in normal demo mode. def demos_in_normal_mode demos_gather(DEMO_LOCATION, DEMO_TYPES) end # Collect default files to process in code comment mode. # # TODO: Sure removing applique files is the best approach? # # TODO: Either add environment alond with applique or deprecate environment # as an alternate name. def demos_in_comment_mode files = demos_gather('lib', CODE_TYPES) files = files.reject{ |f| f.index('applique/') } # don't include applique files ??? files end # def demos_gather(default_location, extensions=DEMO_TYPES) files = self.files files << default_location if files.empty? files = files.map{|pattern| Dir[pattern]}.flatten.uniq files = files.map do |file| if File.directory?(file) Dir[File.join(file,'**','*.{' + extensions.join(',') + '}')] else file end end files = files.flatten.uniq files.map{|f| File.expand_path(f) }.uniq.sort end # Parse command-line options along with profile options. def parse(argv) #@files = [] opts.parse!(argv ||= ARGV.dup) #@files.concat(argv) @files = argv #if profile if args = profiles[profile] argv = Shellwords.shellwords(args) opts.parse!(argv) @files.concat(argv) end #end options.each do |k,v| __send__("#{k}=", v) end end # Run demonstrations. def execute(argv) parse(argv) jump = @options[:root] ? root_directory : Dir.pwd Dir.chdir(jump) do abort "No documents." if demos.empty? prepare_loadpath require_libraries require_profile session.run end end # Session instance. def session @session ||= Session.new(demos, :format=>format, :trace=>trace, :mode=>mode) end # Project's root directory. def root_directory @root_directory ||= find_root end # Project's QED configuation directory. def config_directory @config_directory ||= find_config #Dir[File.join(root_directory, CONFIG_PATTERN)].first end # Profile configurations. def profiles @profiles ||= ( file = Dir["#{config_directory}/profile{,s}.{yml,yaml}"].first file ? YAML.load(File.new(file)) : {} ) end # Add to load path (from -I option). def prepare_loadpath loadpath.each{ |dir| $LOAD_PATH.unshift(dir) } end # Require libraries (from -r option). def require_libraries requires.each{ |file| require(file) } end # Require requirement file (from -e option). def require_profile return unless config_directory if file = Dir["#{config_directory}/#{extension}.rb"].first require(file) end end # Locate project's root directory. This is done by searching upward # in the file heirarchy for the existence of one of the following # path names, each group being tried in turn. # # * .root/ # * .git/ # * .hg/ # * _darcs/ # # Failing to find any of these locations, resort to the fallback: # # * lib/ # def find_root(path=nil) path = File.expand_path(path || Dir.pwd) path = File.dirname(path) unless File.directory?(path) root = lookup(ROOT_PATTERN, path) return root if root #root = lookup(path, '{.qed,.config/qed,config/qed}/') #return root if root #root = lookup(path, '{qed,demo,demos}/') #return root if root root = lookup('lib/', path) return root if root abort "Failed to resolve project's root location. Try adding a .root directory." end # Locate configuration directory by seaching up the # file hierachy relative to the working directory # for one of the following paths: # # * .qed/ # * .config/qed/ # * config/qed/ # def find_config lookup(CONFIG_PATTERN) end # Lookup path +glob+, searching each higher directory # in turn until just before the users home directory # is reached or just before the system's root directory. # # TODO: include HOME directory in search? def lookup(glob, path=Dir.pwd) until path == HOME or path == '/' # until home or root mark = Dir.glob(File.join(path,glob), File::FNM_CASEFOLD).first return path if mark path = File.dirname(path) end end end end