ing 0.1.2 → 0.1.5

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -39,7 +39,7 @@ simplified to
39
39
 
40
40
  The "subcommand" is your task. To take some examples.
41
41
 
42
- ing -r./path/to/some/task.rb some:task run something --verbose
42
+ ing -r ./path/to/some/task.rb some:task run something --verbose
43
43
 
44
44
  1. `ing -r` loads specified ruby files or libraries/gems; then
45
45
  2. it dispatches to `Some::Task.new(:verbose => true).run("something")`.
@@ -53,7 +53,7 @@ By default, it requires a file `./ing.rb` if it exists (the equivalent of
53
53
  Rakefile or Thorfile). In which case, assuming your task class is
54
54
  defined or loaded from there, the command can be simply
55
55
 
56
- ing some:task run --verbose
56
+ ing some:task run something --verbose
57
57
 
58
58
  ### Built-in commands
59
59
 
@@ -129,8 +129,8 @@ end
129
129
 
130
130
  As you can see, the second arg corresponds to the method name. `call` is what
131
131
  gets called when there is no second arg. Organizing the methods like this means
132
- you can also do `ing test type unit`: extra non-option arguments are passed into
133
- the method as parameters.
132
+ you can also do `ing test type custom`: extra non-option arguments are passed
133
+ into the method as parameters.
134
134
 
135
135
  For more worked examples of ing tasks, see the
136
136
  [examples](ing/blob/master/examples) directory.
@@ -140,15 +140,18 @@ For more worked examples of ing tasks, see the
140
140
  ### Option arguments
141
141
 
142
142
  Your tasks (ing subcommands) can specify what options they take by defining a
143
- class method `specify_options`. The best way to understand how this is done is
144
- by example:
143
+ class method `specify_options`. For example:
145
144
 
146
145
  ```ruby
147
146
  class Cleanup
148
147
 
149
- def self.specify_options(expect)
150
- expect.opt :quiet, "Run silently"
151
- expect.opt :path, "Path to clean up", :type => :string, :default => '.'
148
+ def self.specify_options(spec)
149
+ spec.text "Clean up your path"
150
+ spec.text "\nUsage:"
151
+ spec.text "ing cleanup [OPTIONS]"
152
+ spec.text "\nOptions:"
153
+ spec.opt :quiet, "Run silently"
154
+ spec.opt :path, "Path to clean up", :type => :string, :default => '.'
152
155
  end
153
156
 
154
157
  attr_accessor :options
@@ -163,12 +166,46 @@ end
163
166
 
164
167
  The syntax used in `self.specify_options` is Trollop - in fact what you are
165
168
  doing is building a `Trollop::Parser` which then emits the parsed options into
166
- your constructor. In general your constructor should just save the options to
169
+ your constructor.
170
+
171
+ In general your constructor should just save the options to
167
172
  an instance variable like this, but in some cases you might want to do further
168
173
  processing of the passed options.
169
174
 
170
175
  [MORE](ing/blob/master/OPTIONS.md)
171
176
 
177
+ ### Using the Task base class
178
+
179
+ To save some boilerplate, and to allow more flexible options specification,
180
+ as well as a few more conveniences, you can inherit from `Ing::Task` and
181
+ rewrite this example as:
182
+
183
+ ```ruby
184
+ class Cleanup < Ing::Task
185
+ desc "Clean up your path"
186
+ usage "ing cleanup [OPTIONS]"
187
+ opt :quiet, "Run silently"
188
+ opt :path, "Path to clean up", :type => :string, :default => '.'
189
+
190
+ # ...
191
+ end
192
+ ```
193
+
194
+ This gives you a slightly more automated help message, with the description
195
+ lines followed by usage followed by options, and with headers for each section.
196
+
197
+ `Ing::Task` also lets you inherit options. Say you have another task:
198
+
199
+ ```ruby
200
+ class BigCleanup < Cleanup
201
+ opt :servers, "On servers", :type => :string, :multi => true
202
+ end
203
+ ```
204
+
205
+ This task will have the two options from its superclass as well as its own.
206
+ (Note the description and usage lines are _not_ inherited this way, only the
207
+ options).
208
+
172
209
  ### Generator tasks
173
210
 
174
211
  If you want to use Thor-ish generator methods, your task classes need a few more
@@ -177,7 +214,7 @@ things added to their interface. Basically, it should look something like this.
177
214
  ```ruby
