ing 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +5 -0
- data/GENERATORS.md +2 -0
- data/LICENSE +18 -0
- data/OPTIONS.md +2 -0
- data/README.md +251 -0
- data/TASKS.md +21 -0
- data/bin/ing +5 -0
- data/examples/rspec_convert.rb +102 -0
- data/ing.gemspec +29 -0
- data/ing.rb +102 -0
- data/lib/ing.rb +78 -0
- data/lib/ing/actions/create_file.rb +105 -0
- data/lib/ing/actions/create_link.rb +57 -0
- data/lib/ing/actions/directory.rb +98 -0
- data/lib/ing/actions/empty_directory.rb +155 -0
- data/lib/ing/actions/file_manipulation.rb +308 -0
- data/lib/ing/actions/inject_into_file.rb +109 -0
- data/lib/ing/commands/boot.rb +76 -0
- data/lib/ing/commands/generate.rb +64 -0
- data/lib/ing/commands/help.rb +87 -0
- data/lib/ing/commands/implicit.rb +59 -0
- data/lib/ing/commands/list.rb +108 -0
- data/lib/ing/dispatcher.rb +132 -0
- data/lib/ing/files.rb +190 -0
- data/lib/ing/lib_trollop.rb +782 -0
- data/lib/ing/shell.rb +390 -0
- data/lib/ing/trollop/parser.rb +17 -0
- data/lib/ing/util.rb +61 -0
- data/lib/ing/version.rb +3 -0
- data/lib/thor/actions/file_manipulation.rb +30 -0
- data/lib/thor/shell/basic.rb +44 -0
- data/test/acceptance/ing_run_tests.rb +164 -0
- data/test/actions/create_file_spec.rb +209 -0
- data/test/actions/create_link_spec.rb +90 -0
- data/test/actions/directory_spec.rb +167 -0
- data/test/actions/empty_directory_spec.rb +146 -0
- data/test/actions/file_manipulation_spec.rb +433 -0
- data/test/actions/inject_into_file_spec.rb +147 -0
- data/test/fixtures/application.rb +2 -0
- data/test/fixtures/app{1}/README +3 -0
- data/test/fixtures/bundle/execute.rb +6 -0
- data/test/fixtures/bundle/main.thor +1 -0
- data/test/fixtures/doc/%file_name%.rb.tt +1 -0
- data/test/fixtures/doc/COMMENTER +10 -0
- data/test/fixtures/doc/README +3 -0
- data/test/fixtures/doc/block_helper.rb +3 -0
- data/test/fixtures/doc/components/.empty_directory +0 -0
- data/test/fixtures/doc/config.rb +1 -0
- data/test/fixtures/doc/config.yaml.tt +1 -0
- data/test/fixtures/group.ing.rb +76 -0
- data/test/fixtures/invok.ing.rb +50 -0
- data/test/fixtures/namespace.ing.rb +52 -0
- data/test/fixtures/require.ing.rb +7 -0
- data/test/fixtures/task.ing.rb +36 -0
- data/test/fixtures/task.thor +10 -0
- data/test/spec_helper.rb +2 -0
- data/test/test_helper.rb +41 -0
- data/todo.yml +7 -0
- metadata +147 -0
data/.gitignore
ADDED
data/GENERATORS.md
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
Copyright (c) 2012 Eric Gjertsen
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
of this software and associated documentation files (the "Software"), to
|
5
|
+
deal in the Software without restriction, including without limitation the
|
6
|
+
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
7
|
+
sell copies of the Software, and to permit persons to whom the Software is
|
8
|
+
furnished to do so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
11
|
+
all copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
16
|
+
THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
17
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
18
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/OPTIONS.md
ADDED
data/README.md
ADDED
@@ -0,0 +1,251 @@
|
|
1
|
+
# Ing
|
2
|
+
## Vanilla ruby command-line scripting.
|
3
|
+
|
4
|
+
or gratuitous backronym: <b>I</b> <b>N</b>eed a <b>G</b>enerator!
|
5
|
+
|
6
|
+
Note this is a work-in-progress, not quite ready for use.
|
7
|
+
|
8
|
+
The command-line syntax is similar to Thor's, and it incorporates Thor's
|
9
|
+
(Rails') generator methods and shell conventions like
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
if yes? 'process foo files?', :yellow
|
13
|
+
inside('foo') { create_file '%foo_file%.rb' }
|
14
|
+
end
|
15
|
+
```
|
16
|
+
but _unlike_ Thor or Rake, it does not define its own DSL. Your tasks correspond
|
17
|
+
to plain ruby classes and methods. Ing just handles routing from the command line
|
18
|
+
to them, and setting options. Your classes (or even Procs) do the rest.
|
19
|
+
|
20
|
+
Option parsing courtesy of the venerable and excellent
|
21
|
+
[Trollop](http://trollop.rubyforge.org/), under the hood.
|
22
|
+
|
23
|
+
## Installation
|
24
|
+
|
25
|
+
_Note: not yet gemified_
|
26
|
+
|
27
|
+
gem install ing
|
28
|
+
|
29
|
+
## A quick tour
|
30
|
+
|
31
|
+
### The command line
|
32
|
+
|
33
|
+
The ing command line is generally parsed as
|
34
|
+
|
35
|
+
[ing command] [ing command options] [subcommand] [args] [subcommand options]
|
36
|
+
|
37
|
+
But in cases where the first argument isn't an built-in ing command or options, it's
|
38
|
+
simplified to
|
39
|
+
|
40
|
+
[subcommand] [args] [subcommand options]
|
41
|
+
|
42
|
+
The "subcommand" is your task. To take some examples.
|
43
|
+
|
44
|
+
ing -r./path/to/some/task.rb some:task run something --verbose
|
45
|
+
|
46
|
+
1. `ing -r` loads specified ruby files or libraries/gems; then
|
47
|
+
2. it dispatches to `Some::Task.new(:verbose => true).run("something")`.
|
48
|
+
|
49
|
+
(Assuming you define a task `Some::Task#run`, in `/path/to/some/task.rb`.)
|
50
|
+
|
51
|
+
You can -r as many libaries/files as you like. Of course, that gets pretty
|
52
|
+
long-winded.
|
53
|
+
|
54
|
+
By default, it requires a file `./ing.rb` if it exists (the equivalent of
|
55
|
+
Rakefile or Thorfile). In which case, assuming your task class is
|
56
|
+
defined or loaded from there, the command can be simply
|
57
|
+
|
58
|
+
ing some:task run --verbose
|
59
|
+
|
60
|
+
### Built-in commands
|
61
|
+
|
62
|
+
Ing has some built-in commands. These are still being implemented, but
|
63
|
+
you can see what they are so far with `ing list`.
|
64
|
+
|
65
|
+
The most significant subcommand is `generate` or `g`, which
|
66
|
+
simplifies a common and familiar use-case (at the expense of some file-
|
67
|
+
system conventions):
|
68
|
+
|
69
|
+
ing generate some:task --force
|
70
|
+
|
71
|
+
Unlike Thor/Rails generators, these don't need to be packaged up as gems
|
72
|
+
and preloaded into ruby. They can be either parsed as:
|
73
|
+
|
74
|
+
1. A __file__ relative to a root dir: e.g. __some/task__, or
|
75
|
+
2. A __subdirectory__ of the root dir, in which case it attempts to
|
76
|
+
preload `ing.rb` within that subdirectory: e.g. __some/task/ing.rb__
|
77
|
+
|
78
|
+
The command above is then dispatched as normal to
|
79
|
+
`Some::Task.new(:force => true).call` (`#call` is used if no method is
|
80
|
+
specified). So you should put the task code within that namespace in the
|
81
|
+
preloaded file.
|
82
|
+
|
83
|
+
(By default, the generator root directory is specified by
|
84
|
+
`ENV['ING_GENERATORS_ROOT']` or failing that, `~/.ing/generators`.)
|
85
|
+
|
86
|
+
_TODO: more examples needed_
|
87
|
+
|
88
|
+
### A simple example of a plain old ruby task
|
89
|
+
|
90
|
+
Let's say you want to run your project's tests with a command like `ing test`.
|
91
|
+
The default is to run the whole suite; but if you just want unit tests you can
|
92
|
+
say `ing test unit`. This is what it would look like (in `./ing.rb`):
|
93
|
+
|
94
|
+
```ruby
|
95
|
+
class Test
|
96
|
+
|
97
|
+
# no options passed, but you need the constructor
|
98
|
+
def initialize(options); end
|
99
|
+
|
100
|
+
# `ing test`
|
101
|
+
def call(*args)
|
102
|
+
suite
|
103
|
+
end
|
104
|
+
|
105
|
+
# `ing test suite`
|
106
|
+
def suite
|
107
|
+
unit; functional; acceptance
|
108
|
+
end
|
109
|
+
|
110
|
+
# `ing test unit`
|
111
|
+
def unit
|
112
|
+
type 'unit'
|
113
|
+
end
|
114
|
+
|
115
|
+
# `ing test functional`
|
116
|
+
def functional
|
117
|
+
type 'functional'
|
118
|
+
end
|
119
|
+
|
120
|
+
# `ing test acceptance`
|
121
|
+
def acceptance
|
122
|
+
type 'acceptance'
|
123
|
+
end
|
124
|
+
|
125
|
+
def type(dir)
|
126
|
+
Dir["./test/#{dir}/*.rb"].each { |f| require_relative f }
|
127
|
+
end
|
128
|
+
|
129
|
+
end
|
130
|
+
```
|
131
|
+
|
132
|
+
As you can see, the second arg corresponds to the method name. `call` is what
|
133
|
+
gets called when there is no second arg. Organizing the methods like this means
|
134
|
+
you can also do `ing test type unit`: extra non-option arguments are passed into
|
135
|
+
the method as parameters.
|
136
|
+
|
137
|
+
For more worked examples of ing tasks, see the
|
138
|
+
[examples](ing/blob/master/examples) directory.
|
139
|
+
|
140
|
+
[MORE](ing/blob/master/TASKS.md)
|
141
|
+
|
142
|
+
### Option arguments
|
143
|
+
|
144
|
+
Your tasks (ing subcommands) can specify what options they take by defining a
|
145
|
+
class method `specify_options`. The best way to understand how this is done is
|
146
|
+
by example:
|
147
|
+
|
148
|
+
```ruby
|
149
|
+
class Cleanup
|
150
|
+
|
151
|
+
def self.specify_options(expect)
|
152
|
+
expect.opt :quiet, "Run silently"
|
153
|
+
expect.opt :path, "Path to clean up", :type => :string, :default => '.'
|
154
|
+
end
|
155
|
+
|
156
|
+
attr_accessor :options
|
157
|
+
|
158
|
+
def initialize(options)
|
159
|
+
self.options = options
|
160
|
+
end
|
161
|
+
|
162
|
+
# ...
|
163
|
+
end
|
164
|
+
```
|
165
|
+
|
166
|
+
The syntax used in `self.specify_options` is Trollop - in fact what you are
|
167
|
+
doing is building a `Trollop::Parser` which then emits the parsed options into
|
168
|
+
your constructor. In general your constructor should just save the options to
|
169
|
+
an instance variable like this, but in some cases you might want to do further
|
170
|
+
processing of the passed options.
|
171
|
+
|
172
|
+
[MORE](ing/blob/master/OPTIONS.md)
|
173
|
+
|
174
|
+
### Generator tasks
|
175
|
+
|
176
|
+
If you want to use Thor-ish generator methods, your task classes need a few more
|
177
|
+
things added to their interface. Basically, it should look something like this.
|
178
|
+
|
179
|
+
```ruby
|
180
|
+
class MyGenerator
|
181
|
+
|
182
|
+
def self.specify_options(expect)
|
183
|
+
# ...
|
184
|
+
end
|
185
|
+
|
186
|
+
include Ing::Files
|
187
|
+
|
188
|
+
attr_accessor :destination_root, :source_root, :options, :shell
|
189
|
+
|
190
|
+
# default == execution from within your project directory
|
191
|
+
def destination_root
|
192
|
+
@destination_root ||= Dir.pwd
|
193
|
+
end
|
194
|
+
|
195
|
+
# default == current file is within root directory of generator files
|
196
|
+
def source_root
|
197
|
+
@source_root ||= File.expand_path(File.dirname(__FILE__))
|
198
|
+
end
|
199
|
+
|
200
|
+
def initialize(options)
|
201
|
+
self.options = options
|
202
|
+
end
|
203
|
+
|
204
|
+
# ...
|
205
|
+
end
|
206
|
+
```
|
207
|
+
|
208
|
+
The generator methods need `:destination_root`, `:source_root`, and `:shell`.
|
209
|
+
Also, `include Ing::Files` _after_ you specify any options (this is because
|
210
|
+
`Ing::Files` adds several options automatically).
|
211
|
+
|
212
|
+
[MORE](ing/blob/master/GENERATORS.md)
|
213
|
+
|
214
|
+
## Motivation
|
215
|
+
|
216
|
+
I wanted to use Thor's generator methods and shell conventions to write my own
|
217
|
+
generators. But I didn't want to fight against Thor's hijacking of ruby classes.
|
218
|
+
|
219
|
+
### Brief note about the design
|
220
|
+
|
221
|
+
One of the design principles is to limit inheritance (classical and mixin), and
|
222
|
+
most importantly to _avoid introducing new state via inheritance_. An important
|
223
|
+
corollary of this is that the _application objects_, ie. your task classes,
|
224
|
+
must themselves take responsibility for their interface with the underlying
|
225
|
+
resources they mix in or compose, instead of those resources providing the
|
226
|
+
interface (via so-called macro-style class methods, for instance).
|
227
|
+
|
228
|
+
## Q & A
|
229
|
+
|
230
|
+
### But what about task dependency resolution?
|
231
|
+
|
232
|
+
That's what `require` and `||=` are for ;)
|
233
|
+
|
234
|
+
Seriously, you do have `Ing.invoke Some::Task, :some_method` and `Ing.execute ...`
|
235
|
+
for this kind of thing. Personally I think it's a code smell to put reusable
|
236
|
+
code in things that are _also_ run from the command line. Is it application or
|
237
|
+
library code? Controller or model? But `invoke` is there if you must, hopefully
|
238
|
+
with a suitably ugly syntax to dissuade you. :P
|
239
|
+
|
240
|
+
### But what about security?
|
241
|
+
|
242
|
+
Yes, this means any ruby library and even built-in classes can be exercised from
|
243
|
+
the command line... but so what?
|
244
|
+
|
245
|
+
1. You can't run module methods, and the objects you invoke need to have a
|
246
|
+
hash constructor. So Kernel, Process, IO, File, etc. are pretty much ruled out.
|
247
|
+
Most of the ruby built-in classes are ruled out in fact.
|
248
|
+
|
249
|
+
2. More to the point, you're already in a shell with much more dangerous knives
|
250
|
+
lying around. You had better trust the scripts you're working with!
|
251
|
+
|
data/TASKS.md
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
### More about Tasks
|
2
|
+
|
3
|
+
Incidentally, there's nothing stopping you from implementing the "Test" example
|
4
|
+
functionally. It could look (simplifying a little) like this:
|
5
|
+
|
6
|
+
Test = lambda {|arg|
|
7
|
+
type = lambda {|*dirs|
|
8
|
+
dirs.each do |dir|
|
9
|
+
Dir["./test/#{dir}/*.rb"].each { |f| require_relative f }
|
10
|
+
end
|
11
|
+
}
|
12
|
+
suite = lambda { type['unit','functional','acceptance'] }
|
13
|
+
arg ? type[arg] : suite[]
|
14
|
+
}
|
15
|
+
|
16
|
+
Ing can either invoke class instance methods (on things that respond to `new`),
|
17
|
+
or call Procs directly. The advantage of using classes is that some of the
|
18
|
+
argument parsing is done for you (especially true of option arguments as we'll
|
19
|
+
see in a minute). You can use methods to basically model the command line
|
20
|
+
syntax. The advantage of Procs is you have more flexibility in how arguments
|
21
|
+
are interpreted.
|
data/bin/ing
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/ing.gemspec
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
require File.expand_path("lib/ing/version", File.dirname(__FILE__))
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = "ing"
|
5
|
+
s.version = Ing::VERSION
|
6
|
+
s.authors = ["Eric Gjertsen"]
|
7
|
+
s.email = ["ericgj72@gmail.com"]
|
8
|
+
s.homepage = "https://github.com/ericgj/ing"
|
9
|
+
s.summary = %q{Vanilla ruby command-line scripting}
|
10
|
+
s.description = %q{
|
11
|
+
An alternative to Rake and Thor, Ing has a command-line syntax similar to
|
12
|
+
Thor's, and it incorporates Thor's (Rails') generator methods and shell
|
13
|
+
conventions. But unlike Thor or Rake, it does not define its own DSL. Your tasks
|
14
|
+
correspond to plain ruby classes and methods. Ing just handles routing from the
|
15
|
+
command line to them, and setting options. Your classes (or even Procs) do the
|
16
|
+
rest.
|
17
|
+
}
|
18
|
+
|
19
|
+
s.rubyforge_project = ""
|
20
|
+
|
21
|
+
s.files = `git ls-files`.split("\n")
|
22
|
+
s.test_files = `git ls-files -- test/*`.split("\n")
|
23
|
+
s.require_paths = ["lib"]
|
24
|
+
s.executables << 'ing'
|
25
|
+
|
26
|
+
s.requirements << "ruby >= 1.9"
|
27
|
+
|
28
|
+
s.add_development_dependency 'fakeweb'
|
29
|
+
end
|