doodle 0.1.8 → 0.1.9

Sign up to get free protection for your applications and to get access to all the features.
@@ -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