178
215
  class MyGenerator
179
216
 
180
- def self.specify_options(expect)
217
+ def self.specify_options(spec)
181
218
  # ...
182
219
  end
183
220
 
@@ -207,6 +244,11 @@ The generator methods need `:destination_root`, `:source_root`, and `:shell`.
207
244
  Also, `include Ing::Files` _after_ you specify any options (this is because
208
245
  `Ing::Files` adds several options automatically).
209
246
 
247
+ If you prefer, you can inherit from `Ing::Generator`, which gives you all of
248
+ the above defaults, plus the functionality of `Ing::Task`.
249
+
250
+ Like `Ing::Task`, `Ing::Generator` is simply a convenience for common scenarios.
251
+
210
252
  [MORE](ing/blob/master/GENERATORS.md)
211
253
 
212
254
  ## Motivation
data/lib/ing.rb CHANGED
@@ -7,6 +7,8 @@
7
7
  'ing/common_options',
8
8
  'ing/boot',
9
9
  'ing/files',
10
+ 'ing/task',
11
+ 'ing/generator',
10
12
  'ing/commands/implicit',
11
13
  'ing/commands/list',
12
14
  'ing/commands/help',
@@ -1,10 +1,11 @@
1
- # Base implementation of boot dispatch
2
- # Mixed in to Commands::Implicit, Commands::Generate
3
- # Note this does NOT provide any options, only provides implementation.
4
- # Assumes target class will provide +namespace+ option, otherwise defaults to
5
- # global namespace (::Object).
6
- #
7
- module Ing
1
+ module Ing
2
+
3
+ # Base implementation of boot dispatch
4
+ # Mixed in to Commands::Implicit, Commands::Generate
5
+ # Note this does NOT provide any options, only provides implementation.
6
+ # Assumes target class will provide +namespace+ option, otherwise defaults to
7
+ # global namespace (::Object).
8
+ #
8
9
  module Boot
9
10
 
10
11
  # Configure the command prior to dispatch.
@@ -1,5 +1,6 @@
1
- # Common options for built-in Ing commands
2
- module Ing
1
+ module Ing
2
+
3
+ # Common options for built-in Ing commands
3
4
  module CommonOptions
4
5
 
5
6
  # a bit of trickiness to change a singleton method...
@@ -3,6 +3,17 @@ require 'set'
3
3
 
4
4
  module Ing
5
5
 
6
+ # Generic router for ing commands (both built-in and user-defined).
7
+ # Resolves class, parses options with Trollop if target class
8
+ # defines +specify_options+, then dispatches like (simplifying):
9
+ #
10
+ # Target.new(options).send(*args)
11
+ #
12
+ # if the target is class-like, i.e. responds to +new+. Otherwise it dispatches
13
+ # to the target as a callable:
14
+ #
15
+ # Target.call(*args, options)
16
+ #
6
17
  class Dispatcher
7
18
 
8
19
  # Global set of dispatched commands as [dispatch_class, dispatch_meth],
@@ -35,7 +46,6 @@ module Ing
35
46
  ns = Util.namespaced_const_get(namespaces)
36
47
  self.dispatch_class = Util.namespaced_const_get(classes, ns)
37
48
  self.dispatch_meth = extract_method!(args, dispatch_class)
38
- self.options = parse_options!(args, dispatch_class) || {}
39
49
  self.args = args
40
50
  @invoking = false
41
51
  end
@@ -92,6 +102,7 @@ module Ing
92
102
  end
93
103
 
94
104
  def execute
105
+ self.options = parse_options!(args, dispatch_class) || {}
95
106
  if dispatch_class.respond_to?(:new)
96
107
  cmd = dispatch_class.new(options)
97
108
  yield cmd if block_given?
@@ -32,12 +32,17 @@ require File.expand_path('actions/file_manipulation', File.dirname(__FILE__))
32
32
  require File.expand_path('actions/inject_into_file', File.dirname(__FILE__))
33
33
 
34
34
 
35
- # Interface with base class:
36
- # - attr_reader :source_root, :destination_root
37
- # - attr_reader :shell, :options
38
- # - self.specify_options (optional; adds to it if defined)
39
35
  module Ing
