doodle 0.1.8 → 0.1.9

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.
@@ -0,0 +1,445 @@
1
+ # -*- mode: ruby; -*-
2
+ # command line option handling DSL implemented using Doodle
3
+ # Sean O'Halpin, 2008-09-29
4
+
5
+ =begin
6
+
7
+ to do:
8
+ - add file mode to filename (or have separate file type)
9
+ - use PathName?
10
+ - sort out lists of things (with types)
11
+ - apply match ~before~ from
12
+ - handle config files
13
+
14
+ =end
15
+
16
+ require 'doodle'
17
+ require 'doodle/datatypes'
18
+ require 'time'
19
+ require 'date'
20
+ require 'pp'
21
+
22
+ # note that all the datatypes within doodle do ... end blocks are
23
+ # doodle/datatypes - I then go on to define specialized option types
24
+ # with many of the same names but they are not the same - could be
25
+ # confusing :)
26
+
27
+ class Doodle
28
+ class App < Doodle
29
+ # specialised classes for handling attributes
30
+ def self.tidy_dir(path)
31
+ path.to_s.gsub(Regexp.new("^#{ Dir.pwd }/"), './')
32
+ end
33
+
34
+ # generic option
35
+ class Option < Doodle::DoodleAttribute
36
+ doodle do
37
+ string :flag, :max => 1, :doc => "one character abbreviation" do
38
+ init {
39
+ mf = name.to_s[0].chr
40
+ attrs = doodle_owner.doodle.attributes.map{ |k, v| v}
41
+ chk_attrs = attrs.reject{ |v|
42
+ v.name == name
43
+ }
44
+ # note: cannot check v.flag inside default clause because that
45
+ # causes an infinite regress (yes, I found out the hard way! :)
46
+ if chk_attrs.any?{|v|
47
+ v.respond_to?(:flag) && v.flag == mf
48
+ }
49
+ ""
50
+ else
51
+ mf
52
+ end
53
+ }
54
+ end
55
+ integer :arity, :default => 1, :doc => "how many arguments are expected"
56
+ has :values, :default => [], :doc => "valid values for this option"
57
+ has :match, :default => nil, :doc => "regex to match against"
58
+ end
59
+ end
60
+ # specialied Filename attribute
61
+ class Filename < Option
62
+ doodle do
63
+ boolean :existing, :default => false, :doc => "set to true if file must exist"
64
+ end
65
+ end
66
+
67
+ RX_ISODATE = /^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(\.\d+)? ?Z$/
68
+
69
+ # App directives
70
+ class << self
71
+ public
72
+
73
+ has :script_name, :default => File.basename($0)
74
+ has :doc, :default => $0
75
+ has :usage do
76
+ default { "./#{script_name} #{required_args.map{ |a| %[-#{ a.flag } #{format_kind(a.kind)}]}.join(' ')}" + ((required_args.size - doodle.attributes.size) > 0 ? " [OPTIONS]" : '') }
77
+ end
78
+ has :examples, :default => nil
79
+ alias :example :examples
80
+
81
+ def required
82
+ @optional = false
83
+ end
84
+ def optional
85
+ @optional = true
86
+ end
87
+ def optional?
88
+ instance_variable_defined?("@optional") ? @optional : false
89
+ end
90
+ def required_args
91
+ doodle.attributes.select{ |k, v| v.required?}.map{ |k,v| v}
92
+ end
93
+ alias :options :optional
94
+
95
+ # the generic option - can be any type
96
+ def option(*args, &block)
97
+ #p [:option, args, :optional, optional?]
98
+ key_values, args = args.partition{ |x| x.kind_of?(Hash)}
99
+ key_values = key_values.inject({ }){ |hash, kv| hash.merge(kv)}
100
+
101
+ errors = []
102
+
103
+ # handle optional/required flipflop
104
+ if optional?
105
+ required = { :default => nil }
106
+ if key_values.delete(:required)
107
+ if key_values.key?(:optional)
108
+ errors << "Can't specify both :required and :optional"
109
+ end
110
+ if key_values.key?(:default)
111
+ errors << "Can't specify both :required and :default"
112
+ end
113
+ required = { }
114
+ elsif key_values.delete(:optional) && !key_values.key?(:default)
115
+ required = { :default => nil }
116
+ end
117
+ else
118
+ key_values.delete(:required)
119
+ required = { }
120
+ end
121
+ args = [{ :using => Option }.merge(required).merge(key_values), *args]
122
+ da = has(*args, &block)
123
+ if errors.size > 0
124
+ raise ArgumentError, "#{da.name}: #{errors.join(', ')}", [caller[-1]]
125
+ end
126
+ da.instance_eval do
127
+ #p [:checking_values, values, values.class]
128
+ if da.values.kind_of?(Range)
129
+ must "be in range #{da.values}" do |s|
130
+ da.values.include?(s)
131
+ end
132
+ elsif da.values.respond_to?(:size) && da.values.size > 0
133
+ must "be one of #{da.values.join(', ')}" do |s|
134
+ da.values.include?(s)
135
+ end
136
+ end
137
+ if da.match
138
+ must "match pattern #{da.match.inspect}" do |s|
139
+ #p [:matching, s, da.match.inspect]
140
+ s.to_s =~ da.match
141
+ end
142
+ end
143
+ end
144
+ da
145
+ end
146
+ # expect string
147
+ def string(*args, &block)
148
+ args = [{ :using => Option, :kind => String }, *args]
149
+ da = option(*args, &block)
150
+ end
151
+ # expect symbol
152
+ def symbol(*args, &block)
153
+ args = [{ :using => Option, :kind => Symbol }, *args]
154
+ da = option(*args, &block)
155
+ da.instance_eval do
156
+ from String do |s|
157
+ s.to_sym
158
+ end
159
+ end
160
+ end
161
+ # expect filename
162
+ # filename :input, :existing => true, :flag => "i", :doc => "input file name"
163
+ def filename(*args, &block)
164
+ args = [{ :using => Filename, :kind => String }, *args ]
165
+ da = option(*args, &block)
166
+ da.instance_eval do
167
+ if da.existing
168
+ must "exist" do |s|
169
+ File.exist?(s)
170
+ end
171
+ end
172
+ end
173
+ end
174
+ # expect on/off flag, e.g. -b
175
+ # doesn't take any arguments (mere presence sets it to true)
176
+ # booleans are false by default
177
+ def boolean(*args, &block)
178
+ args = [{ :using => Option, :default => false, :arity => 0}, *args]
179
+ da = option(*args, &block)
180
+ da.instance_eval do
181
+ kind FalseClass, TrueClass
182
+ from NilClass do
183
+ false
184
+ end
185
+ from Numeric do |n|
186
+ n == 0 ? false : true
187
+ end
188
+ from String do |s|
189
+ case s
190
+ when "on", "true", "yes", "1"
191
+ true
192
+ when "off", "false", "no", "0"
193
+ false
194
+ else
195
+ raise Doodle::ValidationError, "unknown value for boolean: #{s}"
196
+ end
197
+ end
198
+ end
199
+ end
200
+ # whole number, e.g. -n 10
201
+ # you can use, e.g. :values => [1,2,3] or :values => (0..99) to restrict valid values
202
+ def integer(*args, &block)
203
+ args = [{ :using => Option, :kind => Integer }, *args]
204
+ da = option(*args, &block)
205
+ da.instance_eval do
206
+ from String do |s|
207
+ s =~ /^\d+$/ or raise "must be a whole number"
208
+ s.to_i
209
+ end
210
+ end
211
+ end
212
+ # date: -d 2008-09-28
213
+ def date(*args, &block)
214
+ args = [{ :using => Option, :kind => Date }, *args]
215
+ da = option(*args, &block)
216
+ da.instance_eval do
217
+ from String do |s|
218
+ Date.parse(s)
219
+ end
220
+ end
221
+ end
222
+ # time: -d 2008-09-28T18:00:00
223
+ def time(*args, &block)
224
+ args = [{ :using => Option, :kind => Time }, *args]
225
+ da = option(*args, &block)
226
+ da.instance_eval do
227
+ from String do |s|
228
+ Time.parse(s)
229
+ end
230
+ end
231
+ end
232
+ # utcdate: -d 2008-09-28T21:41:29Z
233
+ # actually uses Time (so restricted range)
234
+ def utcdate(*args, &block)
235
+ args = [{ :using => Option, :kind => Time }, *args]
236
+ da = option(*args, &block)
237
+ da.instance_eval do
238
+ from String do |s|
239
+ if s !~ RX_ISODATE
240
+ raise ArgumentError, "date must be in ISO format (YYYY-MM-DDTHH:MM:SS, e.g. #{Time.now.utc.xmlschema})"
241
+ end
242
+ Time.parse(s)
243
+ end
244
+ end
245
+ end
246
+
247
+ # use this to include 'standard' flags
248
+ def std_flags
249
+ boolean :help, :flag => "h", :doc => "display this help"
250
+ boolean :verbose, :flag => "v", :doc => "verbose output"
251
+ boolean :debug, :flag => "D", :doc => "turn on debugging"
252
+ end
253
+
254
+ has :exit_status, :default => 0
255
+
256
+ # call App.run to start your application (calls instance.run)
257
+ def run(argv = ARGV)
258
+ begin
259
+ app = from_argv(argv)
260
+ if app.help
261
+ puts help_text
262
+ else
263
+ app.run
264
+ end
265
+ rescue Object => e
266
+ if exit_status == 0
267
+ exit_status 1
268
+ end
269
+ puts "\nERROR: #{e}"
270
+ puts
271
+ puts help_text
272
+ ensure
273
+ exit(exit_status)
274
+ end
275
+ end
276
+
277
+ private
278
+
279
+ # helpers
280
+ def flag_to_attribute(flag)
281
+ a = doodle.attributes.select do |key, attr|
282
+ (key.to_s == flag.to_s) || (attr.respond_to?(:flag) && attr.flag.to_s == flag.to_s)
283
+ end
284
+ if a.size == 0
285
+ raise ArgumentError, "Unknown option: #{flag}"
286
+ elsif a.size > 1
287
+ #raise ArgumentError, "More than one option matches: #{flag}"
288
+ end
289
+ a.first
290
+ end
291
+ def key_value(arg, argv)
292
+ value = nil
293
+ if arg[0] == ?-
294
+ # got flag
295
+ #p [:a, 1, arg]
296
+ if arg[1] == ?-
297
+ # got --flag
298
+ # --key value
299
+ key = arg[2..-1]
300
+ #p [:a, 2, arg, key]
301
+ if key == ""
302
+ key = "--"
303
+ end
304
+ else
305
+ key = arg[1].chr
306
+ #p [:a, 4, key]
307
+ if arg[2]
308
+ # -kvalue
309
+ value = arg[2..-1]
310
+ #p [:a, 5, key, value]
311
+ end
312
+ end
313
+ pkey, attr = flag_to_attribute(key)
314
+ if pkey.nil?
315
+ raise Exception, "Internal error: #{key} does not match attribute"
316
+ end
317
+ #p [:flag_to_attribute, key, value, pkey, attr]
318
+ if value.nil?
319
+ if attr.arity == 0
320
+ value = true
321
+ else
322
+ #p [:args, 5, :getting_args, attr.arity]
323
+ value = []
324
+ 1.upto(attr.arity) do
325
+ a = argv.shift
326
+ break if a.nil?
327
+ #p [:a, 6, key, value]
328
+ if a =~ /^-/
329
+ # got a switch - break? (what should happen here?)
330
+ #p [:a, 7, key, value]
331
+ argv.unshift a
332
+ break
333
+ else
334
+ value << a
335
+ end
336
+ end
337
+ if attr.arity == 1
338
+ value = *value
339
+ end
340
+ end
341
+ end
342
+ if key.size == 1
343
+ #p [:finding, key]
344
+ #p [:found, pkey, attr]
345
+ key = pkey.to_s
346
+ end
347
+ end
348
+ [key, value]
349
+ end
350
+ def params_args(argv)
351
+ argv = argv.dup
352
+ params = { }
353
+ args = []
354
+ while arg = argv.shift
355
+ key, value = key_value(arg, argv)
356
+ if key.nil?
357
+ args << arg
358
+ else
359
+ #p [:setting, key, value]
360
+ params[key] = value
361
+ end
362
+ end
363
+ [params, args]
364
+ end
365
+ def from_argv(argv)
366
+ params, args = params_args(argv)
367
+ args << params
368
+ #pp [:args, args]
369
+ new(*args)
370
+ end
371
+ def format_values(values)
372
+ case values
373
+ when Array
374
+ values.map{ |s| s.to_s }.join(', ')
375
+ when Range
376
+ values.inspect
377
+ else
378
+ values.inspect
379
+ end
380
+ end
381
+ def format_doc(attr)
382
+ #p [:doc, attr.doc]
383
+ doc = attr.doc
384
+ if doc.kind_of?(Doodle::DeferredBlock)
385
+ doc = doc.block
386
+ end
387
+ if doc.kind_of?(Proc)
388
+ doc = attr.instance_eval(&doc)
389
+ end
390
+ if doc.respond_to?(:call)
391
+ doc = doc.call
392
+ end
393
+ doc = doc.to_s
394
+ if attr.respond_to?(:values) && (attr.values.kind_of?(Range) || attr.values.size > 0)
395
+ doc = "#{doc} [#{format_values(attr.values)}]"
396
+ end
397
+ doc
398
+ end
399
+ def format_kind(kind)
400
+ if (kind & [TrueClass, FalseClass, NilClass]).size > 0
401
+ "Boolean"
402
+ else
403
+ kind.map{ |k| k.to_s }.join(', ')
404
+ end.upcase
405
+ end
406
+ def help_attributes
407
+ options, args = doodle.attributes.partition { |key, attr| attr.respond_to?(:flag)}
408
+ args = args.map { |key, attr|
409
+ [
410
+ '',
411
+ '',
412
+ format_doc(attr),
413
+ attr.required?,
414
+ format_kind(attr.kind),
415
+ ]
416
+ }
417
+ options = options.map { |key, attr|
418
+ [
419
+ "--#{key}",
420
+ attr.flag.to_s.size > 0 ? "-#{attr.flag}," : '',
421
+ format_doc(attr),
422
+ attr.required?,
423
+ format_kind(attr.kind),
424
+ ]
425
+ }
426
+ args + options
427
+ end
428
+ public
429
+ def help_text
430
+ format_block = proc {|key, flag, doc, required, kind|
431
+ sprintf(" %-3s %-14s %-10s %s %s", flag, key, kind, doc, required ? '(REQUIRED)' : '')
432
+ }
433
+ required, options = help_attributes.partition{ |a| a[3]}
434
+ [
435
+ self.doc,
436
+ "\n",
437
+ self.usage ? ["Usage: " + self.usage, "\n"] : [],
438
+ required.size > 0 ? ["Required args:", required.map(&format_block), "\n"] : [],
439
+ options.size > 0 ? ["Options:", options.map(&format_block), "\n"] : [],
440
+ (self.examples && self.examples.size > 0) ? "Examples:\n" + " " + self.examples.join("\n ") : [],
441
+ ]
442
+ end
443
+ end
444
+ end
445
+ end
@@ -3,7 +3,6 @@ $:.unshift(File.join(File.dirname(__FILE__), '.'))
3
3
 
