ing 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. data/.gitignore +5 -0
  2. data/GENERATORS.md +2 -0
  3. data/LICENSE +18 -0
  4. data/OPTIONS.md +2 -0
  5. data/README.md +251 -0
  6. data/TASKS.md +21 -0
  7. data/bin/ing +5 -0
  8. data/examples/rspec_convert.rb +102 -0
  9. data/ing.gemspec +29 -0
  10. data/ing.rb +102 -0
  11. data/lib/ing.rb +78 -0
  12. data/lib/ing/actions/create_file.rb +105 -0
  13. data/lib/ing/actions/create_link.rb +57 -0
  14. data/lib/ing/actions/directory.rb +98 -0
  15. data/lib/ing/actions/empty_directory.rb +155 -0
  16. data/lib/ing/actions/file_manipulation.rb +308 -0
  17. data/lib/ing/actions/inject_into_file.rb +109 -0
  18. data/lib/ing/commands/boot.rb +76 -0
  19. data/lib/ing/commands/generate.rb +64 -0
  20. data/lib/ing/commands/help.rb +87 -0
  21. data/lib/ing/commands/implicit.rb +59 -0
  22. data/lib/ing/commands/list.rb +108 -0
  23. data/lib/ing/dispatcher.rb +132 -0
  24. data/lib/ing/files.rb +190 -0
  25. data/lib/ing/lib_trollop.rb +782 -0
  26. data/lib/ing/shell.rb +390 -0
  27. data/lib/ing/trollop/parser.rb +17 -0
  28. data/lib/ing/util.rb +61 -0
  29. data/lib/ing/version.rb +3 -0
  30. data/lib/thor/actions/file_manipulation.rb +30 -0
  31. data/lib/thor/shell/basic.rb +44 -0
  32. data/test/acceptance/ing_run_tests.rb +164 -0
  33. data/test/actions/create_file_spec.rb +209 -0
  34. data/test/actions/create_link_spec.rb +90 -0
  35. data/test/actions/directory_spec.rb +167 -0
  36. data/test/actions/empty_directory_spec.rb +146 -0
  37. data/test/actions/file_manipulation_spec.rb +433 -0
  38. data/test/actions/inject_into_file_spec.rb +147 -0
  39. data/test/fixtures/application.rb +2 -0
  40. data/test/fixtures/app{1}/README +3 -0
  41. data/test/fixtures/bundle/execute.rb +6 -0
  42. data/test/fixtures/bundle/main.thor +1 -0
  43. data/test/fixtures/doc/%file_name%.rb.tt +1 -0
  44. data/test/fixtures/doc/COMMENTER +10 -0
  45. data/test/fixtures/doc/README +3 -0
  46. data/test/fixtures/doc/block_helper.rb +3 -0
  47. data/test/fixtures/doc/components/.empty_directory +0 -0
  48. data/test/fixtures/doc/config.rb +1 -0
  49. data/test/fixtures/doc/config.yaml.tt +1 -0
  50. data/test/fixtures/group.ing.rb +76 -0
  51. data/test/fixtures/invok.ing.rb +50 -0
  52. data/test/fixtures/namespace.ing.rb +52 -0
  53. data/test/fixtures/require.ing.rb +7 -0
  54. data/test/fixtures/task.ing.rb +36 -0
  55. data/test/fixtures/task.thor +10 -0
  56. data/test/spec_helper.rb +2 -0
  57. data/test/test_helper.rb +41 -0
  58. data/todo.yml +7 -0
  59. metadata +147 -0
