adhearsion-loquacious 1.9.2
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +23 -0
- data/Guardfile +5 -0
- data/History.txt +127 -0
- data/README.rdoc +229 -0
- data/Rakefile +30 -0
- data/adhearsion-loquacious.gemspec +32 -0
- data/examples/gutters.rb +29 -0
- data/examples/nested.rb +43 -0
- data/examples/simple.rb +20 -0
- data/lib/loquacious.rb +165 -0
- data/lib/loquacious/configuration.rb +406 -0
- data/lib/loquacious/configuration/help.rb +249 -0
- data/lib/loquacious/configuration/iterator.rb +158 -0
- data/lib/loquacious/core_ext/string.rb +75 -0
- data/lib/loquacious/undefined.rb +92 -0
- data/lib/loquacious/utility.rb +14 -0
- data/loquacious.gemspec +32 -0
- data/spec/configuration_spec.rb +513 -0
- data/spec/help_spec.rb +369 -0
- data/spec/iterator_spec.rb +70 -0
- data/spec/loquacious_spec.rb +76 -0
- data/spec/spec_helper.rb +70 -0
- data/spec/string_spec.rb +53 -0
- data/spec/utility_spec.rb +28 -0
- data/version.txt +1 -0
- metadata +102 -0
@@ -0,0 +1,249 @@
|
|
1
|
+
|
2
|
+
require 'pp'
|
3
|
+
require 'stringio'
|
4
|
+
|
5
|
+
class Loquacious::Configuration
|
6
|
+
|
7
|
+
# Generate nicely formatted help messages for a configuration. The Help
|
8
|
+
# class iterates over all the attributes in a configuration and outputs
|
9
|
+
# the name, value, and description to an IO stream. The format of the
|
10
|
+
# messages can be configured, and the description and/or value of the
|
11
|
+
# attribute can be shown or hidden independently.
|
12
|
+
#
|
13
|
+
class Help
|
14
|
+
|
15
|
+
# :stopdoc:
|
16
|
+
@@defaults = {
|
17
|
+
:io => $stdout,
|
18
|
+
:name_leader => ' - '.freeze,
|
19
|
+
:name_length => 0,
|
20
|
+
:name_value_sep => ' => '.freeze,
|
21
|
+
:desc_leader => ' '.freeze,
|
22
|
+
:nesting_nodes => true,
|
23
|
+
:colorize => false,
|
24
|
+
:colors => {
|
25
|
+
:name => :white,
|
26
|
+
:value => :cyan,
|
27
|
+
:description => :green,
|
28
|
+
:leader => :yellow
|
29
|
+
}.freeze
|
30
|
+
}.freeze
|
31
|
+
|
32
|
+
class Error < StandardError; end
|
33
|
+
# :startdoc:
|
34
|
+
|
35
|
+
# Create a new Help instance for the given configuration where _config_
|
36
|
+
# can be either a Configuration instance or a configuration name or
|
37
|
+
# symbol. Several options can be provided to determine how the
|
38
|
+
# configuration information will be printed to the IO stream.
|
39
|
+
#
|
40
|
+
# :name_leader String appearing before the attribute name
|
41
|
+
# :name_length Maximum length for an attribute name
|
42
|
+
# :name_value_sep String separating the attribute name from the value
|
43
|
+
# :desc_leader String appearing before the description
|
44
|
+
# :io The IO object where help will be written
|
45
|
+
# :nesting_nodes Flag to enable or disable output of nesting nodes
|
46
|
+
# (this does not affect display of attributes
|
47
|
+
# contained by the nesting nodes)
|
48
|
+
# :colorize Flag to colorize the output or not
|
49
|
+
# :colors Hash of colors for the name, value, description
|
50
|
+
# :name Name color
|
51
|
+
# :value Value color
|
52
|
+
# :description Description color
|
53
|
+
# :leader Leader and spacer color
|
54
|
+
#
|
55
|
+
# The description is printed before each attribute name and value on its
|
56
|
+
# own line.
|
57
|
+
#
|
58
|
+
def initialize( config, opts = {} )
|
59
|
+
opts = @@defaults.merge opts
|
60
|
+
@config = config.kind_of?(::Loquacious::Configuration) ? config :
|
61
|
+
::Loquacious::Configuration.for(config)
|
62
|
+
|
63
|
+
@io = opts[:io]
|
64
|
+
@name_length = Integer(opts[:name_length])
|
65
|
+
@desc_leader = opts[:desc_leader]
|
66
|
+
@nesting_nodes = opts[:nesting_nodes]
|
67
|
+
@colorize = opts[:colorize]
|
68
|
+
@colors = opts[:colors]
|
69
|
+
|
70
|
+
unless @name_length > 0
|
71
|
+
Iterator.new(@config).each do |node|
|
72
|
+
length = node.name.length
|
73
|
+
@name_length = length if length > @name_length
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
name_leader = opts[:name_leader]
|
78
|
+
name_value_sep = opts[:name_value_sep]
|
79
|
+
extra_length = name_leader.length + name_value_sep.length
|
80
|
+
name_value_sep = name_value_sep.gsub('%', '%%')
|
81
|
+
|
82
|
+
@value_length = 78 - @name_length - extra_length
|
83
|
+
@value_leader = "\n" + ' '*(@name_length + extra_length)
|
84
|
+
@format = "#{name_leader}%-#{@name_length}s#{name_value_sep}%s"
|
85
|
+
@name_format = "#{name_leader}%s"
|
86
|
+
|
87
|
+
if colorize?
|
88
|
+
@desc_leader = self.__send__(@colors[:leader], @desc_leader)
|
89
|
+
name_leader = self.__send__(@colors[:leader], name_leader)
|
90
|
+
name_value_sep = self.__send__(@colors[:leader], name_value_sep)
|
91
|
+
|
92
|
+
@format = name_leader.dup
|
93
|
+
@format << self.__send__(@colors[:name], "%-#{@name_length}s")
|
94
|
+
@format << name_value_sep.dup
|
95
|
+
@format << self.__send__(@colors[:value], "%s")
|
96
|
+
|
97
|
+
@name_format = name_leader.dup
|
98
|
+
@name_format << self.__send__(@colors[:name], "%s")
|
99
|
+
end
|
100
|
+
|
101
|
+
@desc_leader.freeze
|
102
|
+
@value_leader.freeze
|
103
|
+
@format.freeze
|
104
|
+
@name_format.freeze
|
105
|
+
end
|
106
|
+
|
107
|
+
# Returns +true+ if the help instance is configured to colorize the
|
108
|
+
# output messages. Returns +false+ otherwise.
|
109
|
+
#
|
110
|
+
def colorize?
|
111
|
+
@colorize
|
112
|
+
end
|
113
|
+
|
114
|
+
# Returns +true+ if the help instance is configured to show nesting
|
115
|
+
# configuration nodes when iterating over the attributes. This only
|
116
|
+
# prevents the nesting node name from being displayed. The attributes
|
117
|
+
# nested under the node are still displayed regardless of this setting.
|
118
|
+
#
|
119
|
+
def show_nesting_nodes?
|
120
|
+
@nesting_nodes
|
121
|
+
end
|
122
|
+
|
123
|
+
# call-seq:
|
124
|
+
# show_attribute( name = nil, opts = {} )
|
125
|
+
#
|
126
|
+
# Use this method to show the description for a single attribute or for
|
127
|
+
# all the attributes if no _name_ is given. The options allow you to
|
128
|
+
# show the values along with the attributes and to hide the descriptions
|
129
|
+
# (if all you want to see are the values).
|
130
|
+
#
|
131
|
+
# :descriptions => true to show descriptions and false to hide them
|
132
|
+
# :values => true to show values and false to hide them
|
133
|
+
#
|
134
|
+
def show_attribute( name = nil, opts = {} )
|
135
|
+
name, opts = nil, name if name.is_a?(Hash)
|
136
|
+
opts = {
|
137
|
+
:descriptions => true,
|
138
|
+
:values => false
|
139
|
+
}.merge!(opts)
|
140
|
+
|
141
|
+
rgxp = Regexp.new(normalize_attr(name))
|
142
|
+
show_description = opts[:descriptions]
|
143
|
+
show_value = opts[:values]
|
144
|
+
|
145
|
+
Iterator.new(@config).each do |node|
|
146
|
+
next unless rgxp =~ node.name
|
147
|
+
next if !show_nesting_nodes? and node.config?
|
148
|
+
print_node(node, show_description, show_value)
|
149
|
+
end
|
150
|
+
end
|
151
|
+
alias :show :show_attribute
|
152
|
+
|
153
|
+
# Show all attributes for the configuration. The same options allowed by
|
154
|
+
# the +show+ method are also supported by this method.
|
155
|
+
#
|
156
|
+
def show_all( opts = {} )
|
157
|
+
show_attribute(nil, opts)
|
158
|
+
end
|
159
|
+
alias :show_attributes :show_all
|
160
|
+
|
161
|
+
# Normalize the attribute _name_.
|
162
|
+
#
|
163
|
+
def normalize_attr( name )
|
164
|
+
case name
|
165
|
+
when String, nil; name.to_s
|
166
|
+
when Symbol; name.to_s
|
167
|
+
when Array; name.join('.')
|
168
|
+
else
|
169
|
+
raise Error, "cannot convert #{name.inspect} into an attribute identifier"
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
# Format the attribute name, value, and description and print the
|
174
|
+
# results. The value can be printed or not by setting the _show_value_
|
175
|
+
# flag to either +true+ or +false+. The description can be printed or
|
176
|
+
# not by setting the _show_description_ flag to either +true+ or
|
177
|
+
# +false+.
|
178
|
+
#
|
179
|
+
def print_node( node, show_description, show_value )
|
180
|
+
desc = node.desc.to_s.dup
|
181
|
+
show_description = false if desc.empty?
|
182
|
+
|
183
|
+
if show_description
|
184
|
+
if colorize?
|
185
|
+
desc = desc.gsub(%r/([^\n]+)/,
|
186
|
+
self.__send__(@colors[:description], '\1'))
|
187
|
+
end
|
188
|
+
@io.puts(desc.indent(@desc_leader))
|
189
|
+
end
|
190
|
+
|
191
|
+
@io.puts(format_name(node, show_value))
|
192
|
+
@io.puts if show_description
|
193
|
+
end
|
194
|
+
|
195
|
+
# Format the name of the attribute pointed at by the given _node_. If
|
196
|
+
# the _show_value_ flag is set to +true+, then the attribute value will
|
197
|
+
# also be included in the returned string.
|
198
|
+
#
|
199
|
+
def format_name( node, show_value )
|
200
|
+
name = node.name.reduce @name_length
|
201
|
+
return @name_format % name if node.config? or !show_value
|
202
|
+
|
203
|
+
sio = StringIO.new
|
204
|
+
PP.pp(node.obj, sio, @value_length)
|
205
|
+
sio.seek 0
|
206
|
+
obj = sio.read.chomp.gsub("\n", @value_leader)
|
207
|
+
@format % [name, obj]
|
208
|
+
end
|
209
|
+
|
210
|
+
[ [ :clear , 0 ],
|
211
|
+
[ :reset , 0 ], # synonym for :clear
|
212
|
+
[ :bold , 1 ],
|
213
|
+
[ :dark , 2 ],
|
214
|
+
[ :italic , 3 ], # not widely implemented
|
215
|
+
[ :underline , 4 ],
|
216
|
+
[ :underscore , 4 ], # synonym for :underline
|
217
|
+
[ :blink , 5 ],
|
218
|
+
[ :rapid_blink , 6 ], # not widely implemented
|
219
|
+
[ :negative , 7 ], # no reverse because of String#reverse
|
220
|
+
[ :concealed , 8 ],
|
221
|
+
[ :strikethrough, 9 ], # not widely implemented
|
222
|
+
[ :black , 30 ],
|
223
|
+
[ :red , 31 ],
|
224
|
+
[ :green , 32 ],
|
225
|
+
[ :yellow , 33 ],
|
226
|
+
[ :blue , 34 ],
|
227
|
+
[ :magenta , 35 ],
|
228
|
+
[ :cyan , 36 ],
|
229
|
+
[ :white , 37 ],
|
230
|
+
[ :on_black , 40 ],
|
231
|
+
[ :on_red , 41 ],
|
232
|
+
[ :on_green , 42 ],
|
233
|
+
[ :on_yellow , 43 ],
|
234
|
+
[ :on_blue , 44 ],
|
235
|
+
[ :on_magenta , 45 ],
|
236
|
+
[ :on_cyan , 46 ],
|
237
|
+
[ :on_white , 47 ] ].each do |name,code|
|
238
|
+
|
239
|
+
class_eval <<-CODE
|
240
|
+
def #{name.to_s}( str )
|
241
|
+
"\e[#{code}m\#{str}\e[0m"
|
242
|
+
end
|
243
|
+
CODE
|
244
|
+
end
|
245
|
+
|
246
|
+
end # class Help
|
247
|
+
end # module Loquacious
|
248
|
+
|
249
|
+
# EOF
|
@@ -0,0 +1,158 @@
|
|
1
|
+
|
2
|
+
class Loquacious::Configuration
|
3
|
+
|
4
|
+
# Provides an external iteraotr for a Loquacious::Configuration object.
|
5
|
+
# The iterator allows the user to retrieve all the configuration settings
|
6
|
+
# along with their descriptions and values.
|
7
|
+
#
|
8
|
+
# cfg = Configuration.for('foo') {
|
9
|
+
# bar 'value', :desc => 'the bar attribute'
|
10
|
+
# baz 42, :desc => 'the baz attribute'
|
11
|
+
# }
|
12
|
+
#
|
13
|
+
# i = Iterator.new(cfg)
|
14
|
+
# i.each do |node|
|
15
|
+
# puts "#{node.name} :: #{node.desc}"
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# Results in
|
19
|
+
#
|
20
|
+
# bar :: the bar attribute
|
21
|
+
# baz :: the baz attribute
|
22
|
+
#
|
23
|
+
class Iterator
|
24
|
+
|
25
|
+
# :stopdoc:
|
26
|
+
attr_reader :stack
|
27
|
+
private :stack
|
28
|
+
# :startdoc:
|
29
|
+
|
30
|
+
# Create a new iterator that will operate on the _config_ (configuration
|
31
|
+
# object). The iterator allows the attributes of the configuration object
|
32
|
+
# to be accessed -- this includes nested configuration objects.
|
33
|
+
#
|
34
|
+
def initialize( config )
|
35
|
+
@config = config
|
36
|
+
@stack = []
|
37
|
+
reset
|
38
|
+
end
|
39
|
+
|
40
|
+
# Iterate over each node in the configuration object yielding each to
|
41
|
+
# the supplied block in turn. The return value of the block is returned
|
42
|
+
# from this method. +nil+ is returned if there are no nodes in the
|
43
|
+
# iterator.
|
44
|
+
#
|
45
|
+
# If an _attribute_ is given, then the iteration starts at that
|
46
|
+
# particular attribute and recurse if it is a nested configuration.
|
47
|
+
# Otherwise, only that attribute is yielded to the block.
|
48
|
+
#
|
49
|
+
def each( attribute = nil )
|
50
|
+
reset
|
51
|
+
rv = nil
|
52
|
+
|
53
|
+
if attribute and !attribute.empty?
|
54
|
+
node = while (n = next_node) do
|
55
|
+
break n if n.name == attribute
|
56
|
+
end
|
57
|
+
return if node.nil?
|
58
|
+
|
59
|
+
rv = yield node
|
60
|
+
return rv unless node.config?
|
61
|
+
|
62
|
+
stack.clear
|
63
|
+
stack << new_frame(node.obj, node.name) if node.config?
|
64
|
+
end
|
65
|
+
|
66
|
+
while (node = next_node) do
|
67
|
+
rv = yield node
|
68
|
+
end
|
69
|
+
return rv
|
70
|
+
end
|
71
|
+
|
72
|
+
# Find the given named _attribute_ in the iterator. Returns a node
|
73
|
+
# representing the attribute; or +nil+ is returned if the named
|
74
|
+
# attribute could not be found.
|
75
|
+
#
|
76
|
+
def find( attribute )
|
77
|
+
attribute = attribute.to_s
|
78
|
+
return if attribute.empty?
|
79
|
+
|
80
|
+
node = self.each {|n| break n if n.name == attribute}
|
81
|
+
reset
|
82
|
+
return node
|
83
|
+
end
|
84
|
+
|
85
|
+
private
|
86
|
+
|
87
|
+
# Reset the iterator back to the beginning.
|
88
|
+
#
|
89
|
+
def reset
|
90
|
+
stack.clear
|
91
|
+
stack << new_frame(@config)
|
92
|
+
end
|
93
|
+
|
94
|
+
# Returns the next node from the current iteration stack frame. Returns
|
95
|
+
# +nil+ if there are no more nodes in the iterator.
|
96
|
+
#
|
97
|
+
def next_node
|
98
|
+
frame = stack.last
|
99
|
+
node = new_node(frame)
|
100
|
+
|
101
|
+
while node.nil?
|
102
|
+
stack.pop
|
103
|
+
return if stack.empty?
|
104
|
+
frame = stack.last
|
105
|
+
node = new_node(frame)
|
106
|
+
end
|
107
|
+
|
108
|
+
frame.index += 1
|
109
|
+
stack << new_frame(node.obj, node.name) if node.config?
|
110
|
+
|
111
|
+
node = next_node if node.undefined?
|
112
|
+
return node
|
113
|
+
end
|
114
|
+
|
115
|
+
# Create a new stack frame from the given _cfg_ (configuration object)
|
116
|
+
# and the optional _prefix_. The _prefix_ is used to complete the full
|
117
|
+
# name for each attribute key in the configuration object.
|
118
|
+
#
|
119
|
+
def new_frame( cfg, prefix = nil )
|
120
|
+
keys = cfg.__desc.keys.map {|k| k.to_s}
|
121
|
+
keys.sort!
|
122
|
+
keys.map! {|k| k.to_sym}
|
123
|
+
|
124
|
+
Frame.new(cfg, prefix.to_s, keys, 0)
|
125
|
+
end
|
126
|
+
|
127
|
+
# Create the next iteration node from the given stack _frame_. Returns
|
128
|
+
# +nil+ when there are no more nodes in the _frame_.
|
129
|
+
#
|
130
|
+
def new_node( frame )
|
131
|
+
key = frame.keys[frame.index]
|
132
|
+
return if key.nil?
|
133
|
+
|
134
|
+
cfg = frame.config
|
135
|
+
name = frame.prefix.empty? ? key.to_s : frame.prefix + ".#{key}"
|
136
|
+
description = cfg.__desc[key]
|
137
|
+
if Loquacious.env_config && description
|
138
|
+
description += " [" + Loquacious::Utility.env_var_name(name, cfg) + "]"
|
139
|
+
end
|
140
|
+
Node.new(cfg, name, description, key)
|
141
|
+
end
|
142
|
+
|
143
|
+
# Structure describing a single iteration stack frame. A new stack frame
|
144
|
+
# is created when we descend into a nested Configuration object.
|
145
|
+
#
|
146
|
+
Frame = Struct.new( :config, :prefix, :keys, :index )
|
147
|
+
|
148
|
+
# This is a single node in a Configuration object. It corresponds to a
|
149
|
+
# single configuration attribute.
|
150
|
+
#
|
151
|
+
Node = Struct.new( :config, :name, :desc, :key ) {
|
152
|
+
def config?() obj.kind_of? ::Loquacious::Configuration; end
|
153
|
+
def undefined?() obj.kind_of? ::Loquacious::Undefined; end
|
154
|
+
def obj() config.__send__(key); end
|
155
|
+
}
|
156
|
+
|
157
|
+
end # class Iterator
|
158
|
+
end # class Loquacious::Configuration
|
@@ -0,0 +1,75 @@
|
|
1
|
+
|
2
|
+
class String
|
3
|
+
|
4
|
+
# call-seq:
|
5
|
+
# reduce( width, ellipses = '...' ) #=> string
|
6
|
+
#
|
7
|
+
# Reduce the size of the current string to the given _width_ by removing
|
8
|
+
# characters from the middle of the string and replacing them with
|
9
|
+
# _ellipses_. If the _width_ is greater than the length of the string, the
|
10
|
+
# string is returned unchanged. If the _width_ is less than the length of
|
11
|
+
# the _ellipses_, then the _ellipses_ are returned.
|
12
|
+
#
|
13
|
+
def reduce( width, ellipses = '...')
|
14
|
+
raise ArgumentError, "width cannot be negative: #{width}" if width < 0
|
15
|
+
|
16
|
+
return self if length <= width
|
17
|
+
|
18
|
+
remove = length - width + ellipses.length
|
19
|
+
return ellipses.dup if remove >= length
|
20
|
+
|
21
|
+
left_end = (length + 1 - remove) / 2
|
22
|
+
right_start = left_end + remove
|
23
|
+
|
24
|
+
left = self[0,left_end]
|
25
|
+
right = self[right_start,length-right_start]
|
26
|
+
|
27
|
+
left << ellipses << right
|
28
|
+
end
|
29
|
+
|
30
|
+
# call-seq:
|
31
|
+
# "foo".indent( 2 ) #=> " foo"
|
32
|
+
# "foo".indent( '# ' ) #=> "# foo"
|
33
|
+
#
|
34
|
+
# Indent the string by the given number of spaces. Alternately, if a
|
35
|
+
# leader string is given it will be used to indent with instead of spaces.
|
36
|
+
# Indentation is performed at the beginning of the string and after every
|
37
|
+
# newline character.
|
38
|
+
#
|
39
|
+
# "foo\nbar".indent( 2 ) #=> " foo\n bar"
|
40
|
+
#
|
41
|
+
def indent( leader )
|
42
|
+
leader =
|
43
|
+
Numeric === leader ? ' ' * leader.to_i : leader.to_s
|
44
|
+
str = self.gsub("\n", "\n"+leader)
|
45
|
+
str.insert(0, leader)
|
46
|
+
str
|
47
|
+
end
|
48
|
+
|
49
|
+
# call-seq:
|
50
|
+
# " | foo\n | bar".gutter! #=> " foo\n bar"
|
51
|
+
#
|
52
|
+
# Removes a leading _gutter_ from all lines in the string. The gutter is
|
53
|
+
# defined leading whitespace followed by a single pipe character. This
|
54
|
+
# method is very useful with heredocs.
|
55
|
+
#
|
56
|
+
# The string will be altered by this method.
|
57
|
+
#
|
58
|
+
def gutter!
|
59
|
+
gsub! %r/^[\t\f\r ]*\|?/, ''
|
60
|
+
self
|
61
|
+
end
|
62
|
+
|
63
|
+
# call-seq:
|
64
|
+
# " | foo\n | bar".gutter! #=> " foo\n bar"
|
65
|
+
#
|
66
|
+
# Removes a leading _gutter_ from all lines in the string. The gutter is
|
67
|
+
# defined leading whitespace followed by a single pipe character. This
|
68
|
+
# method is very useful with heredocs.
|
69
|
+
#
|
70
|
+
def gutter
|
71
|
+
self.dup.gutter!
|
72
|
+
end
|
73
|
+
end # class String
|
74
|
+
|
75
|
+
# EOF
|