bahuvrihi-tap 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (86) hide show
  1. data/History +69 -0
  2. data/MIT-LICENSE +21 -0
  3. data/README +119 -0
  4. data/bin/tap +114 -0
  5. data/cmd/console.rb +42 -0
  6. data/cmd/destroy.rb +16 -0
  7. data/cmd/generate.rb +16 -0
  8. data/cmd/run.rb +126 -0
  9. data/doc/Class Reference +362 -0
  10. data/doc/Command Reference +153 -0
  11. data/doc/Tutorial +237 -0
  12. data/lib/tap.rb +32 -0
  13. data/lib/tap/app.rb +720 -0
  14. data/lib/tap/constants.rb +8 -0
  15. data/lib/tap/env.rb +640 -0
  16. data/lib/tap/file_task.rb +547 -0
  17. data/lib/tap/generator/base.rb +109 -0
  18. data/lib/tap/generator/destroy.rb +37 -0
  19. data/lib/tap/generator/generate.rb +61 -0
  20. data/lib/tap/generator/generators/command/command_generator.rb +21 -0
  21. data/lib/tap/generator/generators/command/templates/command.erb +32 -0
  22. data/lib/tap/generator/generators/config/config_generator.rb +26 -0
  23. data/lib/tap/generator/generators/config/templates/doc.erb +12 -0
  24. data/lib/tap/generator/generators/config/templates/nodoc.erb +8 -0
  25. data/lib/tap/generator/generators/file_task/file_task_generator.rb +27 -0
  26. data/lib/tap/generator/generators/file_task/templates/file.txt +11 -0
  27. data/lib/tap/generator/generators/file_task/templates/result.yml +6 -0
  28. data/lib/tap/generator/generators/file_task/templates/task.erb +33 -0
  29. data/lib/tap/generator/generators/file_task/templates/test.erb +29 -0
  30. data/lib/tap/generator/generators/root/root_generator.rb +55 -0
  31. data/lib/tap/generator/generators/root/templates/Rakefile +86 -0
  32. data/lib/tap/generator/generators/root/templates/gemspec +27 -0
  33. data/lib/tap/generator/generators/root/templates/tapfile +8 -0
  34. data/lib/tap/generator/generators/root/templates/test/tap_test_helper.rb +3 -0
  35. data/lib/tap/generator/generators/root/templates/test/tap_test_suite.rb +5 -0
  36. data/lib/tap/generator/generators/root/templates/test/tapfile_test.rb +15 -0
  37. data/lib/tap/generator/generators/task/task_generator.rb +27 -0
  38. data/lib/tap/generator/generators/task/templates/task.erb +14 -0
  39. data/lib/tap/generator/generators/task/templates/test.erb +21 -0
  40. data/lib/tap/generator/manifest.rb +14 -0
  41. data/lib/tap/patches/rake/rake_test_loader.rb +8 -0
  42. data/lib/tap/patches/rake/testtask.rb +55 -0
  43. data/lib/tap/patches/ruby19/backtrace_filter.rb +51 -0
  44. data/lib/tap/patches/ruby19/parsedate.rb +16 -0
  45. data/lib/tap/root.rb +581 -0
  46. data/lib/tap/support/aggregator.rb +55 -0
  47. data/lib/tap/support/assignments.rb +172 -0
  48. data/lib/tap/support/audit.rb +418 -0
  49. data/lib/tap/support/batchable.rb +47 -0
  50. data/lib/tap/support/batchable_class.rb +107 -0
  51. data/lib/tap/support/class_configuration.rb +194 -0
  52. data/lib/tap/support/command_line.rb +98 -0
  53. data/lib/tap/support/comment.rb +270 -0
  54. data/lib/tap/support/configurable.rb +114 -0
  55. data/lib/tap/support/configurable_class.rb +296 -0
  56. data/lib/tap/support/configuration.rb +122 -0
  57. data/lib/tap/support/constant.rb +70 -0
  58. data/lib/tap/support/constant_utils.rb +127 -0
  59. data/lib/tap/support/declarations.rb +111 -0
  60. data/lib/tap/support/executable.rb +111 -0
  61. data/lib/tap/support/executable_queue.rb +82 -0
  62. data/lib/tap/support/framework.rb +71 -0
  63. data/lib/tap/support/framework_class.rb +199 -0
  64. data/lib/tap/support/instance_configuration.rb +147 -0
  65. data/lib/tap/support/lazydoc.rb +428 -0
  66. data/lib/tap/support/manifest.rb +89 -0
  67. data/lib/tap/support/run_error.rb +39 -0
  68. data/lib/tap/support/shell_utils.rb +71 -0
  69. data/lib/tap/support/summary.rb +30 -0
  70. data/lib/tap/support/tdoc.rb +404 -0
  71. data/lib/tap/support/tdoc/tdoc_html_generator.rb +38 -0
  72. data/lib/tap/support/tdoc/tdoc_html_template.rb +42 -0
  73. data/lib/tap/support/templater.rb +180 -0
  74. data/lib/tap/support/validation.rb +410 -0
  75. data/lib/tap/support/versions.rb +97 -0
  76. data/lib/tap/task.rb +259 -0
  77. data/lib/tap/tasks/dump.rb +56 -0
  78. data/lib/tap/tasks/rake.rb +93 -0
  79. data/lib/tap/test.rb +37 -0
  80. data/lib/tap/test/env_vars.rb +29 -0
  81. data/lib/tap/test/file_methods.rb +377 -0
  82. data/lib/tap/test/script_methods.rb +144 -0
  83. data/lib/tap/test/subset_methods.rb +420 -0
  84. data/lib/tap/test/tap_methods.rb +237 -0
  85. data/lib/tap/workflow.rb +187 -0
  86. metadata +145 -0