40
36
 
37
+ # Provides filesystem actions using the same interface as Thor::Actions
38
+ #
39
+ # The target class must provide at least:
40
+ # attr_reader :source_root, :destination_root
41
+ # attr_reader :shell, :options
42
+ #
43
+ # Adds to target class options:
44
+ # verbose, force, pretend, revoke, quiet, skip.
45
+ #
41
46
  module Files
42
47
 
43
48
  # a bit of trickiness to change a singleton method...
@@ -0,0 +1,26 @@
1
+ module Ing
2
+ class Generator < Task
3
+
4
+ opt :dest, "Destination root", :type => :string
5
+ opt :source, "Source root", :type => :string
6
+
7
+ include Ing::Files
8
+
9
+ # Destination root for filesystem actions
10
+ def destination_root
11
+ File.expand_path(options[:dest])
12
+ end
13
+
14
+ # Source root for filesystem actions
15
+ def source_root
16
+ File.expand_path(options[:source])
17
+ end
18
+
19
+ def initialize(options)
20
+ super
21
+ validate_option_exists :dest, 'destination_root'
22
+ validate_option_exists :source, 'source_root'
23
+ end
24
+
25
+ end
26
+ end
@@ -1,4 +1,27 @@
1
- require 'fileutils'
1
+ #
2
+ # Copyright (c) 2008 Yehuda Katz, Eric Hodel, et al.
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+ #
23
+
24
+ require 'fileutils'
2
25
  require 'tempfile'
3
26
 
4
27
  module Ing
@@ -0,0 +1,176 @@
1
+ 
2
+ module Ing
3
+
4
+ # A base class to simplify typical task use-cases.
5
+ # Adds some class methods and state to allow inherited options/flexibly-
6
+ # ordered option specification.
7
+ # Note that options are inherited to subclasses, but description and usage
8
+ # lines are not.
9
+ #
10
+ class Task
11
+
12
+ class << self
13
+
14
+ def inherited(subclass)
15
+ subclass.set_options self.options.dup
16
+ end
17
+
18
+ # Modify the option named +name+ according to +specs+ (Hash).
19
+ # Option will be created if it doesn't exist.
20
+ #
21
+ # Example:
22
+ #
23
+ # modify_option :file, :required => true
24
+ #
25
+ def modify_option(name, specs)
26
+ opt(name) unless options[name]
27
+ options[name].opts.merge!(specs)
28
+ end
29
+
30
+ # Modify the default for option +name+ to +val+.
31
+ # Option will be created if it doesn't exist.
32
+ def default(name, val)
33
+ modify_option name, {:default => val}
34
+ end
35
+
36
+ # Add a description line
37
+ def desc(line="")
38
+ desc_lines << line
39
+ end
40
+ alias description desc
41
+
42
+ # Add a usage line
43
+ def usage(line="")
44
+ usage_lines << line
45
+ end
46
+
47
+ # Add an option. Note the syntax is identical to +Trollop::Parser#opt+
48
+ def opt(name, desc="", settings={})
49
+ options[name] = Option.new(name, desc, settings)
50
+ end
51
+ alias option opt
52
+
53
+ # Build option parser based on desc, usage, and options (including
54
+ # inherited options). This method is called by `Ing::Dispatcher`.
55
+ #
56
+ def specify_options(parser)
57
+ desc_lines.each do |line|
58
+ parser.text line
59
+ end
60
+ unless usage_lines.empty?
61
+ parser.text "\nUsage:"
62
+ usage_lines.each do |line|
63
+ parser.text line
64
+ end
65
+ end
66
+ unless options.empty?
67
+ parser.text "\nOptions:"
68
+ options.each do |name, opt|
69
+ parser.opt *opt.to_args
70
+ end
71
+ end
72
+ end
73
+
74
+ # Description lines
75
+ def desc_lines
76
+ @desc_lines ||= []
77
+ end
78
+
79
+ # Usage lines
80
+ def usage_lines
81
+ @usage_lines ||= []
82
+ end
83
+
84
+ # Options hash. Note that in a subclass, options are copied down from
85
+ # superclass.
86
+ def options
87
+ @options ||= {}
88
+ end
89
+
90
+ protected
91
+
92
+ def set_options(o) #:nodoc:
93
+ @options = o
94
+ end
95
+
96
+ end
97
+
98
+ attr_accessor :options, :shell
99
+ def initialize(options)
100
+ self.options = initial_options(options)
101
+ end
102
+
103
+ # Override in subclass for adjusting given options on initialization
104
+ def initial_options(given)
105
+ given
106
+ end
107
+
108
+ # Use in initialization for option validation (post-parsing).
109
+ #
110
+ # Example:
111
+ #
112
+ # validate_option(:color, "Color must be :black or :white") do |actual|
113
+ # [:black, :white].include?(actual)
114
+ # end
115
+ #
116
+ def validate_option(opt, desc=opt, msg=nil)
117
+ msg ||= "Error in option #{desc} for `#{self.class}`."
118
+ !!yield(self.options[opt]) or raise ArgumentError, msg
119
+ end
120
+
121
+ # Validate that the option was passed or otherwise defaulted to something truthy.
122
+ # Note that in most cases, instead you should set :required => true on the option
123
+ # and let Trollop catch the error -- rather than catching it post-parsing.
124
+ #
125
+ # Note +validate_option_exists+ will raise an error if the option is passed
126
+ # but false or nil, unlike the Trollop parser.
127
+ #
128
+ def validate_option_exists(opt, desc=opt)
129
+ msg = "No #{desc} specified for #{self.class}. You must either " +
130
+ "specify a `--#{opt}` option or set a default in #{self.class} or " +
131
+ "in its superclass(es)."
132
+ validate_option(opt, desc, msg) {|val| val }
133
+ end
134
+
135
+ end
136
+
137
+ class Option < Struct.new(:name, :desc, :opts)
138
+
139
+ def initialize(*args)
140
+ super
141
+ self.opts ||= {}
142
+ end
143
+
144
+ def default; opts[:default]; end
145
+ def default=(val)
146
+ opts[:default] = val
147
+ end
148
+
149
+ def type; opts[:type]; end
150
+ def type=(val)
151
+ opts[:type] = val
152
+ end
153
+
154
+ def multi; opts[:multi]; end
155
+ def multi=(val)
156
+ opts[:multi] = val
157
+ end
158
+
159
+ def long; opts[:long]; end
160
+ def long=(val)
161
+ opts[:long] = val
162
+ end
163
+
164
+ def short; opts[:short]; end
165
+ def short=(val)
166
+ opts[:short] = val
167
+ end
168
+
169
+ def to_args
170
+ [name, desc, opts]
171
+ end
172
+
173
+ end
174
+
175
+ end
176
+
@@ -1,3 +1,3 @@
1
1
  module Ing