4
4
  require 'doodle'
5
5
 
6
- ### user code
7
6
  require 'date'
8
7
  require 'uri'
9
8
  require 'rfc822'
@@ -11,28 +10,82 @@ require 'rfc822'
11
10
  # note: this doesn't have to be in Doodle namespace
12
11
  class Doodle
13
12
  module DataTypes
13
+ class DataType < Doodle::DoodleAttribute
14
+ doodle do
15
+ has :values, :default => [], :doc => "valid values for this option"
16
+ has :match, :default => nil, :doc => "regex to match against"
17
+ end
18
+ end
19
+ def datatype(name, params, block, type_params, &type_block)
20
+ define name, params, block, { :using => DataType }.merge(type_params) do
21
+ #p [:self, __doodle__.__inspect__]
22
+ #p [:checking_values, values, values.class]
23
+ if respond_to?(:values)
24
+ if values.kind_of?(Range)
25
+ must "be in range #{values}" do |s|
26
+ values.include?(s)
27
+ end
28
+ # array of values
29
+ elsif values.respond_to?(:size) && values.size > 0
30
+ must "be one of #{values.join(', ')}" do |s|
31
+ values.include?(s)
32
+ end
33
+ end
34
+ end
35
+ if respond_to?(:match) && match
36
+ must "match pattern #{match.inspect}" do |s|
37
+ #p [:matching, s, da.match.inspect]
38
+ s.to_s =~ match
39
+ end
40
+ end
41
+ instance_eval(&type_block) if type_block
42
+ end
43
+ end
44
+
14
45
  def integer(name, params = { }, &block)
