configurable 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/MIT-LICENSE +19 -0
- data/README +237 -0
- data/lib/config_parser.rb +216 -0
- data/lib/config_parser/option.rb +52 -0
- data/lib/config_parser/switch.rb +29 -0
- data/lib/config_parser/utils.rb +133 -0
- data/lib/configurable.rb +155 -0
- data/lib/configurable/class_methods.rb +308 -0
- data/lib/configurable/delegate.rb +75 -0
- data/lib/configurable/delegate_hash.rb +165 -0
- data/lib/configurable/indifferent_access.rb +22 -0
- data/lib/configurable/validation.rb +480 -0
- metadata +84 -0
data/MIT-LICENSE
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
Copyright (c) 2008, Regents of the University of Colorado.
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
4
|
+
software and associated documentation files (the "Software"), to deal in the Software
|
5
|
+
without restriction, including without limitation the rights to use, copy, modify, merge,
|
6
|
+
publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
7
|
+
to whom the Software is furnished to do so, subject to the following conditions:
|
8
|
+
|
9
|
+
The above copyright notice and this permission notice shall be included in all copies or
|
10
|
+
substantial portions of the Software.
|
11
|
+
|
12
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
13
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
14
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
15
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
16
|
+
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
17
|
+
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
18
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
19
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
data/README
ADDED
@@ -0,0 +1,237 @@
|
|
1
|
+
= Configurable[http://tap.rubyforge.org/configurable]
|
2
|
+
|
3
|
+
Class configurations that map to the command line. Configurable is used by the
|
4
|
+
Tap[http://tap.rubyforge.org] framework.
|
5
|
+
|
6
|
+
== Description
|
7
|
+
|
8
|
+
Configurable allows the declaration of inheritable, class-based configurations
|
9
|
+
that map to methods but may be accessed like a hash; a setup that is both fast
|
10
|
+
and convenient. Configurable facilitates the use of configuration files, and
|
11
|
+
parsing of configurations from the command line.
|
12
|
+
|
13
|
+
Check out these links for development, and bug tracking.
|
14
|
+
|
15
|
+
* Website[http://tap.rubyforge.org/configurable]
|
16
|
+
* Github[http://github.com/bahuvrihi/configurable/tree/master]
|
17
|
+
* Lighthouse[]
|
18
|
+
* {Google Group}[http://groups.google.com/group/ruby-on-tap]
|
19
|
+
|
20
|
+
== Usage
|
21
|
+
|
22
|
+
=== Quickstart
|
23
|
+
|
24
|
+
class ConfigClass
|
25
|
+
include Configurable
|
26
|
+
|
27
|
+
config :key, 'default', :short => 'k' # a simple config with short
|
28
|
+
config :flag, false, &c.flag # a flag config
|
29
|
+
config :switch, false, &c.switch # a --[no-]switch config
|
30
|
+
config :num, 10, &c.integer # integer only
|
31
|
+
config :range, 1..10, &c.range # range only
|
32
|
+
config :upcase, 'default' do |value| # custom transformation
|
33
|
+
value.upcase
|
34
|
+
end
|
35
|
+
|
36
|
+
def initialize(overrides={})
|
37
|
+
initialize_config(overrides)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
Configurations are present and documented in the class ConfigParser, the
|
42
|
+
Configurable equivalent of OptionParser:
|
43
|
+
|
44
|
+
parser = ConfigClass.parser
|
45
|
+
parser.class # => ConfigParser
|
46
|
+
"\n" + parser.to_s
|
47
|
+
# => %Q{
|
48
|
+
# -k, --key KEY a simple config with short
|
49
|
+
# --flag a flag config
|
50
|
+
# --[no-]switch a --[no-]switch config
|
51
|
+
# --num NUM integer only
|
52
|
+
# --range RANGE range only
|
53
|
+
# --upcase UPCASE custom transformation
|
54
|
+
# }
|
55
|
+
|
56
|
+
Command line arguments parse as expected:
|
57
|
+
|
58
|
+
parser.parse "one two --key=value --flag --no-switch --num 8 --range a..z three"
|
59
|
+
# => ['one', 'two', 'three']
|
60
|
+
|
61
|
+
parser.config
|
62
|
+
# => {
|
63
|
+
# :key => 'value',
|
64
|
+
# :flag => true,
|
65
|
+
# :switch => false,
|
66
|
+
# :num => '8',
|
67
|
+
# :range => 'a..z',
|
68
|
+
# :upcase => 'default'
|
69
|
+
# }
|
70
|
+
|
71
|
+
Validations/transformations occur upon initialization:
|
72
|
+
|
73
|
+
c = ConfigClass.new(parser.config)
|
74
|
+
c.config.to_hash
|
75
|
+
# => {
|
76
|
+
# :key => 'value',
|
77
|
+
# :flag => true,
|
78
|
+
# :switch => false,
|
79
|
+
# :num => 8,
|
80
|
+
# :range => 'a'..'z',
|
81
|
+
# :upcase => 'DEFAULT'
|
82
|
+
# }
|
83
|
+
|
84
|
+
Configurations have accessors, and are accessible through config.
|
85
|
+
|
86
|
+
c.upcase # => 'DEFAULT'
|
87
|
+
|
88
|
+
c.config[:upcase] = 'neW valuE'
|
89
|
+
c.upcase # => 'NEW VALUE'
|
90
|
+
|
91
|
+
c.upcase = 'fiNal Value'
|
92
|
+
c.config[:upcase] # => 'FINAL VALUE'
|
93
|
+
|
94
|
+
Note that configurations are validated every time they are set:
|
95
|
+
|
96
|
+
c.num = 'blue' # !> ValidationError
|
97
|
+
|
98
|
+
By default config treats strings and symbols as the same, so YAML config files
|
99
|
+
are easily created and used.
|
100
|
+
|
101
|
+
yaml_str = %Q{
|
102
|
+
key: a new value
|
103
|
+
flag: false
|
104
|
+
range: 1..100
|
105
|
+
}
|
106
|
+
|
107
|
+
c.reconfigure(YAML.load(yaml_str))
|
108
|
+
c.config.to_hash
|
109
|
+
# => {
|
110
|
+
# :key => 'a new value',
|
111
|
+
# :flag => false,
|
112
|
+
# :switch => false,
|
113
|
+
# :num => 8,
|
114
|
+
# :range => 1..100,
|
115
|
+
# :upcase => 'FINAL VALUE'
|
116
|
+
# }
|
117
|
+
|
118
|
+
=== Declarations
|
119
|
+
|
120
|
+
Configurations are added to classes via declarations. Declarations are a lot
|
121
|
+
like specifying an attribute reader, writer, and the initialization code.
|
122
|
+
|
123
|
+
class ConfigClass
|
124
|
+
include Configurable
|
125
|
+
|
126
|
+
config :key, 'value' do |input|
|
127
|
+
input.upcase
|
128
|
+
end
|
129
|
+
|
130
|
+
def initialize
|
131
|
+
initialize_config
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
Is basically the same as:
|
136
|
+
|
137
|
+
class RegularClass
|
138
|
+
attr_reader :key
|
139
|
+
|
140
|
+
def key=(input)
|
141
|
+
@key = input.upcase
|
142
|
+
end
|
143
|
+
|
144
|
+
def initialize
|
145
|
+
self.key = 'value'
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
As far as the reader/writer goes, the analogy is quite good. The writer
|
150
|
+
method is defined so it sets the instance variable using the return of the
|
151
|
+
block. To literally define the writer with the block, use config_attr.
|
152
|
+
|
153
|
+
class ConfigAttrClass
|
154
|
+
include Configurable
|
155
|
+
|
156
|
+
config_attr :key, 'value' do |input|
|
157
|
+
@key = input.upcase
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
Literally defines methods:
|
162
|
+
|
163
|
+
class RegularClass
|
164
|
+
attr_reader :key
|
165
|
+
|
166
|
+
def key=(input)
|
167
|
+
@key = input.upcase
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
=== Validation
|
172
|
+
|
173
|
+
When configurations are parsed from the command line, the config writers will
|
174
|
+
inevitably receive a string (even though the code may want a different object).
|
175
|
+
The {Validation}[link:classes/Configurable/Validation.html] module provides
|
176
|
+
standard blocks for validating and transforming string inputs and is accessible
|
177
|
+
in classes via the <tt>c</tt> method (ex: <tt>c.integer</tt> or
|
178
|
+
<tt>c.regexp</tt>). These blocks (generally) load string inputs as YAML and
|
179
|
+
validate that the result is the correct class; non-string inputs are simply
|
180
|
+
validated.
|
181
|
+
|
182
|
+
class ValidatingClass
|
183
|
+
include Configurable
|
184
|
+
|
185
|
+
config :int, 1, &c.integer # assures the input is an integer
|
186
|
+
config :int_or_nil, 1, &c.integer_or_nil # integer or nil only
|
187
|
+
config :array, [], &c.array # you get the idea
|
188
|
+
end
|
189
|
+
|
190
|
+
vc = ValidatingClass.new
|
191
|
+
|
192
|
+
vc.array = [:a, :b, :c]
|
193
|
+
vc.array # => [:a, :b, :c]
|
194
|
+
|
195
|
+
vc.array = "[1, 2, 3]"
|
196
|
+
vc.array # => [1, 2, 3]
|
197
|
+
|
198
|
+
vc.array = "string" # !> ValidationError
|
199
|
+
|
200
|
+
Validation blocks sometimes imply metadata. For instance <tt>c.flag</tt> causes
|
201
|
+
the config to appear as a flag on the command line. Metadata can be manually
|
202
|
+
specified in the options:
|
203
|
+
|
204
|
+
class ManualMetadata
|
205
|
+
include Configurable
|
206
|
+
|
207
|
+
config :key, 'default', :type => :flag do
|
208
|
+
# this block is only called if --key
|
209
|
+
# is specified, and will not take a
|
210
|
+
# value
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
=== Documentation
|
215
|
+
|
216
|
+
Documentation on the command line is pulled from the code directly using
|
217
|
+
Lazydoc[http://tap.rubyforge.org/lazydoc/]. Documentation is a kind of
|
218
|
+
metadata for configurations, and may be specified manually as an option:
|
219
|
+
|
220
|
+
class ManualDocumentation
|
221
|
+
include Configurable
|
222
|
+
config :key, 'default', :desc => 'this is the command line description'
|
223
|
+
end
|
224
|
+
|
225
|
+
== Installation
|
226
|
+
|
227
|
+
Configurable is available as a gem on RubyForge[http://rubyforge.org/projects/tap]. Use:
|
228
|
+
|
229
|
+
% gem install configurable
|
230
|
+
|
231
|
+
== Info
|
232
|
+
|
233
|
+
Copyright (c) 2008, Regents of the University of Colorado.
|
234
|
+
Developer:: {Simon Chiang}[http://bahuvrihi.wordpress.com], {Biomolecular Structure Program}[http://biomol.uchsc.edu/], {Hansen Lab}[http://hsc-proteomics.uchsc.edu/hansenlab/]
|
235
|
+
Support:: CU Denver School of Medicine Deans Academic Enrichment Fund
|
236
|
+
Licence:: {MIT-Style}[link:files/MIT-LICENSE.html]
|
237
|
+
|
@@ -0,0 +1,216 @@
|
|
1
|
+
require 'config_parser/option'
|
2
|
+
require 'config_parser/switch'
|
3
|
+
|
4
|
+
autoload(:Shellwords, 'shellwords')
|
5
|
+
|
6
|
+
class ConfigParser
|
7
|
+
class << self
|
8
|
+
# Splits and nests compound keys of a hash.
|
9
|
+
#
|
10
|
+
# ConfigParser.nest('key' => 1, 'compound:key' => 2)
|
11
|
+
# # => {
|
12
|
+
# # 'key' => 1,
|
13
|
+
# # 'compound' => {'key' => 2}
|
14
|
+
# # }
|
15
|
+
#
|
16
|
+
# Nest does not do any consistency checking, so be aware that results will
|
17
|
+
# be ambiguous for overlapping compound keys.
|
18
|
+
#
|
19
|
+
# ConfigParser.nest('key' => {}, 'key:overlap' => 'value')
|
20
|
+
# # =? {'key' => {}}
|
21
|
+
# # =? {'key' => {'overlap' => 'value'}}
|
22
|
+
#
|
23
|
+
def nest(hash, split_char=":")
|
24
|
+
result = {}
|
25
|
+
hash.each_pair do |compound_key, value|
|
26
|
+
if compound_key.kind_of?(String)
|
27
|
+
keys = compound_key.split(split_char)
|
28
|
+
|
29
|
+
unless keys.length == 1
|
30
|
+
nested_key = keys.pop
|
31
|
+
nested_hash = keys.inject(result) {|target, key| target[key] ||= {}}
|
32
|
+
nested_hash[nested_key] = value
|
33
|
+
next
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
result[compound_key] = value
|
38
|
+
end
|
39
|
+
|
40
|
+
result
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
include Utils
|
45
|
+
|
46
|
+
# A hash of (switch, Option) pairs mapping switches to
|
47
|
+
# options.
|
48
|
+
attr_reader :switches
|
49
|
+
|
50
|
+
attr_reader :config
|
51
|
+
|
52
|
+
attr_reader :default_config
|
53
|
+
|
54
|
+
def initialize
|
55
|
+
@options = []
|
56
|
+
@switches = {}
|
57
|
+
@config = {}
|
58
|
+
@default_config = {}
|
59
|
+
|
60
|
+
yield(self) if block_given?
|
61
|
+
end
|
62
|
+
|
63
|
+
# Returns an array of options registered with self.
|
64
|
+
def options
|
65
|
+
@options.select do |opt|
|
66
|
+
opt.kind_of?(Option)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Adds a separator string to self.
|
71
|
+
def separator(str)
|
72
|
+
@options << str
|
73
|
+
end
|
74
|
+
|
75
|
+
# Registers the option with self by adding opt to options and mapping
|
76
|
+
# the opt switches. Raises an error for conflicting keys and switches.
|
77
|
+
def register(opt)
|
78
|
+
@options << opt unless @options.include?(opt)
|
79
|
+
|
80
|
+
opt.switches.each do |switch|
|
81
|
+
case @switches[switch]
|
82
|
+
when opt then next
|
83
|
+
when nil then @switches[switch] = opt
|
84
|
+
else raise ArgumentError, "switch is already mapped to a different option: #{switch}"
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
opt
|
89
|
+
end
|
90
|
+
|
91
|
+
# Defines and registers a config with self.
|
92
|
+
def define(key, default_value=nil, options={})
|
93
|
+
# check for conflicts and register
|
94
|
+
if default_config.has_key?(key)
|
95
|
+
raise ArgumentError, "already set by a different option: #{key.inspect}"
|
96
|
+
end
|
97
|
+
default_config[key] = default_value
|
98
|
+
|
99
|
+
block = case options[:type]
|
100
|
+
when :switch then setup_switch(key, default_value, options)
|
101
|
+
when :flag then setup_flag(key, default_value, options)
|
102
|
+
when :list then setup_list(key, options)
|
103
|
+
when nil then setup_option(key, options)
|
104
|
+
when respond_to?("setup_#{options[:type]}")
|
105
|
+
send("setup_#{options[:type]}", key, default_value, options)
|
106
|
+
else
|
107
|
+
raise ArgumentError, "unsupported type: #{options[:type]}"
|
108
|
+
end
|
109
|
+
|
110
|
+
on(options, &block)
|
111
|
+
end
|
112
|
+
|
113
|
+
def on(*args, &block)
|
114
|
+
options = args.last.kind_of?(Hash) ? args.pop : {}
|
115
|
+
args.each do |arg|
|
116
|
+
# split switch arguments... descriptions
|
117
|
+
# still won't match as a switch even
|
118
|
+
# after a split
|
119
|
+
switch, arg_name = arg.split(' ', 2)
|
120
|
+
|
121
|
+
# determine the kind of argument specified
|
122
|
+
key = case switch
|
123
|
+
when SHORT_OPTION then :short
|
124
|
+
when LONG_OPTION then :long
|
125
|
+
else :desc
|
126
|
+
end
|
127
|
+
|
128
|
+
# check for conflicts
|
129
|
+
if options[key]
|
130
|
+
raise ArgumentError, "conflicting #{key} options: [#{options[key]}, #{arg}]"
|
131
|
+
end
|
132
|
+
|
133
|
+
# set the option
|
134
|
+
case key
|
135
|
+
when :long, :short
|
136
|
+
options[key] = switch
|
137
|
+
options[:arg_name] = arg_name.strip if arg_name
|
138
|
+
else
|
139
|
+
options[key] = arg.strip
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
# check if the option is specifying a Switch
|
144
|
+
klass = case
|
145
|
+
when options[:long].to_s =~ /^--\[no-\](.*)$/
|
146
|
+
options[:long] = "--#{$1}"
|
147
|
+
Switch
|
148
|
+
else
|
149
|
+
Option
|
150
|
+
end
|
151
|
+
|
152
|
+
# instantiate and register the new option
|
153
|
+
register klass.new(options, &block)
|
154
|
+
end
|
155
|
+
|
156
|
+
# Parse is non-destructive to argv. If a string argv is provided, parse
|
157
|
+
# splits it into an array using Shellwords.
|
158
|
+
#
|
159
|
+
def parse(argv=ARGV, config={})
|
160
|
+
@config = config
|
161
|
+
argv = argv.kind_of?(String) ? Shellwords.shellwords(argv) : argv.dup
|
162
|
+
args = []
|
163
|
+
|
164
|
+
while !argv.empty?
|
165
|
+
arg = argv.shift
|
166
|
+
|
167
|
+
# determine if the arg is an option
|
168
|
+
unless arg.kind_of?(String) && arg[0] == ?-
|
169
|
+
args << arg
|
170
|
+
next
|
171
|
+
end
|
172
|
+
|
173
|
+
# add the remaining args and break
|
174
|
+
# for the option break
|
175
|
+
if arg == OPTION_BREAK
|
176
|
+
args.concat(argv)
|
177
|
+
break
|
178
|
+
end
|
179
|
+
|
180
|
+
# split the arg...
|
181
|
+
# switch= $1
|
182
|
+
# value = $4 || $3 (if arg matches SHORT_OPTION, value is $4 or $3 otherwise)
|
183
|
+
arg =~ LONG_OPTION || arg =~ SHORT_OPTION || arg =~ ALT_SHORT_OPTION
|
184
|
+
|
185
|
+
# lookup the option
|
186
|
+
unless option = @switches[$1]
|
187
|
+
raise "unknown option: #{$1}"
|
188
|
+
end
|
189
|
+
|
190
|
+
option.parse($1, $4 || $3, argv)
|
191
|
+
end
|
192
|
+
|
193
|
+
default_config.each_pair do |key, default|
|
194
|
+
config[key] = default unless config.has_key?(key)
|
195
|
+
end
|
196
|
+
|
197
|
+
args
|
198
|
+
end
|
199
|
+
|
200
|
+
# Same as parse, but removes parsed args from argv.
|
201
|
+
def parse!(argv=ARGV, config={})
|
202
|
+
argv.replace(parse(argv, config))
|
203
|
+
end
|
204
|
+
|
205
|
+
def to_s
|
206
|
+
comments = @options.collect do |option|
|
207
|
+
next unless option.respond_to?(:desc)
|
208
|
+
option.desc.kind_of?(Lazydoc::Comment) ? option.desc : nil
|
209
|
+
end.compact
|
210
|
+
Lazydoc.resolve_comments(comments)
|
211
|
+
|
212
|
+
@options.collect do |option|
|
213
|
+
option.to_s.rstrip
|
214
|
+
end.join("\n") + "\n"
|
215
|
+
end
|
216
|
+
end
|