2
- VERSION = '0.1.2'
2
+ VERSION = '0.1.5'
3
3
  end
@@ -0,0 +1,54 @@
1
+ require File.expand_path('../test_helper', File.dirname(__FILE__))
2
+
3
+ describe Ing::Task do
4
+ include TestHelpers
5
+
6
+ def capture_help(args)
7
+ capture(:stdout) { Ing.run ["help", "-n", "object"] + args }
8
+ end
9
+
10
+ def capture_run(args)
11
+ capture(:stdout) { Ing.run args }
12
+ end
13
+
14
+ describe "single inheritance" do
15
+
16
+ subject { ["simple_task"] }
17
+
18
+ it "help should display the description, followed by usage, followed by options" do
19
+ lines = capture_help(subject).split("\n")
20
+ assert_equal 9, lines.length
21
+ assert_equal "My great task of immense importance", lines[0]
22
+ assert_equal "A simple example of a task using Ing::Task", lines[1]
23
+ assert_empty lines[2]
24
+ assert_equal "Usage:", lines[3]
25
+ assert_equal " ing simple_task [OPTIONS]", lines[4]
26
+ assert_empty lines[5]
27
+ assert_equal "Options:", lines[6]
28
+ assert_match /--fast/, lines[7]
29
+ assert_match /--altitude/, lines[8]
30
+ end
31
+
32
+ end
33
+
34
+ describe "double inheritance" do
35
+
36
+ subject { ["big_task"] }
37
+
38
+ it "help should display all the options defined by the task and its superclass" do
39
+ output = capture_help(subject)
40
+ assert_match(/^\s*--fast/, output)
41
+ assert_match(/^\s*--altitude/, output)
42
+ assert_match(/^\s*--yards/, output)
43
+ assert_match(/^\s*--color/, output)
44
+ end
45
+
46
+ it "run should reflect modifications to superclass options" do
47
+ output = capture_help(subject)
48
+ assert_match(/^\s*--fast.+\(default: true\)/, output)
49
+ assert_match(/^\s*--altitude\, -l.+\(default: 2500\)/, output)
50
+ end
51
+
52
+ end
53
+
54
+ end
@@ -34,3 +34,29 @@ class Amazing
34
34
 