15
- define name, params, block, { :kind => Integer } do
46
+ if params.key?(:max)
47
+ max = params.delete(:max)
48
+ end
49
+ if params.key?(:min)
50
+ min = params.delete(:min)
51
+ end
52
+ datatype name, params, block, { :kind => Integer } do
16
53
  from Float do |n|
17
54
  n.to_i
18
55
  end
19
56
  from String do |n|
20
- n =~ /[0-9]+(.[0-9]+)?/ or raise ArgumentError, "#{name} must be numeric"
57
+ n =~ /[0-9]+(.[0-9]+)?/ or raise ArgumentError, "#{name} must be numeric", [caller[-1]]
21
58
  n.to_i
22
59
  end
60
+ if max
61
+ must "be <= #{max}" do |s|
62
+ s.size <= max
63
+ end
64
+ end
65
+ if min
66
+ must "be >= #{min}" do |s|
67
+ s.size >= min
68
+ end
69
+ end
23
70
  end
24
71
  end
25
72
 
26
73
  def boolean(name, params = { }, &block)
27
- define name, params, block, { } do
74
+ datatype name, params, block, { :default => true } do
28
75
  must "be true or false" do |v|
29
76
  [true, false].include?(v)
30
77
  end
78
+ from NilClass do |v|
79
+ false
80
+ end
81
+ from Integer do |v|
82
+ v == 0 ? false : true
83
+ end
31
84
  from String, Symbol do |v|