data/ing.rb ADDED
@@ -0,0 +1,102 @@
1
+ # Usage:
2
+ # ing rspec:convert which is equivalent to
3
+ # ing rspec:convert files './{test,spec}/**/*.rb' --convert-dir 'converted'
4
+ #
5
+ module Rspec
6
+
7
+ class Convert
8
+
9
+ GSUBS = \
10
+ [
11
+ [ /^(\s*)(.+)\.should\s+be_true/ , '\1assert \2' ],
12
+ [ /^(\s*)(.+)\.should\s+be_false/ , '\1refute \2' ],
13
+ [ /^(\s*)(.+)\.should\s*==\s*(.+)$/ , '\1assert_equal \3, \2' ],
14
+ [ /^(\s*)(.+)\.should_not\s*==\s*(.+)$/ , '\1refute_equal \3, \2' ],
15
+ [ /^(\s*)(.+)\.should\s*=~\s*(.+)$/ , '\1assert_match(\3, \2)' ],
16
+ [ /^(\s*)(.+)\.should_not\s*=~\s*(.+)$/ , '\1refute_match(\3, \2)' ],
17
+ [ /^(\s*)(.+)\.should\s+be_(.+)$/ , '\1assert \2.\3?' ],
18
+ [ /^(\s*)(.+)\.should_not\s+be_(.+)$/ , '\1refute \2.\3?' ],
19
+ [ /expect\s+\{(.+)\}\.to\s+raise_error\s*\((.*)\)\s*\Z/m,
20
+ 'assert_raises(\2) {\1}' ],
21
+ [ /\{(.+)\}\.should raise_error\s*\((.*)\)\s*\Z/m,
22
+ 'assert_raises(\2) {\1}' ],
23
+ # these next aren't quite right because they need to wrap the next
24
+ # lines as a lambda. Thus the FIXME notes.
25
+ [ /\.should_receive\(([^\)]+)\)\.and_return\((.+)\)/,
26
+ '.stub(\1, \2) do |s| # FIXME' ],
27
+ [ /.stub\!\(([\w:]+)\)\.and_return\((.+)\)/,
28
+ '.stub(\1, \2) do |s| # FIXME' ]
29
+
30
+ ]
31
+
32
+ def self.specify_options(expect)
33
+ expect.banner "Convert rspec 'should/not' matchers to minitest 'assert/refute'"
34
+ expect.text "It's not magic, you still need to hand edit your test files after running this"
35
+ expect.text "\nUsage:"
36
+ expect.text " ing rspec:convert # which is equivalent to"
37
+ expect.text " ing rspec:convert files './{test,spec}/**/*.rb' --convert-dir 'converted'"
38
+ expect.text "\nOptions:"
39
+ expect.opt :pattern, "Directory glob pattern for test files",
40
+ :type => :string, :default => './{test,spec}/**/*.rb'
41
+ expect.opt :convert_dir, "Subdirectory to save converted files",
42
+ :type => :string, :default => 'converted'
43
+ end
44
+
45
+ include Ing::Files
46
+
47
+ attr_accessor :shell, :options, :destination_root, :source_root
48
+
49
+ def destination_root; @destination_root ||= Dir.pwd; end
50
+ def source_root; @source_root ||= File.dirname(__FILE__); end
51
+
52
+ def input_files
53
+ @input_files ||= Dir[ File.expand_path(options[:pattern], source_root) ]
54
+ end
55
+
56
+ def converted_files
57
+ input_files.map {|f|
58
+ File.join( File.dirname(f), options[:convert_dir], File.basename(f) )
59
+ }
60
+ end
61
+
62
+ def conversion_map
63
+ input_files.zip(converted_files)
64
+ end
65
+
66
+ def initialize(options)
67
+ self.options = options
68
+ end
69
+
70
+ def call(pattern=nil)
71
+ options[:pattern] = pattern || options[:pattern]
72
+ shell.say "Processing #{input_files.length} input files: #{options[:pattern]}" if verbose?
73
+ conversion_map.each do |input_f, output_f|
74
+ new_lines = convert_lines(input_f)
75
+ create_file output_f, new_lines.join
76
+ end
77
+ end
78
+ alias :files :call
79
+
80
+ private
81
+
82
+ def convert_lines(fname)
83
+ count = 0; accum = []
84
+ File.open(fname) do |f|
85
+ f.each_line do |line|
86
+ new_line = GSUBS.inject(line) do |str, (rx, replace)|
87
+ str = str.gsub(rx, replace)
88
+ end
89
+ count += 1 if line != new_line
90
+ accum << new_line
91
+ end
92
+ end
93
+ shell.say_status(:convert,
94
+ "#{relative_to_original_destination_root(fname)}: " +
95
+ "#{count} changes",
96
+ :green) if verbose?
97
+ accum
98
+ end
99
+
100
+ end
101
+
102
+ end
data/lib/ing.rb ADDED
@@ -0,0 +1,78 @@
1
+ ['ing/version',
2
+ 'ing/lib_trollop',
3
+ 'ing/trollop/parser',
4
+ 'ing/util',
5
+ 'ing/dispatcher',
6
+ 'ing/shell',
7
+ 'ing/files',
8
+ 'ing/commands/boot',
9
+ 'ing/commands/implicit',
10
+ 'ing/commands/list',
11
+ 'ing/commands/help',
12
+ 'ing/commands/generate'
13
+ ].each do |f|
14
+ require_relative f
15
+ end
16
+
17
+ module Ing
18
+ extend self
19
+
20
+ Error = Class.new(StandardError)
21
+ FileNotFoundError = Class.new(Error)
22
+
23
+ attr_writer :shell_class
24
+
25
+ def shell_class
26
+ @shell_class ||= Shell::Basic
27
+ end
28
+
29
+ def implicit_booter
30
+ ["Implicit"]
31
+ end
32
+
33
+ # Dispatch command line to boot class (if specified, or Implicit otherwise),
34
+ # which in turn dispatches the command after parsing args.
35
+ #
36
+ # Note boot dispatch happens within +Ing::Commands+ namespace.
37
+ #
38
+ def run(argv=ARGV)
39
+ booter = extract_boot_class!(argv) || implicit_booter
40
+ run_boot booter, "call", *argv
41
+ end
42
+
43
+ # Dispatch to the command via +Ing::Boot#call_invoke+
44
+ # Use this when you want to invoke a command from another command, but only
45
+ # if it hasn't been run yet. For example,
46
+ #
47
+ # invoke Some::Task, :some_instance_method, some_argument, :some_option => true
48
+ #
49
+ # You can skip the method and it will assume +#call+ :
50
+ #
51
+ # invoke Some::Task, :some_option => true
52
+ def invoke(klass, *args)
53
+ run_boot implicit_booter, "call_invoke", klass, *args
54
+ end
55
+
56
+ # Dispatch to the command via +Ing::Boot#call_execute+
57
+ # Use this when you want to execute a command from another command, and you
58
+ # don't care if it has been run yet or not. See equivalent examples for
59
+ # +invoke+.
60
+ #
61
+ def execute(klass, *args)
62
+ run_boot implicit_booter, "call_execute", klass, *args
63
+ end
64
+
65
+ private
66
+
67
+ def run_boot(booter, *args)
68
+ Dispatcher.new(["Ing","Commands"], booter, *args).dispatch
69
+ end
70
+
71
+ def extract_boot_class!(args)
72
+ c = Util.to_class_names(args.first)
73
+ if (Commands.const_defined?(c.first, false) rescue nil)
74
+ args.shift; c
75
+ end
76
+ end
77
+
78
+ end
@@ -0,0 +1,105 @@
1
+ require File.expand_path('empty_directory', File.dirname(__FILE__))
2
+
3
+ module Ing
4
+ module Files
5
+
6
+ # Create a new file relative to the destination root with the given data,
7
+ # which is the return value of a block or a data string.
8
+ #
9
+ # ==== Parameters
10
+ # destination<String>:: the relative path to the destination root.
11
+ # data<String|NilClass>:: the data to append to the file.
12
+ # config<Hash>:: give :verbose => false to not log the status.
13
+ #
14
+ # ==== Examples
15
+ #
16
+ # create_file "lib/fun_party.rb" do
17
+ # hostname = ask("What is the virtual hostname I should use?")
18
+ # "vhost.name = #{hostname}"
19
+ # end
20
+ #
21
+ # create_file "config/apache.conf", "your apache config"
22
+ #
23
+ def create_file(destination, *args, &block)
24
+ config = args.last.is_a?(Hash) ? args.pop : {}
25
+ data = args.first
26
+ action CreateFile.new(self, destination, block || data.to_s, config)
27
+ end
28
+ alias :add_file :create_file
29
+
30
+ # CreateFile is a subset of Template, which instead of rendering a file with
31
+ # ERB, it gets the content from the user.
32
+ #
33
+ class CreateFile < EmptyDirectory #:nodoc:
34
+ attr_reader :data
35
+
36
+ def initialize(base, destination, data, config={})
37
+ @data = data
38
+ super(base, destination, config)
39
+ end
40
+
41
+ # Checks if the content of the file at the destination is identical to the rendered result.
42
+ #
43
+ # ==== Returns
44
+ # Boolean:: true if it is identical, false otherwise.
45
+ #
46
+ def identical?
47
+ exists? && File.binread(destination) == render
48
+ end
49
+
50
+ # Holds the content to be added to the file.
51
+ #
52
+ def render
53
+ @render ||= if data.is_a?(Proc)
54
+ data.call
55
+ else
56
+ data
57
+ end
58
+ end
59
+
60
+ def invoke!
61
+ invoke_with_conflict_check do
62
+ FileUtils.mkdir_p(File.dirname(destination))
63
+ File.open(destination, 'wb') { |f| f.write render }
64
+ end
65
+ given_destination
66
+ end
67
+
68
+ protected
69
+
70
+ # Now on conflict we check if the file is identical or not.
71
+ #
72
+ def on_conflict_behavior(&block)
73
+ if identical?
74
+ say_status :identical, :blue
75
+ else
76
+ options = base.options.merge(config)
77
+ force_or_skip_or_conflict(options[:force], options[:skip], &block)
78
+ end
79
+ end
80
+
81
+ # If force is true, run the action, otherwise check if it's not being
82
+ # skipped. If both are false, show the file_collision menu, if the menu
83
+ # returns true, force it, otherwise skip.
84
+ #
85
+ def force_or_skip_or_conflict(force, skip, &block)
86
+ if force
87
+ say_status :force, :yellow
88
+ block.call unless pretend?
89
+ elsif skip
90
+ say_status :skip, :yellow
91
+ else
92
+ say_status :conflict, :red
93
+ force_or_skip_or_conflict(force_on_collision?, true, &block)
94
+ end
95
+ end
96
+
97
+ # Shows the file collision menu to the user and gets the result.
98
+ #
99
+ def force_on_collision?
100
+ base.shell.file_collision(destination){ render }
101
+ end
102
+
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,57 @@
1
+ require File.expand_path('create_file', File.dirname(__FILE__))
2
+
3
+ module Ing
4
+ module Files
5
+
6
+ # Create a new file relative to the destination root from the given source.
7
+ #
8
+ # ==== Parameters
9
+ # destination<String>:: the relative path to the destination root.
10
+ # source<String|NilClass>:: the relative path to the source root.
11
+ # config<Hash>:: give :verbose => false to not log the status.
12
+ # :: give :symbolic => false for hard link.
13
+ #
14
+ # ==== Examples
15
+ #
16
+ # create_link "config/apache.conf", "/etc/apache.conf"
17
+ #
18
+ def create_link(destination, *args, &block)
19
+ config = args.last.is_a?(Hash) ? args.pop : {}
20
+ source = args.first
21
+ action CreateLink.new(self, destination, source, config)
22
+ end
23
+ alias :add_link :create_link
24
+
25
+ # CreateLink is a subset of CreateFile, which instead of taking a block of
26
+ # data, just takes a source string from the user.
27
+ #
28
+ class CreateLink < CreateFile #:nodoc:
29
+ attr_reader :data
30
+
31
+ # Checks if the content of the file at the destination is identical to the rendered result.
32
+ #
33
+ # ==== Returns
34
+ # Boolean:: true if it is identical, false otherwise.
35
+ #
36
+ def identical?
37
+ exists? && File.identical?(render, destination)
38
+ end
39
+
40
+ def invoke!
41
+ invoke_with_conflict_check do
42
+ FileUtils.mkdir_p(File.dirname(destination))
43
+ # Create a symlink by default
44
+ config[:symbolic] = true if config[:symbolic].nil?
45
+ File.unlink(destination) if exists?
46
+ if config[:symbolic]
47
+ File.symlink(render, destination)
48
+ else
49
+ File.link(render, destination)
50
+ end
51
+ end
52
+ given_destination
53
+ end
54
+
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,98 @@
1
+ require File.expand_path('empty_directory', File.dirname(__FILE__))
2
+
3
+ module Ing
4
+ module Files
5
+ # Copies recursively the files from source directory to root directory.
6
+ # If any of the files finishes with .tt, it's considered to be a template
7
+ # and is placed in the destination without the extension .tt. If any
8
+ # empty directory is found, it's copied and all .empty_directory files are
9
+ # ignored. If any file name is wrapped within % signs, the text within
10
+ # the % signs will be executed as a method and replaced with the returned
11
+ # value. Let's suppose a doc directory with the following files:
12
+ #
13
+ # doc/
14
+ # components/.empty_directory
15
+ # README
16
+ # rdoc.rb.tt
17
+ # %app_name%.rb
18
+ #
19
+ # When invoked as:
20
+ #
21
+ # directory "doc"
22
+ #
23
+ # It will create a doc directory in the destination with the following
24
+ # files (assuming that the `app_name` method returns the value "blog"):
25
+ #
26
+ # doc/
27
+ # components/
28
+ # README
29
+ # rdoc.rb
30
+ # blog.rb
31
+ #
32
+ # <b>Encoded path note:</b> Since Thor internals use Object#respond_to? to check if it can
33
+ # expand %something%, this `something` should be a public method in the class calling
34
+ # #directory. If a method is private, Thor stack raises PrivateMethodEncodedError.
35
+ #
36
+ # ==== Parameters
37
+ # source<String>:: the relative path to the source root.
38
+ # destination<String>:: the relative path to the destination root.
39
+ # config<Hash>:: give :verbose => false to not log the status.
40
+ # If :recursive => false, does not look for paths recursively.
41
+ #
42
+ # ==== Examples
43
+ #
44
+ # directory "doc"
45
+ # directory "doc", "docs", :recursive => false
46
+ #
47
+ def directory(source, *args, &block)
48
+ config = args.last.is_a?(Hash) ? args.pop : {}
49
+ destination = args.first || source
50
+ action Directory.new(self, source, destination || source, config, &block)
51
+ end
52
+
53
+ class Directory < EmptyDirectory #:nodoc:
54
+ attr_reader :source
55
+
56
+ def initialize(base, source, destination=nil, config={}, &block)
57
+ @source = File.expand_path(base.find_in_source_paths(source.to_s))
58
+ @block = block
59
+ super(base, destination, { :recursive => true }.merge(config))
60
+ end
61
+
62
+ def invoke!
63
+ base.empty_directory given_destination, config
64
+ execute!
65
+ end
66
+
67
+ def revoke!
68
+ execute!
69
+ end
70
+
71
+ protected
72
+
73
+ def execute!
74
+ lookup = Util.escape_globs(source)
75
+ lookup = config[:recursive] ? File.join(lookup, '**') : lookup
76
+ lookup = File.join(lookup, '{*,.[a-z]*}')
77
+
78
+ Dir[lookup].sort.each do |file_source|
79
+ next if File.directory?(file_source)
80
+ file_destination = File.join(given_destination, file_source.gsub(source, '.'))
81
+ file_destination.gsub!('/./', '/')
82
+
83
+ case file_source
84
+ when /\.empty_directory$/
85
+ dirname = File.dirname(file_destination).gsub(/\/\.$/, '')
86
+ next if dirname == given_destination
87
+ base.empty_directory(dirname, config)
88
+ when /\.tt$/
89
+ destination = base.template(file_source, file_destination[0..-4], config, &@block)
90
+ else
91
+ destination = base.copy_file(file_source, file_destination, config, &@block)
92
+ end
93
+ end
94
+ end
95
+
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,155 @@
1
+ module Ing
2
+ module Files
3
+
4
+ # Creates an empty directory.
5
+ #
6
+ # ==== Parameters
7
+ # destination<String>:: the relative path to the destination root.
8
+ # config<Hash>:: give :verbose => false to not log the status.
9
+ #
10
+ # ==== Examples
11
+ #
12
+ # empty_directory "doc"
13
+ #
14
+ def empty_directory(destination, config={})
15
+ action EmptyDirectory.new(self, destination, config)
16
+ end
17
+
18
+ # Class which holds create directory logic. This is the base class for
19
+ # other actions like create_file and directory.
20
+ #
21
+ # This implementation is based in Templater actions, created by Jonas Nicklas
22
+ # and Michael S. Klishin under MIT LICENSE.
23
+ #
24
+ class EmptyDirectory #:nodoc:
25
+ attr_reader :base, :destination, :given_destination, :relative_destination, :config
26
+
27
+ # Initializes given the source and destination.
28
+ #
29
+ # ==== Parameters
30
+ # base<Thor::Base>:: A Thor::Base instance
31
+ # source<String>:: Relative path to the source of this file
32
+ # destination<String>:: Relative path to the destination of this file
33
+ # config<Hash>:: give :verbose => false to not log the status.
34
+ #
35
+ def initialize(base, destination, config={})
36
+ @base, @config = base, { :verbose => true }.merge(config)
37
+ self.destination = destination
38
+ end
39
+
40
+ # Checks if the destination file already exists.
41
+ #
42
+ # ==== Returns
43
+ # Boolean:: true if the file exists, false otherwise.
44
+ #
45
+ def exists?
46
+ ::File.exists?(destination)
47
+ end
48
+
49
+ def invoke!
50
+ invoke_with_conflict_check do
51
+ ::FileUtils.mkdir_p(destination)
52
+ end
53
+ end
54
+
55
+ def revoke!
56
+ say_status :remove, :red
57
+ ::FileUtils.rm_rf(destination) if !pretend? && exists?
58
+ given_destination
59
+ end
60
+
61
+ protected
62
+
63
+ # Shortcut for pretend.
64
+ #
65
+ def pretend?
66
+ base.options[:pretend]
67
+ end
68
+
69
+ # Sets the absolute destination value from a relative destination value.
70
+ # It also stores the given and relative destination. Let's suppose our
71
+ # script is being executed on "dest", it sets the destination root to
72
+ # "dest". The destination, given_destination and relative_destination
73
+ # are related in the following way:
74
+ #
75
+ # inside "bar" do
76
+ # empty_directory "baz"
77
+ # end
78
+ #
79
+ # destination #=> dest/bar/baz
80
+ # relative_destination #=> bar/baz
81
+ # given_destination #=> baz
82
+ #
83
+ def destination=(destination)
84
+ if destination
85
+ @given_destination = convert_encoded_instructions(destination.to_s)
86
+ @destination = ::File.expand_path(@given_destination, base.current_destination)
87
+ @relative_destination = base.relative_to_original_destination_root(@destination)
88
+ end
89
+ end
90
+
91
+ # Filenames in the encoded form are converted. If you have a file:
92
+ #
93
+ # %file_name%.rb
94
+ #
95
+ # It calls #file_name from the base and replaces %-string with the
96
+ # return value (should be String) of #file_name:
97
+ #
98
+ # user.rb
99
+ #
100
+ # The method referenced by %-string SHOULD be public. Otherwise you
101
+ # get the exception with the corresponding error message.
102
+ #
103
+ def convert_encoded_instructions(filename)
104
+ filename.gsub(/%(.*?)%/) do |initial_string|
105
+ call_public_method($1.strip) or initial_string
106
+ end
107
+ end
108
+
109
+ #TODO: base.public_send(sym) rescue nil instead?
110
+
111
+ # Calls `base`'s public method `sym`.
112
+ # Returns:: result of `base.sym` or `nil` if `sym` wasn't found in
113
+ # `base`
114
+ # Raises:: NoMethodError if `sym` references
115
+ # a private method.
116
+ def call_public_method(sym)
117
+ if base.respond_to?(sym)
118
+ base.send(sym)
119
+ elsif base.respond_to?(sym, true)
120
+ raise NoMethodError,
121
+ "Method #{base.class}##{sym} should be public, not private"
122
+ else
123
+ nil
124
+ end
125
+ end
126
+
127
+ # Receives a hash of options and just execute the block if some
128
+ # conditions are met.
129
+ #
130
+ def invoke_with_conflict_check(&block)
131
+ if exists?
132
+ on_conflict_behavior(&block)
133
+ else
134
+ say_status :create, :green
135
+ block.call unless pretend?
136
+ end
137
+
138
+ destination
139
+ end
140
+
141
+ # What to do when the destination file already exists.
142
+ #
143
+ def on_conflict_behavior(&block)
144
+ say_status :exist, :blue
145
+ end
146
+
147
+ # Shortcut to say_status shell method.
148
+ #
149
+ def say_status(status, color)
150
+ base.shell.say_status status, relative_destination, color if config[:verbose]
151
+ end
152
+
153
+ end
154
+ end
155
+ end