caty 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/HISTORY.markdown +7 -0
- data/LICENSE.txt +22 -0
- data/README.markdown +92 -0
- data/Rakefile +33 -0
- data/VERSION +1 -0
- data/caty.gemspec +73 -0
- data/lib/caty.rb +341 -0
- data/lib/caty/converters.rb +101 -0
- data/lib/caty/errors.rb +17 -0
- data/lib/caty/global_option.rb +40 -0
- data/lib/caty/has_description.rb +26 -0
- data/lib/caty/help_system.rb +107 -0
- data/lib/caty/helpers.rb +19 -0
- data/lib/caty/indirection.rb +26 -0
- data/lib/caty/option.rb +100 -0
- data/lib/caty/option_array.rb +30 -0
- data/lib/caty/option_constructor.rb +89 -0
- data/lib/caty/task.rb +74 -0
- data/lib/caty/task_hash.rb +75 -0
- data/test/kitty.rb +87 -0
- data/test/test_caty.rb +100 -0
- data/test/test_option.rb +81 -0
- data/test/test_option_array.rb +25 -0
- data/test/test_structure.rb +72 -0
- data/test/test_task.rb +28 -0
- metadata +94 -0
data/HISTORY.markdown
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
/*---- DON'T PANIC License 1.1 -----------
|
2
|
+
|
3
|
+
Don't panic, this piece of software is
|
4
|
+
free, i.e. you can do with it whatever
|
5
|
+
you like, including, but not limited to:
|
6
|
+
|
7
|
+
* using it
|
8
|
+
* copying it
|
9
|
+
* (re)distributing it
|
10
|
+
* burning/burying/shredding it
|
11
|
+
* eating it
|
12
|
+
* using it to obtain world domination
|
13
|
+
* and ignoring it
|
14
|
+
|
15
|
+
Under the sole condition that you
|
16
|
+
|
17
|
+
* CONSIDER buying the author a strong
|
18
|
+
brownian motion producer, say a nice
|
19
|
+
hot cup of tea, should you ever meet
|
20
|
+
him in person.
|
21
|
+
|
22
|
+
----------------------------------------*/
|
data/README.markdown
ADDED
@@ -0,0 +1,92 @@
|
|
1
|
+
# Caty #
|
2
|
+
|
3
|
+
* http://github.com/karottenreibe/caty
|
4
|
+
|
5
|
+
|
6
|
+
## Description ##
|
7
|
+
|
8
|
+
Caty is a command line parser that operates on the same
|
9
|
+
principle as Yehuda Katz's Thor (http://github.com/wycats/thor/tree/master).
|
10
|
+
It maps the arguments given by the user to instance methods of a class
|
11
|
+
and can automatically parse options.
|
12
|
+
|
13
|
+
|
14
|
+
## Features ##
|
15
|
+
|
16
|
+
* Define tasks that get mapped directly to instance methods of a class
|
17
|
+
* Define global (e.g. --beer) and task-specific (e.g. -rootbeer) options
|
18
|
+
* Have String, Integer and Boolean options
|
19
|
+
* Have options with default values
|
20
|
+
|
21
|
+
|
22
|
+
## Synopsis ##
|
23
|
+
|
24
|
+
require 'rubygems'
|
25
|
+
require 'caty'
|
26
|
+
|
27
|
+
class Kitty < Caty
|
28
|
+
|
29
|
+
global_options do
|
30
|
+
boolean :quiet, false
|
31
|
+
end
|
32
|
+
|
33
|
+
desc('WHAT', cut("
|
34
|
+
Kitty's hungry!
|
35
|
+
You can specify any amount of food to feed the kitty.
|
36
|
+
"))
|
37
|
+
|
38
|
+
task_options :amount => 1
|
39
|
+
|
40
|
+
def eat( something )
|
41
|
+
unless global_options.quiet
|
42
|
+
puts "I'm eating #{something}. I'm #{task_options.speed}!"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
help_task
|
47
|
+
|
48
|
+
end
|
49
|
+
|
50
|
+
Kitty.start!
|
51
|
+
|
52
|
+
|
53
|
+
#
|
54
|
+
# now you can do:
|
55
|
+
# $> kitty eat fish -amount=5 --quiet
|
56
|
+
# $> kitty help
|
57
|
+
# ...
|
58
|
+
#
|
59
|
+
|
60
|
+
|
61
|
+
|
62
|
+
## Install ##
|
63
|
+
|
64
|
+
* sudo gem install karottenreibe-caty --source http://gems.github.com
|
65
|
+
* sudo gem install caty
|
66
|
+
|
67
|
+
|
68
|
+
## License ##
|
69
|
+
|
70
|
+
/*---- DON'T PANIC License 1.1 -----------
|
71
|
+
|
72
|
+
Don't panic, this piece of software is
|
73
|
+
free, i.e. you can do with it whatever
|
74
|
+
you like, including, but not limited to:
|
75
|
+
|
76
|
+
* using it
|
77
|
+
* copying it
|
78
|
+
* (re)distributing it
|
79
|
+
* burning/burying/shredding it
|
80
|
+
* eating it
|
81
|
+
* using it to obtain world domination
|
82
|
+
* and ignoring it
|
83
|
+
|
84
|
+
Under the sole condition that you
|
85
|
+
|
86
|
+
* CONSIDER buying the author a strong
|
87
|
+
brownian motion producer, say a nice
|
88
|
+
hot cup of tea, should you ever meet
|
89
|
+
him in person.
|
90
|
+
|
91
|
+
----------------------------------------*/
|
92
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'jeweler'
|
2
|
+
|
3
|
+
task :release do
|
4
|
+
sh "vim HISTORY.markdown"
|
5
|
+
sh "vim README.markdown"
|
6
|
+
sh "git commit -a -m 'prerelease adjustments'; true"
|
7
|
+
end
|
8
|
+
|
9
|
+
Jeweler::Tasks.new do |gem|
|
10
|
+
gem.name = 'caty'
|
11
|
+
gem.summary = gem.description =
|
12
|
+
'Caty is a command line parser that maps arguments to instance methods'
|
13
|
+
gem.email = 'karottenreibe@gmail.com'
|
14
|
+
gem.homepage = 'http://github.com/karottenreibe/caty'
|
15
|
+
gem.authors = ['Fabian Streitel']
|
16
|
+
gem.rubyforge_project = 'k-gems'
|
17
|
+
gem.add_dependency('ohash')
|
18
|
+
end
|
19
|
+
|
20
|
+
Jeweler::RubyforgeTasks.new
|
21
|
+
|
22
|
+
require 'rake/rdoctask'
|
23
|
+
Rake::RDocTask.new do |rdoc|
|
24
|
+
rdoc.rdoc_dir = 'rdoc'
|
25
|
+
rdoc.title = 'Caty'
|
26
|
+
rdoc.rdoc_files.include('lib/*.rb')
|
27
|
+
rdoc.rdoc_files.include('lib/*/*.rb')
|
28
|
+
end
|
29
|
+
|
30
|
+
task :test do
|
31
|
+
sh 'bacon -Ilib test/test_*.rb'
|
32
|
+
end
|
33
|
+
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.0.1
|
data/caty.gemspec
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run `rake gemspec`
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{caty}
|
8
|
+
s.version = "0.0.1"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Fabian Streitel"]
|
12
|
+
s.date = %q{2009-09-08}
|
13
|
+
s.description = %q{Caty is a command line parser that maps arguments to instance methods}
|
14
|
+
s.email = %q{karottenreibe@gmail.com}
|
15
|
+
s.extra_rdoc_files = [
|
16
|
+
"LICENSE.txt",
|
17
|
+
"README.markdown"
|
18
|
+
]
|
19
|
+
s.files = [
|
20
|
+
"HISTORY.markdown",
|
21
|
+
"LICENSE.txt",
|
22
|
+
"README.markdown",
|
23
|
+
"Rakefile",
|
24
|
+
"VERSION",
|
25
|
+
"caty.gemspec",
|
26
|
+
"lib/caty.rb",
|
27
|
+
"lib/caty/converters.rb",
|
28
|
+
"lib/caty/errors.rb",
|
29
|
+
"lib/caty/global_option.rb",
|
30
|
+
"lib/caty/has_description.rb",
|
31
|
+
"lib/caty/help_system.rb",
|
32
|
+
"lib/caty/helpers.rb",
|
33
|
+
"lib/caty/indirection.rb",
|
34
|
+
"lib/caty/option.rb",
|
35
|
+
"lib/caty/option_array.rb",
|
36
|
+
"lib/caty/option_constructor.rb",
|
37
|
+
"lib/caty/task.rb",
|
38
|
+
"lib/caty/task_hash.rb",
|
39
|
+
"test/kitty.rb",
|
40
|
+
"test/test_caty.rb",
|
41
|
+
"test/test_option.rb",
|
42
|
+
"test/test_option_array.rb",
|
43
|
+
"test/test_structure.rb",
|
44
|
+
"test/test_task.rb"
|
45
|
+
]
|
46
|
+
s.homepage = %q{http://github.com/karottenreibe/caty}
|
47
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
48
|
+
s.require_paths = ["lib"]
|
49
|
+
s.rubyforge_project = %q{k-gems}
|
50
|
+
s.rubygems_version = %q{1.3.4}
|
51
|
+
s.summary = %q{Caty is a command line parser that maps arguments to instance methods}
|
52
|
+
s.test_files = [
|
53
|
+
"test/kitty.rb",
|
54
|
+
"test/test_caty.rb",
|
55
|
+
"test/test_option.rb",
|
56
|
+
"test/test_option_array.rb",
|
57
|
+
"test/test_structure.rb",
|
58
|
+
"test/test_task.rb"
|
59
|
+
]
|
60
|
+
|
61
|
+
if s.respond_to? :specification_version then
|
62
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
63
|
+
s.specification_version = 3
|
64
|
+
|
65
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
66
|
+
s.add_runtime_dependency(%q<ohash>, [">= 0"])
|
67
|
+
else
|
68
|
+
s.add_dependency(%q<ohash>, [">= 0"])
|
69
|
+
end
|
70
|
+
else
|
71
|
+
s.add_dependency(%q<ohash>, [">= 0"])
|
72
|
+
end
|
73
|
+
end
|
data/lib/caty.rb
ADDED
@@ -0,0 +1,341 @@
|
|
1
|
+
#
|
2
|
+
# Caty is a command line parser that supports tasks as well as
|
3
|
+
# options and maps user input directly to instance methods.
|
4
|
+
#
|
5
|
+
# To find out more about caty, file bug reports and other stuff,
|
6
|
+
# go to http://karottenreibe.github.com/caty.
|
7
|
+
#
|
8
|
+
# Inspired by Thor (http://www.yehudakatz.com/2008/05/12/by-thors-hammer)
|
9
|
+
#
|
10
|
+
# Kudos to Yehuda Katz.
|
11
|
+
#
|
12
|
+
|
13
|
+
#
|
14
|
+
# Handles command line parsing.
|
15
|
+
#
|
16
|
+
# Subclass this and add public methods.
|
17
|
+
# These methods will become tasks, which will be
|
18
|
+
# callable via
|
19
|
+
#
|
20
|
+
# app method_name
|
21
|
+
#
|
22
|
+
# Use the #global_options() and #task_options()
|
23
|
+
# methods to add global or task-specific options.
|
24
|
+
#
|
25
|
+
# Use the ::map() method to create aliases for
|
26
|
+
# tasks.
|
27
|
+
#
|
28
|
+
# Use the ::start!() method to start parsing.
|
29
|
+
#
|
30
|
+
# Use ::default() to set a default task.
|
31
|
+
#
|
32
|
+
# Use ::before() and ::after() to define Rails-style
|
33
|
+
# before and after filters.
|
34
|
+
#
|
35
|
+
class Caty
|
36
|
+
|
37
|
+
#
|
38
|
+
# Returns the options for the called task as
|
39
|
+
# an OpenHash.
|
40
|
+
#
|
41
|
+
attr_accessor :task_options
|
42
|
+
|
43
|
+
#
|
44
|
+
# Returns the global options for this invocation as
|
45
|
+
# an OpenHash.
|
46
|
+
#
|
47
|
+
attr_accessor :global_options
|
48
|
+
|
49
|
+
class << self
|
50
|
+
|
51
|
+
#
|
52
|
+
# Starts command line parsing.
|
53
|
+
#
|
54
|
+
# Subclass.start!( arguments_array )
|
55
|
+
# Subclass.start!
|
56
|
+
#
|
57
|
+
# Returns true on success, false when an ArgumentError
|
58
|
+
# was detected.
|
59
|
+
#
|
60
|
+
def start!( args = ARGV )
|
61
|
+
initialize_instance
|
62
|
+
|
63
|
+
begin
|
64
|
+
caty = self.new
|
65
|
+
caty.global_options = @global_options.grep!(args)
|
66
|
+
|
67
|
+
task_name = args.delete_at(0) || @default
|
68
|
+
raise Caty::NoSuchTaskError, "You need to provide a task" if task_name.nil?
|
69
|
+
|
70
|
+
task = @tasks.resolve(task_name.to_sym)
|
71
|
+
|
72
|
+
if task.nil?
|
73
|
+
raise Caty::NoSuchTaskError, "There is no task named `#{task_name}'"
|
74
|
+
else
|
75
|
+
caty.task_options = task.parse!(args)
|
76
|
+
|
77
|
+
caty.instance_exec(task_name.to_sym, &@before) unless @before.nil?
|
78
|
+
task.execute(caty)
|
79
|
+
caty.instance_exec(task_name.to_sym, &@after) unless @after.nil?
|
80
|
+
end
|
81
|
+
|
82
|
+
return true
|
83
|
+
|
84
|
+
rescue Caty::NoSuchTaskError, Caty::OptionArgumentError => e
|
85
|
+
$stdout.puts e.message
|
86
|
+
return false
|
87
|
+
|
88
|
+
rescue ArgumentError => e
|
89
|
+
# verify that this is actually the task throwing the error
|
90
|
+
if is_task_argument_error(e.backtrace, task_name)
|
91
|
+
$stdout.puts "Bad arguments for task #{task.name}."
|
92
|
+
$stdout.puts "Usage: #{task.to_s}"
|
93
|
+
return false
|
94
|
+
else
|
95
|
+
raise
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
#
|
101
|
+
# Simply class_evals the given block on
|
102
|
+
# your Caty subtype, thus allowing you to
|
103
|
+
# add new tasks in different source files.
|
104
|
+
#
|
105
|
+
# class X < Caty
|
106
|
+
# end
|
107
|
+
#
|
108
|
+
# X.append do
|
109
|
+
# def bar
|
110
|
+
# puts 'bar task'
|
111
|
+
# end
|
112
|
+
# end
|
113
|
+
#
|
114
|
+
# X.start!(%w{bar})
|
115
|
+
#
|
116
|
+
def append( &block )
|
117
|
+
self.class_eval(&block)
|
118
|
+
end
|
119
|
+
|
120
|
+
#
|
121
|
+
# Methods to be used by the inheriting class.
|
122
|
+
#
|
123
|
+
protected
|
124
|
+
|
125
|
+
#
|
126
|
+
# Can be used to cut off whitespace in front of
|
127
|
+
# #desc() descriptions, so it can be written more
|
128
|
+
# nicely in the source.
|
129
|
+
#
|
130
|
+
# Describing this method accurately is terribly complicated,
|
131
|
+
# so here's an example:
|
132
|
+
#
|
133
|
+
# cut("
|
134
|
+
# first
|
135
|
+
# second
|
136
|
+
# third
|
137
|
+
# ")
|
138
|
+
#
|
139
|
+
# produces the string
|
140
|
+
#
|
141
|
+
# "first\n second\nthird"
|
142
|
+
#
|
143
|
+
# Notice the preserved whitespace in front of
|
144
|
+
# 'second'!
|
145
|
+
#
|
146
|
+
def cut( description )
|
147
|
+
description.sub!(%r{\A\n+}, '')
|
148
|
+
description =~ %r{\A([ \t]*)}
|
149
|
+
|
150
|
+
unless $1.nil?
|
151
|
+
space = $1
|
152
|
+
description.gsub!(%r{^#{space}}, '')
|
153
|
+
end
|
154
|
+
|
155
|
+
description.gsub(%r{\n\Z}, '')
|
156
|
+
end
|
157
|
+
|
158
|
+
#
|
159
|
+
# Adds options for the task defined next.
|
160
|
+
#
|
161
|
+
# task_options :option_name => default, :option2 ...
|
162
|
+
#
|
163
|
+
def task_options( options_hash )
|
164
|
+
initialize_instance
|
165
|
+
options_hash.each do |name,default|
|
166
|
+
@task_options << Caty::Option.new(name.to_sym, default)
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
#
|
171
|
+
# Adds global options.
|
172
|
+
#
|
173
|
+
# The first option will be decorated with the latest description
|
174
|
+
# defined via #desc().
|
175
|
+
#
|
176
|
+
# global_options do
|
177
|
+
# desc('description')
|
178
|
+
# string 'option_name' => 'default'
|
179
|
+
#
|
180
|
+
# desc('desc2')
|
181
|
+
# integer 'option2'
|
182
|
+
# end
|
183
|
+
#
|
184
|
+
def global_options( &block )
|
185
|
+
initialize_instance
|
186
|
+
constructor = Caty::OptionConstructor.new(Caty::GlobalOption)
|
187
|
+
@global_options.concat(constructor.construct(&block))
|
188
|
+
end
|
189
|
+
|
190
|
+
#
|
191
|
+
# Creates aliases for tasks.
|
192
|
+
#
|
193
|
+
# map :alias => :task_name
|
194
|
+
# map %w{alias1 alias2} => :task_name
|
195
|
+
#
|
196
|
+
def map( mapping_hash )
|
197
|
+
initialize_instance
|
198
|
+
|
199
|
+
mapping_hash.each do |mappings,target|
|
200
|
+
mappings = [mappings] unless mappings.is_a?(Array)
|
201
|
+
|
202
|
+
mappings.each do |mapping|
|
203
|
+
@tasks[mapping.to_sym] = Caty::Indirection.new(mapping.to_sym, target.to_sym)
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
#
|
209
|
+
# Defines a block of code that will be executed right
|
210
|
+
# before any task is called.
|
211
|
+
# _self_ will point to the Caty instance and the name of
|
212
|
+
# the task (Symbol) will be given as the only argument.
|
213
|
+
#
|
214
|
+
# before do |task|
|
215
|
+
# puts task
|
216
|
+
# end
|
217
|
+
#
|
218
|
+
def before( &block )
|
219
|
+
@before = block
|
220
|
+
end
|
221
|
+
|
222
|
+
#
|
223
|
+
# Defines a default task.
|
224
|
+
#
|
225
|
+
# default :task
|
226
|
+
#
|
227
|
+
def default( task )
|
228
|
+
@default = task.to_sym
|
229
|
+
end
|
230
|
+
|
231
|
+
#
|
232
|
+
# Defines a block of code that will be executed right
|
233
|
+
# after any task is called.
|
234
|
+
# _self_ will point to the Caty instance and the name of
|
235
|
+
# the task (Symbol) will be given as the only argument.
|
236
|
+
#
|
237
|
+
# NOTE: this code will not be executed if an error occured
|
238
|
+
# during task execution.
|
239
|
+
#
|
240
|
+
# after do |task|
|
241
|
+
# puts task
|
242
|
+
# end
|
243
|
+
#
|
244
|
+
def after( &block )
|
245
|
+
@after = block
|
246
|
+
end
|
247
|
+
|
248
|
+
#
|
249
|
+
# Decorates the next definition with a description and
|
250
|
+
# optionally a usage string.
|
251
|
+
#
|
252
|
+
# desc 'usage info', 'long description'
|
253
|
+
# desc 'long description'
|
254
|
+
#
|
255
|
+
def desc( usage, description = nil )
|
256
|
+
if description.nil?
|
257
|
+
@description = usage
|
258
|
+
else
|
259
|
+
@description, @usage = description, usage
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
#
|
264
|
+
# Methods to be used by Caty itself.
|
265
|
+
#
|
266
|
+
private
|
267
|
+
|
268
|
+
#
|
269
|
+
# Metaprogramming.
|
270
|
+
#
|
271
|
+
# See Module#method_added.
|
272
|
+
# Creates a new task, if the method that was added, was
|
273
|
+
# public.
|
274
|
+
#
|
275
|
+
def method_added( meth )
|
276
|
+
initialize_instance
|
277
|
+
name = meth.to_s
|
278
|
+
|
279
|
+
# only add public methods as tasks
|
280
|
+
if self.public_instance_methods.include?(name)
|
281
|
+
method = self.instance_method(name)
|
282
|
+
task = @tasks[meth] || Caty::Task.new(meth, method, @task_options)
|
283
|
+
task.description = @description
|
284
|
+
task.usage = @usage
|
285
|
+
|
286
|
+
@tasks[meth] = task
|
287
|
+
reset_decorators
|
288
|
+
end
|
289
|
+
end
|
290
|
+
|
291
|
+
#
|
292
|
+
# Tests whether the given backtrace is from
|
293
|
+
# an argument error in task invocation.
|
294
|
+
#
|
295
|
+
def is_task_argument_error( backtrace, task_name )
|
296
|
+
backtrace[0].end_with?("in `#{task_name}'") and # inside the task method
|
297
|
+
backtrace[1].end_with?("in `call'") and # in Task#call
|
298
|
+
backtrace[2].end_with?("in `execute'") and # in Task#execute
|
299
|
+
backtrace[3].end_with?("in `start!'") # in Caty::start!
|
300
|
+
end
|
301
|
+
|
302
|
+
#
|
303
|
+
# Resets the decorators applied via #desc() and #task_options().
|
304
|
+
#
|
305
|
+
def reset_decorators
|
306
|
+
@task_options = OptionArray.new
|
307
|
+
@description = nil
|
308
|
+
@usage = nil
|
309
|
+
end
|
310
|
+
|
311
|
+
#
|
312
|
+
# Initializes the tasks', options' and global options'
|
313
|
+
# storage instance variables.
|
314
|
+
#
|
315
|
+
def initialize_instance
|
316
|
+
@task_options ||= Caty::OptionArray.new
|
317
|
+
@global_options ||= Caty::OptionArray.new
|
318
|
+
@tasks ||= Caty::TaskHash.new
|
319
|
+
end
|
320
|
+
|
321
|
+
end
|
322
|
+
|
323
|
+
end
|
324
|
+
|
325
|
+
require 'caty/help_system'
|
326
|
+
Caty.extend(Caty::HelpSystem)
|
327
|
+
|
328
|
+
require 'caty/helpers'
|
329
|
+
require 'caty/errors'
|
330
|
+
require 'caty/has_description'
|
331
|
+
|
332
|
+
require 'caty/task_hash'
|
333
|
+
require 'caty/task'
|
334
|
+
require 'caty/indirection'
|
335
|
+
|
336
|
+
require 'caty/option_array'
|
337
|
+
require 'caty/option_constructor'
|
338
|
+
require 'caty/converters'
|
339
|
+
require 'caty/option'
|
340
|
+
require 'caty/global_option'
|
341
|
+
|