32
85
  case v.to_s
33
- when /^(yes|true|on)$/
86
+ when /^(yes|true|on|1)$/
34
87
  true
35
- when /^(no|false|off)$/
88
+ when /^(no|false|off|0)$/
36
89
  false
37
90
  else
38
91
  v
@@ -40,18 +93,18 @@ class Doodle
40
93
  end
41
94
  end
42
95
  end
43
-
96
+
44
97
  def symbol(name, params = { }, &block)
45
- define name, params, block, { :kind => Symbol } do
98
+ datatype name, params, block, { :kind => Symbol } do
46
99
  from String do |s|
47
100
  s.to_sym
48
101
  end
49
102
  end
50
103
  end
51
-
104
+
52
105
  def string(name, params = { }, &block)
53
106
  # must extract non-standard attributes before processing with
54
- # define otherwise causes UnknownAttribute error in Attribute definition
107
+ # datatype otherwise causes UnknownAttribute error in Attribute definition
55
108
  if params.key?(:max)
56
109
  max = params.delete(:max)
57
110
  end
@@ -60,7 +113,7 @@ class Doodle
60
113
  # size should be a Range
61
114
  size.kind_of?(Range) or raise ArgumentError, "#{name}: size should be a Range", [caller[-1]]
62
115
  end
63
- define name, params, block, { :kind => String } do
116
+ datatype name, params, block, { :kind => String } do
64
117
  from String do |s|