@@ -0,0 +1,86 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+ require 'rake/rdoctask'
4
+ require 'rake/gempackagetask'
5
+
6
+ require 'tap/constants'
7
+ require 'tap/patches/rake/testtask.rb'
8
+
9
+ #
10
+ # Gem specification
11
+ #
12
+
13
+ def gemspec
14
+ data = File.read('<%= project_name %>.gemspec')
15
+ spec = nil
16
+ Thread.new { spec = eval("$SAFE = 3\n#{data}") }.join
17
+ spec
18
+ end
19
+
20
+ Rake::GemPackageTask.new(gemspec) do |pkg|
21
+ pkg.need_tar = true
22
+ end
23
+
24
+ desc 'Prints the gemspec manifest.'
25
+ task :print_manifest do
26
+ # collect files from the gemspec, labeling
27
+ # with true or false corresponding to the
28
+ # file existing or not
29
+ files = gemspec.files.inject({}) do |files, file|
30
+ files[File.expand_path(file)] = [File.exists?(file), file]
31
+ files
32
+ end
33
+
34
+ # gather non-rdoc/pkg files for the project
35
+ # and add to the files list if they are not
36
+ # included already (marking by the absence
37
+ # of a label)
38
+ Dir.glob("**/*").each do |file|
39
+ next if file =~ /^(rdoc|pkg)/ || File.directory?(file)
40
+
41
+ path = File.expand_path(file)
42
+ files[path] = ["", file] unless files.has_key?(path)
43
+ end
44
+
45
+ # sort and output the results
46
+ files.values.sort_by {|exists, file| file }.each do |entry|
47
+ puts "%-5s : %s" % entry
48
+ end
49
+ end
50
+
51
+ #
52
+ # Documentation tasks
53
+ #
54
+
55
+ desc 'Generate documentation.'
56
+ Rake::RDocTask.new(:rdoc) do |rdoc|
57
+ spec = gemspec
58
+
59
+ rdoc.rdoc_dir = 'rdoc'
60
+ rdoc.title = '<%= project_name %>'
61
+ rdoc.options << '--line-numbers' << '--inline-source'
62
+ rdoc.rdoc_files.include( spec.extra_rdoc_files )
63
+ rdoc.rdoc_files.include( spec.files.select {|file| file =~ /^lib.*\.rb$/} )
64
+
65
+ # Using Tdoc to template your Rdoc will result in configurations being
66
+ # listed with documentation in a subsection following attributes. Not
67
+ # necessary, but nice.
68
+ require 'tap/support/tdoc'
69
+ rdoc.template = 'tap/support/tdoc/tdoc_html_template'
70
+ rdoc.options << '--fmt' << 'tdoc'
71
+ end
72
+
73
+ #
74
+ # Test tasks
75
+ #
76
+
77
+ desc 'Default: Run tests.'
78
+ task :default => :test
79
+
80
+ desc 'Run tests.'
81
+ Rake::TestTask.new(:test) do |t|
82
+ t.test_files = Dir.glob( File.join('test', ENV['pattern'] || '**/*_test.rb') )
83
+ t.verbose = true
84
+ t.warning = true
85
+ end
86
+
@@ -0,0 +1,27 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = "<%= project_name %>"
3
+ s.version = "0.0.1"
4
+ #s.author = "Your Name Here"
5
+ #s.email = "your.email@pubfactory.edu"
6
+ #s.homepage = "http://rubyforge.org/projects/<%= project_name %>/"
7
+ s.platform = Gem::Platform::RUBY
8
+ s.summary = "<%= project_name %> task library"
9
+ s.require_path = "lib"
10
+ s.test_file = "test/tap_test_suite.rb"
11
+ #s.rubyforge_project = "<%= project_name %>"
12
+ #s.has_rdoc = true
13
+ s.add_dependency("tap", "~> <%= Tap::VERSION %>")
14
+
15
+ # list extra rdoc files like README here.
16
+ s.extra_rdoc_files = %W{
17
+ }
18
+
19
+ # list the files you want to include here. you can
20
+ # check this manifest using 'rake :print_manifest'
21
+ s.files = %W{
22
+ tapfile.rb
23
+ test/tap_test_helper.rb
24
+ test/tap_test_suite.rb
25
+ test/tapfile_test.rb
26
+ }
27
+ end
@@ -0,0 +1,8 @@
1
+ require 'tap'
2
+
3
+ # Goodnight::manifest your basic goodnight moon task
4
+ # Prints the input with a configurable message.
5
+ Tap.task('goodnight', :message => 'goodnight') do |task, name|
6
+ task.log task.message, name
7
+ "#{task.message} #{name}"
8
+ end
@@ -0,0 +1,3 @@
1
+ require 'rubygems'
2
+ require 'tap'
3
+ require 'tap/test'
@@ -0,0 +1,5 @@
1
+ $:.unshift File.join(File.dirname(__FILE__), '../lib')
2
+
3
+ # runs all subsets (see Tap::Test::SubsetMethods)
4
+ ENV["ALL"] = "true"
5
+ Dir.glob("./**/*_test.rb").each {|test| require test}
@@ -0,0 +1,15 @@
1
+ require File.dirname(__FILE__) + '/tap_test_helper.rb'
2
+ require File.dirname(__FILE__) + '/../tapfile.rb'
3
+
4
+ class TapfileTest < Test::Unit::TestCase
5
+ acts_as_tap_test
6
+
7
+ def test_goodnight
8
+ task = Goodnight.new :message => "goodnight"
9
+
10
+ # a simple test
11
+ assert_equal({:message => 'goodnight'}, task.config)
12
+ assert_equal "goodnight moon", task.process("moon")
13
+ end
14
+
15
+ end
@@ -0,0 +1,27 @@
1
+ module Tap::Generator::Generators
2
+
3
+ # :startdoc::generator a task and test
4
+ #
5
+ # Generates a new Tap::Task and an associated test file.
6
+ class TaskGenerator < Tap::Generator::Base
7
+
8
+ config :test, true, &c.switch # Generates the task without test files.
9
+
10
+ def manifest(m, const_name)
11
+ const = Constant.new(const_name.camelize)
12
+
13
+ task_path = app.filepath('lib', "#{const.path}.rb")
14
+ m.directory File.dirname(task_path)
15
+ m.template task_path, "task.erb", :const => const
16
+
17
+ if test
18
+ test_path = app.filepath('test', "#{const.path}_test.rb")
19
+ m.directory File.dirname(test_path)
20
+ m.template test_path, "test.erb", :const => const
21
+ end
22
+
23
+ const
24
+ end
25
+
26
+ end
27
+ end
@@ -0,0 +1,14 @@
1
+ <% redirect do |target| %># <%= const.name %>::manifest <replace with manifest summary>
2
+ # <replace with command line description>
3
+
4
+ # <%= const.const_name %> Documentation
5
+ class <%= const.const_name %> < Tap::Task
6
+
7
+ # <config file documentation>
8
+ config :message, 'goodnight' # a sample config
9
+
10
+ def process(name)
11
+ log message, name
12
+ "#{message} #{name}"
13
+ end
14
+ end <% module_nest(const.nesting, ' ') { target } end %>
@@ -0,0 +1,21 @@
1
+ require File.join(File.dirname(__FILE__), '<%= '../' * const.nesting_depth %>tap_test_helper.rb')
2
+ require '<%= const.path %>'
3
+
4
+ class <%= const.name %>Test < Test::Unit::TestCase
5
+ acts_as_tap_test
6
+
7
+ def test_<%= const.basename %>
8
+ task = <%= const.name %>.new :message => "goodnight"
9
+
10
+ # a simple test
11
+ assert_equal({:message => 'goodnight'}, task.config)
12
+ assert_equal "goodnight moon", task.process("moon")
13
+
14
+ # a more complex test
15
+ task.enq("moon")
16
+ app.run
17
+
18
+ assert_equal ["goodnight moon"], app.results(task)
19
+ assert_audit_equal ExpAudit[[nil, "moon"], [task, "goodnight moon"]], app._results(task)[0]
20
+ end
21
+ end
@@ -0,0 +1,14 @@
1
+ module Tap
2
+ module Generator
3
+ class Manifest
4
+ def initialize(actions)
5
+ @actions = actions
6
+ end
7
+
8
+ # Record an action.
9
+ def method_missing(action, *args, &block)
10
+ @actions << [action, args, block]
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,8 @@
1
+ # this is the same code as in rake/rake_test_loader.rb
2
+ # except it duplicates ARGV before iterating over it.
3
+ #
4
+ # This prevents an error in Ruby 1.9 when one of the
5
+ # loaded files attempts to modify ARGV. In that case
6
+ # you get an error like: 'can't modify array during
7
+ # iteration (RuntimeError)'
8
+ ARGV.dup.each { |f| load f unless f =~ /^-/ }
@@ -0,0 +1,55 @@
1
+ # NO idea why this prevents an error with @ruby_opts=nil,
2
+ # or even how @ruby_opts could be nil, on ruby 1.9 with
3
+ # rake test and tdoc. It does, though.
4
+ require 'pp'
5
+
6
+ module Rake # :nodoc:
7
+
8
+ class TestTask < TaskLib # :nodoc:
9
+
10
+ # Patch for TestTask#define in 'rake\testtask.rb'
11
+ #
12
+ # This patch lets you specify Windows-style paths in lib
13
+ # (ie with spaces and slashes) and to do something like:
14
+ #
15
+ # Rake::TestTask.new(:test) do |t|
16
+ # t.libs = $: << 'lib'
17
+ # end
18
+ #
19
+ # Using this patch you can specify additional load paths
20
+ # for the test from the command line using --lib-dir
21
+ #
22
+ def define
23
+ lib_opt = @libs.collect {|f| "-I\"#{File.expand_path(f)}\""}.join(' ')
24
+ desc "Run tests" + (@name==:test ? "" : " for #{@name}")
25
+ task @name do
26
+ run_code = ''
27
+ RakeFileUtils.verbose(@verbose) do
28
+ run_code =
29
+ case @loader
30
+ when :direct
31
+ "-e 'ARGV.each{|f| load f}'"
32
+ when :testrb
33
+ "-S testrb #{fix}"
34
+ when :rake
35
+ rake_loader
36
+ end
37
+ @ruby_opts.unshift(lib_opt)
38
+ @ruby_opts.unshift( "-w" ) if @warning
39
+ ruby @ruby_opts.join(" ") +
40
+ " \"#{run_code}\" " +
41
+ file_list.collect { |fn| "\"#{fn}\"" }.join(' ') +
42
+ " #{option_list}"
43
+ end
44
+ end
45
+ self
46
+ end
47
+
48
+ # Loads in the patched rake_test_loader to avoid the ARGV
49
+ # modification error, which arises within TDoc.
50
+ def rake_loader # :nodoc:
51
+ File.expand_path(File.join(File.dirname(__FILE__), 'rake_test_loader.rb'))
52
+ end
53
+ end
54
+
55
+ end
@@ -0,0 +1,51 @@
1
+ module Test
2
+ module Unit
3
+ module Util # :nodoc:
4
+ module BacktraceFilter # :nodoc:
5
+
6
+ if method_defined?(:filter_backtrace)
7
+ alias :tap_original_filter_backtrace :filter_backtrace
8
+ end
9
+
10
+ # This is a slightly-modified version of the default BacktraceFilter
11
+ # provided in the Ruby 1.9 distribution. It solves the issue documented
12
+ # below, and hopefully will not be necessary when Ruby 1.9 is stable.
13
+ #
14
+ def filter_backtrace(backtrace, prefix=nil)
15
+ return ["No backtrace"] unless(backtrace)
16
+ split_p = if(prefix)
17
+ prefix.split(TESTUNIT_FILE_SEPARATORS)
18
+ else
19
+ TESTUNIT_PREFIX
20
+ end
21
+ match = proc do |e|
22
+ split_e = e.split(TESTUNIT_FILE_SEPARATORS)[0, split_p.size]
23
+ next false unless(split_e[0..-2] == split_p[0..-2])
24
+ split_e[-1].sub(TESTUNIT_RB_FILE, '') == split_p[-1]
25
+ end
26
+
27
+ # The Ruby 1.9 issue is that sometimes backtrace is a String
28
+ # and String is no longer Enumerable (hence it doesn't respond
29
+ # respond to detect). Arrayify to solve.
30
+ backtrace = [backtrace] unless backtrace.kind_of?(Array)
31
+
32
+ return backtrace unless(backtrace.detect(&match))
33
+ found_prefix = false
34
+ new_backtrace = backtrace.reverse.reject do |e|
35
+ if(match[e])
36
+ found_prefix = true
37
+ true
38
+ elsif(found_prefix)
39
+ false
40
+ else
41
+ true
42
+ end
43
+ end.reverse
44
+ new_backtrace = (new_backtrace.empty? ? backtrace : new_backtrace)
45
+ new_backtrace = new_backtrace.reject(&match)
46
+ new_backtrace.empty? ? backtrace : new_backtrace
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,16 @@
1
+ require 'date/format'
2
+
3
+ # Taken directly from the Ruby 1.8.6 standard library. Ruby 1.9 has apparently
4
+ # eliminated this class, but it is currently (2008-02-08) required by ActiveSupport.
5
+ #
6
+ # Making this file available to ActiveSupport allows testing under Ruby 1.9,
7
+ # but this patch should be unnecessary when ActiveSupport upgrades and becomes
8
+ # compatible with Ruby 1.9
9
+ module ParseDate # :nodoc:
10
+ def parsedate(str, comp=false)
11
+ Date._parse(str, comp).
12
+ values_at(:year, :mon, :mday, :hour, :min, :sec, :zone, :wday)
13
+ end
14
+
15
+ module_function :parsedate
16
+ end
data/lib/tap/root.rb ADDED
@@ -0,0 +1,581 @@
1
+ require 'tap/support/versions'
2
+ require 'tap/support/configurable'
3
+ autoload(:FileUtils, 'fileutils')
4
+
5
+ module Tap
6
+
7
+ # Root allows you to define a root directory and alias subdirectories, so that
8
+ # you can conceptualize what filepaths you need without predefining the full
9
+ # filepaths. Root also simplifies operations on filepaths.
10
+ #
11
+ # # define a root directory with aliased subdirectories
12
+ # r = Root.new '/root_dir', :input => 'in', :output => 'out'
13
+ #
14
+ # # work with directories
15
+ # r[:input] # => '/root_dir/in'
16
+ # r[:output] # => '/root_dir/out'
17
+ # r['implicit'] # => '/root_dir/implicit'
18
+ #
19
+ # # expanded paths are returned unchanged
20
+ # r[File.expand_path('expanded')] # => File.expand_path('expanded')
21
+ #
22
+ # # work with filepaths
23
+ # fp = r.filepath(:input, 'path/to/file.txt') # => '/root_dir/in/path/to/file.txt'
24
+ # r.relative_filepath(:input, fp) # => 'path/to/file.txt'
25
+ # r.translate(fp, :input, :output) # => '/root_dir/out/path/to/file.txt'
26
+ #
27
+ # # version filepaths
28
+ # r.version('path/to/config.yml', 1.0) # => 'path/to/config-1.0.yml'
29
+ # r.increment('path/to/config-1.0.yml', 0.1) # => 'path/to/config-1.1.yml'
30
+ # r.deversion('path/to/config-1.1.yml') # => ['path/to/config.yml', "1.1"]
31
+ #
32
+ # # absolute paths can also be aliased
33
+ # r[:abs, true] = "/absolute/path"
34
+ # r.filepath(:abs, "to", "file.txt") # => '/absolute/path/to/file.txt'
35
+ #
36
+ # By default, Roots are initialized to the present working directory (Dir.pwd).
37
+ # As in the 'implicit' example, Root infers a path relative to the root directory
38
+ # whenever it needs to resolve an alias that is not explicitly set. The only
39
+ # exceptions to this are fully expanded paths. These are returned unchanged.
40
+ #
41
+ # === Implementation Notes
42
+ #
43
+ # Internally Root stores expanded paths all aliased paths in the 'paths' hash.
44
+ # Expanding paths ensures they remain constant even when the present working
45
+ # directory (Dir.pwd) changes.
46
+ #
47
+ # Root keeps a separate 'directories' hash mapping aliases to their subdirectory paths.
48
+ # This hash allow reassignment if and when the root directory changes. By contrast,
49
+ # there is no separate data structure storing the absolute paths. An absolute path
50
+ # thus has an alias in 'paths' but not 'directories', whereas subdirectory paths
51
+ # have aliases in both.
52
+ #
53
+ # These features may be important to note when subclassing Root:
54
+ # - root and all filepaths in 'paths' are expanded
55
+ # - subdirectory paths are stored in 'directories'
56
+ # - absolute paths are present in 'paths' but not in 'directories'
57
+ #
58
+ class Root
59
+ # Regexp to match a windows-style root filepath.
60
+ WIN_ROOT_PATTERN = /^[A-z]:\//
61
+
62
+ class << self
63
+ include Support::Versions
64
+
65
+ # Returns the filepath of path relative to dir. Both dir and path are
66
+ # expanded before the relative filepath is determined. Returns nil if
67
+ # the path is not relative to dir.
68
+ #
69
+ # Root.relative_filepath('dir', "dir/path/to/file.txt") # => "path/to/file.txt"
70
+ #
71
+ def relative_filepath(dir, path, dir_string=Dir.pwd)
72
+ expanded_dir = File.expand_path(dir, dir_string)
73
+ expanded_path = File.expand_path(path, dir_string)
74
+
75
+ return nil unless expanded_path.index(expanded_dir) == 0
76
+
77
+ # use dir.length + 1 to remove a leading '/'. If dir.length + 1 >= expanded.length
78
+ # as in: relative_filepath('/path', '/path') then the first arg returns nil, and an
79
+ # empty string is returned
80
+ expanded_path[( expanded_dir.chomp("/").length + 1)..-1] || ""
81
+ end
82
+
83
+ # Lists all unique paths matching the input glob patterns.
84
+ def glob(*patterns)
85
+ patterns.collect do |pattern|
86
+ Dir.glob(pattern)
87
+ end.flatten.uniq
88
+ end
89
+
90
+ # Lists all unique versions of path matching the glob version patterns.
91
+ # If no patterns are specified, then all versions of path will be returned.
92
+ def vglob(path, *vpatterns)
93
+ vpatterns << "*" if vpatterns.empty?
94
+ vpatterns.collect do |vpattern|
95
+ results = Dir.glob(version(path, vpattern))
96
+
97
+ # extra work to include the default version path for any version
98
+ results << path if vpattern == "*" && File.exists?(path)
99
+ results
100
+ end.flatten.uniq
101
+ end
102
+
103
+ # Path suffix glob. Globs along the base paths for
104
+ # paths that match the specified suffix pattern.
105
+ def sglob(suffix_pattern, *base_paths)
106
+ base_paths.collect do |base|
107
+ base = File.expand_path(base)
108
+ Dir.glob(File.join(base, suffix_pattern))
109
+ end.flatten.uniq
110
+ end
111
+
112
+ # Executes the block in the specified directory. Makes the directory, if
113
+ # necessary when mkdir is specified. Otherwise, indir raises an error
114
+ # for non-existant directories, as well as non-directory inputs.
115
+ def indir(dir, mkdir=false)
116
+ unless File.directory?(dir)
117
+ if !File.exists?(dir) && mkdir
118
+ FileUtils.mkdir_p(dir)
119
+ else
120
+ raise "not a directory: #{dir}"
121
+ end
122
+ end
123
+
124
+ pwd = Dir.pwd
125
+ begin
126
+ Dir.chdir(dir)
127
+ yield
128
+ ensure
129
+ Dir.chdir(pwd)
130
+ end
131
+ end
132
+
133
+ # The path root type indicating windows, *nix, or some unknown
134
+ # style of filepaths (:win, :nix, :unknown).
135
+ def path_root_type
136
+ @path_root_type ||= case
137
+ when RUBY_PLATFORM =~ /mswin/ && File.expand_path(".") =~ WIN_ROOT_PATTERN then :win
138
+ when File.expand_path(".")[0] == ?/ then :nix
139
+ else :unknown
140
+ end
141
+ end
142
+
143
+ # Returns true if the input path appears to be an expanded path,
144
+ # based on Root.path_root_type.
145
+ #
146
+ # If root_type == :win returns true if the path matches
147
+ # WIN_ROOT_PATTERN.
148
+ #
149
+ # Root.expanded_path?('C:/path') # => true
150
+ # Root.expanded_path?('c:/path') # => true
151
+ # Root.expanded_path?('D:/path') # => true
152
+ # Root.expanded_path?('path') # => false
153
+ #
154
+ # If root_type == :nix, then expanded? returns true if
155
+ # the path begins with '/'.
156
+ #
157
+ # Root.expanded_path?('/path') # => true
158
+ # Root.expanded_path?('path') # => false
159
+ #
160
+ # Otherwise expanded_path? always returns nil.
161
+ def expanded_path?(path, root_type=path_root_type)
162
+ case root_type
163
+ when :win
164
+ path =~ WIN_ROOT_PATTERN ? true : false
165
+ when :nix
166
+ path[0] == ?/
167
+ else
168
+ nil
169
+ end
170
+ end
171
+
172
+ # Minimizes a set of paths to the set of shortest basepaths that unqiuely
173
+ # identify the paths. The path extension and versions are removed from
174
+ # the basepath if possible. For example:
175
+ #
176
+ # Tap::Root.minimize ['path/to/a.rb', 'path/to/b.rb']
177
+ # # => ['a', 'b']
178
+ #
179
+ # Tap::Root.minimize ['path/to/a-0.1.0.rb', 'path/to/b-0.1.0.rb']
180
+ # # => ['a', 'b']
181
+ #
182
+ # Tap::Root.minimize ['path/to/file.rb', 'path/to/file.txt']
183
+ # # => ['file.rb', 'file.txt']
184
+ #
185
+ # Tap::Root.minimize ['path-0.1/to/file.rb', 'path-0.2/to/file.rb']
186
+ # # => ['path-0.1/to/file', 'path-0.2/to/file']
187
+ #
188
+ # Minimized paths that carry their extension will always carry
189
+ # their version as well, but the converse is not true; paths
190
+ # can be minimized to carry just the version and not the path
191
+ # extension.
192
+ #
193
+ # Tap::Root.minimize ['path/to/a-0.1.0.rb', 'path/to/a-0.1.0.txt']
194
+ # # => ['a-0.1.0.rb', 'a-0.1.0.txt']
195
+ #
196
+ # Tap::Root.minimize ['path/to/a-0.1.0.rb', 'path/to/a-0.2.0.rb']
197
+ # # => ['a-0.1.0', 'a-0.2.0']
198
+ #
199
+ # If a block is given, each (path, mini-path) pair will be passed
200
+ # to it after minimization.
201
+ def minimize(paths) # :yields: path, mini_path
202
+ unless block_given?
203
+ mini_paths = []
204
+ minimize(paths) {|p, mp| mini_paths << mp }
205
+ return mini_paths
206
+ end
207
+
208
+ splits = paths.uniq.collect do |path|
209
+ extname = File.extname(path)
210
+ extname = '' if extname =~ /^\.\d+$/
211
+ base = File.basename(path.chomp(extname))
212
+ version = base =~ /(-\d+(\.\d+)*)$/ ? $1 : ''
213
+
214
+ [dirname_or_array(path), base.chomp(version), extname, version, false, path]
215
+ end
216
+
217
+ while !splits.empty?
218
+ index = 0
219
+ splits = splits.collect do |(dir, base, extname, version, flagged, path)|
220
+ index += 1
221
+ case
222
+ when !flagged && just_one?(splits, index, base)
223
+
224
+ # found just one
225
+ yield(path, base)
226
+ nil
227
+ when dir.kind_of?(Array)
228
+
229
+ # no more path segments to use, try to add
230
+ # back version and extname
231
+ if dir.empty?
232
+ dir << File.dirname(base)
233
+ base = File.basename(base)
234
+ end
235
+
236
+ case
237
+ when !version.empty?
238
+ # add back version (occurs first)
239
+ [dir, "#{base}#{version}", extname, '', false, path]
240
+
241
+ when !extname.empty?
242
+
243
+ # add back extension (occurs second)
244
+ [dir, "#{base}#{extname}", '', version, false, path]
245
+ else
246
+
247
+ # nothing more to distinguish... path is minimized (occurs third)
248
+ yield(path, min_join(dir[0], base))
249
+ nil
250
+ end
251
+ else
252
+
253
+ # shift path segment. dirname_or_array returns an
254
+ # array if this is the last path segment to shift.
255
+ [dirname_or_array(dir), min_join(File.basename(dir), base), extname, version, false, path]
256
+ end
257
+ end.compact
258
+ end
259
+ end
260
+
261
+ # Returns true if the mini_path matches path. Matching logic
262
+ # reverses that of minimize:
263
+ # * a match occurs when path ends with mini_path
264
+ # * if mini_path doesn't specify an extension, then mini_path
265
+ # must only match path up to the path extension
266
+ # * if mini_path doesn't specify a version, then mini_path
267
+ # must only match path up to the path basename (minus the
268
+ # version and extname)
269
+ #
270
+ # For example:
271
+ #
272
+ # Tap::Root.minimal_match?('dir/file-0.1.0.rb', 'file') # => true
273
+ # Tap::Root.minimal_match?('dir/file-0.1.0.rb', 'dir/file') # => true
274
+ # Tap::Root.minimal_match?('dir/file-0.1.0.rb', 'file-0.1.0') # => true
275
+ # Tap::Root.minimal_match?('dir/file-0.1.0.rb', 'file-0.1.0.rb') # => true
276
+ #
277
+ # Tap::Root.minimal_match?('dir/file-0.1.0.rb', 'file.rb') # => false
278
+ # Tap::Root.minimal_match?('dir/file-0.1.0.rb', 'file-0.2.0') # => false
279
+ # Tap::Root.minimal_match?('dir/file-0.1.0.rb', 'another') # => false
280
+ #
281
+ # In matching, partial basenames are not allowed but partial directories
282
+ # are allowed. Hence:
283
+ #
284
+ # Tap::Root.minimal_match?('dir/file-0.1.0.txt', 'file') # => true
285
+ # Tap::Root.minimal_match?('dir/file-0.1.0.txt', 'ile') # => false
286
+ # Tap::Root.minimal_match?('dir/file-0.1.0.txt', 'r/file') # => true
287
+ #
288
+ def minimal_match?(path, mini_path)
289
+ extname = File.extname(mini_path)
290
+ extname = '' if extname =~ /^\.\d+$/
291
+ version = mini_path =~ /(-\d+(\.\d+)*)#{extname}$/ ? $1 : ''
292
+
293
+ match_path = case
294
+ when !extname.empty?
295
+ # force full match
296
+ path
297
+ when !version.empty?
298
+ # match up to version
299
+ path.chomp(File.extname(path))
300
+ else
301
+ # match up base
302
+ path.chomp(File.extname(path)).sub(/(-\d+(\.\d+)*)$/, '')
303
+ end
304
+
305
+ # key ends with pattern AND basenames of each are equal...
306
+ # the last check ensures that a full path segment has
307
+ # been specified
308
+ match_path[-mini_path.length, mini_path.length] == mini_path && File.basename(match_path) == File.basename(mini_path)
309
+ end
310
+
311
+ # Returns the path segments for the given path, splitting along the path
312
+ # divider. Root paths are always represented by a string, if only an
313
+ # empty string.
314
+ #
315
+ # os divider example
316
+ # windows '\' Root.split('C:\path\to\file') # => ["C:", "path", "to", "file"]
317
+ # *nix '/' Root.split('/path/to/file') # => ["", "path", "to", "file"]
318
+ #
319
+ # The path is always expanded relative to the expand_dir; so '.' and '..' are
320
+ # resolved. However, unless expand_path == true, only the segments relative
321
+ # to the expand_dir are returned.
322
+ #
323
+ # On windows (note that expanding paths allows the use of slashes or backslashes):
324
+ #
325
+ # Dir.pwd # => 'C:/'
326
+ # Root.split('path\to\..\.\to\file') # => ["C:", "path", "to", "file"]
327
+ # Root.split('path/to/.././to/file', false) # => ["path", "to", "file"]
328
+ #
329
+ # On *nix (or more generally systems with '/' roots):
330
+ #
331
+ # Dir.pwd # => '/'
332
+ # Root.split('path/to/.././to/file') # => ["", "path", "to", "file"]
333
+ # Root.split('path/to/.././to/file', false) # => ["path", "to", "file"]
334
+ #
335
+ def split(path, expand_path=true, expand_dir=Dir.pwd)
336
+ path = if expand_path
337
+ File.expand_path(path, expand_dir)
338
+ else
339
+ # normalize the path by expanding it, then
340
+ # work back to the relative filepath as needed
341
+ expanded_dir = File.expand_path(expand_dir)
342
+ expanded_path = File.expand_path(path, expand_dir)
343
+ expanded_path.index(expanded_dir) != 0 ? expanded_path : Tap::Root.relative_filepath(expanded_dir, expanded_path)
344
+ end
345
+
346
+ segments = path.scan(/[^\/]+/)
347
+
348
+ # add back the root filepath as needed on *nix
349
+ segments.unshift "" if path[0] == ?/
350
+ segments
351
+ end
352
+
353
+ private
354
+
355
+ # utility method for minimize -- joins the
356
+ # dir and path, preventing results like:
357
+ #
358
+ # "./path"
359
+ # "//path"
360
+ def min_join(dir, path) # :nodoc:
361
+ case dir
362
+ when "." then path
363
+ when "/" then "/#{path}"
364
+ else "#{dir}/#{path}"
365
+ end
366
+ end
367
+
368
+ # utility method for minimize -- returns the
369
+ # dirname of path, or an array if the dirname
370
+ # is effectively empty.
371
+ def dirname_or_array(path) # :nodoc:
372
+ dir = File.dirname(path)
373
+ case dir
374
+ when path, '.' then []
375
+ else dir
376
+ end
377
+ end
378
+
379
+ # utility method for minimize -- determines if there
380
+ # is just one of the base in splits, while flagging
381
+ # all matching entries.
382
+ def just_one?(splits, index, base) # :nodoc:
383
+ just_one = true
384
+ index.upto(splits.length-1) do |i|
385
+ if splits[i][1] == base
386
+ splits[i][4] = true
387
+ just_one = false
388
+ end
389
+ end
390
+
391
+ just_one
392
+ end
393
+
394
+ end
395
+
396
+ include Support::Versions
397
+ include Support::Configurable
398
+
399
+ # The root directory.
400
+ config_attr(:root, '.', :writer => false)
401
+
402
+ # A hash of (alias, relative path) pairs for aliased subdirectories.
403
+ config_attr(:directories, {}, :writer => false)
404
+
405
+ # A hash of (alias, relative path) pairs for aliased absolute paths.
406
+ config_attr(:absolute_paths, {}, :reader => false, :writer => false)
407
+
408
+ # A hash of (alias, expanded path) pairs for aliased subdirectories and absolute paths.
409
+ attr_reader :paths
410
+
411
+ # The filesystem root, inferred from self.root
412
+ # (ex '/' on *nix or something like 'C:/' on Windows).
413
+ attr_reader :path_root
414
+
415
+ # Creates a new Root with the given root directory, aliased directories
416
+ # and absolute paths. By default root is the present working directory
417
+ # and no aliased directories or absolute paths are specified.
418
+ def initialize(root=Dir.pwd, directories={}, absolute_paths={})
419
+ assign_paths(root, directories, absolute_paths)
420
+ @config = self.class.configurations.instance_config(self)
421
+ end
422
+
423
+ # Sets the root directory. All paths are reassigned accordingly.
424
+ def root=(path)
425
+ assign_paths(path, directories, absolute_paths)
426
+ end
427
+
428
+ # Sets the directories to those provided. 'root' and :root are reserved
429
+ # and cannot be set using this method (use root= instead).
430
+ #
431
+ # r['alt'] # => File.join(r.root, 'alt')
432
+ # r.directories = {'alt' => 'dir'}
433
+ # r['alt'] # => File.join(r.root, 'dir')
434
+ def directories=(dirs)
435
+ assign_paths(root, dirs, absolute_paths)
436
+ end
437
+
438
+ # Sets the absolute paths to those provided. 'root' and :root are reserved
439
+ # directory keys and cannot be set using this method (use root= instead).
440
+ #
441
+ # r['abs'] # => File.join(r.root, 'abs')
442
+ # r.absolute_paths = {'abs' => '/path/to/dir'}
443
+ # r['abs'] # => '/path/to/dir'
444
+ def absolute_paths=(paths)
445
+ assign_paths(root, directories, paths)
446
+ end
447
+
448
+ # Returns the absolute paths registered with self.
449
+ def absolute_paths
450
+ abs_paths = {}
451
+ paths.each do |da, path|
452
+ abs_paths[da] = path unless directories.include?(da) || da.to_s == 'root'
453
+ end
454
+ abs_paths
455
+ end
456
+
457
+ # Sets an alias for the subdirectory relative to the root directory.
458
+ # The aliases 'root' and :root cannot be set with this method
459
+ # (use root= instead). Absolute filepaths can be set using the
460
+ # second syntax.
461
+ #
462
+ # r = Root.new '/root_dir'
463
+ # r[:dir] = 'path/to/dir'
464
+ # r[:dir] # => '/root_dir/path/to/dir'
465
+ #
466
+ # r[:abs, true] = '/abs/path/to/dir'
467
+ # r[:abs] # => '/abs/path/to/dir'
468
+ #
469
+ #--
470
+ # Implementation Notes:
471
+ # The syntax for setting an absolute filepath requires an odd use []=.
472
+ # In fact the method recieves the arguments (:dir, true, '/abs/path/to/dir')
473
+ # rather than (:dir, '/abs/path/to/dir', true), meaning that internally path
474
+ # and absolute are switched when setting an absolute filepath.
475
+ #++
476
+ def []=(dir, path, absolute=false)
477
+ raise ArgumentError, "The directory key '#{dir}' is reserved." if dir.to_s == 'root'
478
+
479
+ # switch the paths if absolute was provided
480
+ unless absolute == false
481
+ switch = path
482
+ path = absolute
483
+ absolute = switch
484
+ end
485
+
486
+ case
487
+ when path.nil?
488
+ @directories.delete(dir)
489
+ @paths.delete(dir)
490
+ when absolute
491
+ @directories.delete(dir)
492
+ @paths[dir] = File.expand_path(path)
493
+ else
494
+ @directories[dir] = path
495
+ @paths[dir] = File.expand_path(File.join(root, path))
496
+ end
497
+ end
498
+
499
+ # Returns the expanded path for the specified alias. If the alias
500
+ # has not been set, then the path is inferred to be 'root/dir' unless
501
+ # the path is relative to path_root. These paths are returned
502
+ # directly.
503
+ #
504
+ # r = Root.new '/root_dir', :dir => 'path/to/dir'
505
+ # r[:dir] # => '/root_dir/path/to/dir'
506
+ #
507
+ # r.path_root # => '/'
508
+ # r['relative/path'] # => '/root_dir/relative/path'
509
+ # r['/expanded/path'] # => '/expanded/path'
510
+ #
511
+ def [](dir)
512
+ path = self.paths[dir]
513
+ return path unless path == nil
514
+
515
+ dir = dir.to_s
516
+ Root.expanded_path?(dir) ? dir : File.expand_path(File.join(root, dir))
517
+ end
518
+
519
+ # Constructs expanded filepaths relative to the path of the specified alias.
520
+ def filepath(dir, *filename)
521
+ # TODO - consider filename.compact so nils will not raise errors
522
+ File.expand_path(File.join(self[dir], *filename))
523
+ end
524
+
525
+ # Retrieves the filepath relative to the path of the specified alias.
526
+ def relative_filepath(dir, filepath)
527
+ Root.relative_filepath(self[dir], filepath)
528
+ end
529
+
530
+ # Generates a target filepath translated from the aliased input dir to
531
+ # the aliased output dir. Raises an error if the filepath is not relative
532
+ # to the aliased input dir.
533
+ #
534
+ # fp = r.filepath(:in, 'path/to/file.txt') # => '/root_dir/in/path/to/file.txt'
535
+ # r.translate(fp, :in, :out) # => '/root_dir/out/path/to/file.txt'
536
+ def translate(filepath, input_dir, output_dir)
537
+ unless relative_path = relative_filepath(input_dir, filepath)
538
+ raise "\n#{filepath}\nis not relative to:\n#{input_dir}"
539
+ end
540
+ filepath(output_dir, relative_path)
541
+ end
542
+
543
+ # Lists all files in the aliased dir matching the input patterns. Patterns
544
+ # should be valid inputs for +Dir.glob+. If no patterns are specified, lists
545
+ # all files/folders matching '**/*'.
546
+ def glob(dir, *patterns)
547
+ patterns << "**/*" if patterns.empty?
548
+ patterns.collect! {|pattern| filepath(dir, pattern)}
549
+ Root.glob(*patterns)
550
+ end
551
+
552
+ # Lists all versions of filename in the aliased dir matching the version patterns.
553
+ # If no patterns are specified, then all versions of filename will be returned.
554
+ def vglob(dir, filename, *vpatterns)
555
+ Root.vglob(filepath(dir, filename), *vpatterns)
556
+ end
557
+
558
+ # Executes the provided block in the specified directory using Root.indir.
559
+ def indir(dir, mkdir=false)
560
+ Root.indir(self[dir], mkdir) { yield }
561
+ end
562
+
563
+ private
564
+
565
+ # reassigns all paths with the input root, directories, and absolute_paths
566
+ def assign_paths(root, directories, absolute_paths)
567
+ @root = File.expand_path(root)
568
+ @directories = {}
569
+ @paths = {'root' => @root, :root => @root}
570
+
571
+ @path_root = File.dirname(@root)
572
+ while @path_root != (parent = File.dirname(@path_root))
573
+ @path_root = parent
574
+ end
575
+
576
+ directories.each_pair {|dir, path| self[dir] = path }
577
+ absolute_paths.each_pair {|dir, path| self[dir, true] = path }
578
+ end
579
+
580
+ end
581
+ end