35
35
  end
36
36
 
37
+
38
+
39
+ class SimpleTask < Ing::Task
40
+
41
+ desc "My great task of immense importance"
42
+ desc "A simple example of a task using Ing::Task"
43
+ usage " ing simple_task [OPTIONS]"
44
+ opt :fast, "Run it at fast speed"
45
+ opt :altitude, "Start altitude", :type => :integer
46
+
47
+ def call
48
+ # ....
49
+ end
50
+
51
+ end
52
+
53
+ class BigTask < SimpleTask
54
+
55
+ desc "Even bigger!"
56
+ opt :yards, "Yards of fishing line given", :type => :integer, :default => 25
57
+ opt :color, "Color of cloth", :type => :string, :default => 'green'
58
+
59
+ default :fast, true
60
+ modify_option :altitude, :short => 'l', :default => 2500
61
+
62
+ end
data/todo.yml CHANGED
@@ -3,4 +3,3 @@
3
3
  - add Shell::Color
4
4
  - remove global Ing.shell_class, specify via color switch
5
5
  - incorporate gsub_file patch, patches to Shell
6
- - add a generator base class for the common use-case
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ing
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.1.5
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,11 +9,11 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-09-16 00:00:00.000000000Z
12
+ date: 2012-09-17 00:00:00.000000000Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: minitest
16
- requirement: &17352160 !ruby/object:Gem::Requirement
16
+ requirement: &14733000 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ! '>='
@@ -21,10 +21,10 @@ dependencies:
21
21
  version: '0'
22
22
  type: :development
23
23
  prerelease: false
24
- version_requirements: *17352160
24
+ version_requirements: *14733000
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: fakeweb
27
- requirement: &17351600 !ruby/object:Gem::Requirement
27
+ requirement: &14677360 !ruby/object:Gem::Requirement
28
28
  none: false
29
29
  requirements:
30
30
  - - ! '>='
@@ -32,7 +32,7 @@ dependencies:
32
32
  version: '0'
33
33
  type: :development
34
34
  prerelease: false
35
- version_requirements: *17351600
35
+ version_requirements: *14677360
36
36
  description: ! "\nAn alternative to Rake and Thor, Ing has a command-line syntax similar
37
37
  to \nThor's, and it incorporates Thor's (Rails') generator methods and shell \nconventions.
38
38
  But unlike Thor or Rake, it does not define its own DSL. Your tasks\ncorrespond
@@ -70,14 +70,17 @@ files:
70
70
  - lib/ing/common_options.rb
71
71
  - lib/ing/dispatcher.rb
72
72
  - lib/ing/files.rb
73
+ - lib/ing/generator.rb
73
74
  - lib/ing/lib_trollop.rb
74
75
  - lib/ing/shell.rb
76
+ - lib/ing/task.rb
75
77
  - lib/ing/trollop/parser.rb
76
78
  - lib/ing/util.rb
77
79
  - lib/ing/version.rb
78
80
  - lib/thor/actions/file_manipulation.rb
79
81
  - lib/thor/shell/basic.rb
80
82
  - test/acceptance/ing_run_tests.rb
83
+ - test/acceptance/ing_task_tests.rb
81
84
  - test/actions/create_file_spec.rb
82
85
  - test/actions/create_link_spec.rb
83
86
  - test/actions/directory_spec.rb
@@ -132,6 +135,7 @@ specification_version: 3
132
135
  summary: Vanilla ruby command-line scripting
133
136
  test_files:
134
137
  - test/acceptance/ing_run_tests.rb
138
+ - test/acceptance/ing_task_tests.rb
135
139
  - test/actions/create_file_spec.rb
136
140
  - test/actions/create_link_spec.rb
137
141
  - test/actions/directory_spec.rb