65
118
  s
66
119
  end
@@ -84,7 +137,7 @@ class Doodle
84
137
  end
85
138
 
86
139
  def uri(name, params = { }, &block)
87
- define name, params, block, { :kind => URI } do
140
+ datatype name, params, block, { :kind => URI } do
88
141
  from String do |s|
89
142
  URI.parse(s)
90
143
  end
@@ -100,9 +153,9 @@ class Doodle
100
153
  end
101
154
  end
102
155
  end
103
-
156
+
104
157
  def date(name, params = { }, &block)
105
- define name, params, block, { :kind => Date } do
158
+ datatype name, params, block, { :kind => Date } do
106
159
  from String do |s|
107
160
  Date.parse(s)
108
161
  end
@@ -115,8 +168,46 @@ class Doodle
115
168
  end
116
169
  end
117
170
 
171
+ # defaults to UTC if passed an array
172
+ # use :timezone => :local if you want local time
173
+ def time(name, params = { }, &block)
174
+ timezone_method = params.delete(:timezone) || :utc
175
+ if timezone_method == :local
176
+ timezone_method = :mktime
177
+ end
178
+ datatype name, params, block, { :kind => Time } do
179
+ from String do |s|
180
+ Time.parse(s)
181
+ end
182
+ from Array do |*args|
183
+ Time.send(timezone_method, *args)
184
+ end
185
+ # seconds since Thu Jan 01 00:00:00 UTC 1970
186
+ from Integer do |epoch_seconds|
187
+ Time.at(epoch_seconds)
188
+ end
189
+ end
190
+ end
191
+
192
+ RX_ISODATE = /^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(\.\d+)? ?Z$/
193
+
194
+ def utc(name, params = { }, &block)
195
+ da = time( name, { :kind => Time }.merge(params))
196
+ da.instance_eval do
197
+ # override time from String
198
+ from String do |s|
199
+ if s !~ RX_ISODATE
200
+ raise ArgumentError, "date must be in ISO format (YYYY-MM-DDTHH:MM:SS, e.g. #{Time.now.utc.xmlschema})"
201
+ end
202
+ Time.parse(s)
203
+ end
204
+ end
205
+ da.instance_eval(&block) if block_given?
206
+ da
207
+ end
208
+
118
209
  def version(name, params = { }, &block)
119
- define name, params, block, { :kind => String } do
210
+ datatype name, params, block, { :kind => String } do
120
211
  must "be of form n.n.n" do |str|
121
212
  str =~ /\d+\.\d+.\d+/
122
213
  end
@@ -126,6 +217,24 @@ class Doodle
126
217
  end
127
218
  end
128
219
  end
220
+
221
+ def list(name, params = { }, &block)
222
+ if name.kind_of?(Class)
223
+ params[:collect] = name
224
+ name = Doodle::Utils.pluralize(Doodle::Utils.snake_case(name))
225
+ end
226
+ raise ArgumentError, "#{name} must specify what to :collect", [caller[-1]] if !params.key?(:collect)
227
+ datatype name, params, block, { :using => Doodle::AppendableAttribute }
228
+ end
229
+
230
+ def dictionary(name, params = { }, &block)
231
+ if name.kind_of?(Class)
232
+ params[:collect] = name
233
+ name = Doodle::Utils.pluralize(Doodle::Utils.snake_case(name))
234
+ end
235
+ raise ArgumentError, "#{name} must specify what to :collect", [caller[-1]] if !params.key?(:collect)
236
+ datatype name, params, block, { :using => Doodle::KeyedAttribute }
237
+ end
129
238
  end
130
239
  end
131
240