rubycom 0.3.2 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +8 -8
  2. data/README.md +162 -146
  3. data/Rakefile +12 -12
  4. data/lib/rubycom.rb +156 -226
  5. data/lib/rubycom/arg_parse.rb +252 -0
  6. data/lib/rubycom/command_interface.rb +97 -0
  7. data/lib/rubycom/completions.rb +62 -0
  8. data/lib/rubycom/error_handler.rb +15 -0
  9. data/lib/rubycom/executor.rb +23 -0
  10. data/lib/rubycom/helpers.rb +98 -0
  11. data/lib/rubycom/output_handler.rb +15 -0
  12. data/lib/rubycom/parameter_extract.rb +262 -0
  13. data/lib/rubycom/singleton_commands.rb +78 -0
  14. data/lib/rubycom/sources.rb +99 -0
  15. data/lib/rubycom/version.rb +1 -1
  16. data/lib/rubycom/yard_doc.rb +146 -0
  17. data/rubycom.gemspec +14 -16
  18. data/test/rubycom/arg_parse_test.rb +247 -0
  19. data/test/rubycom/command_interface_test.rb +293 -0
  20. data/test/rubycom/completions_test.rb +94 -0
  21. data/test/rubycom/error_handler_test.rb +72 -0
  22. data/test/rubycom/executor_test.rb +64 -0
  23. data/test/rubycom/helpers_test.rb +467 -0
  24. data/test/rubycom/output_handler_test.rb +76 -0
  25. data/test/rubycom/parameter_extract_test.rb +141 -0
  26. data/test/rubycom/rubycom_test.rb +290 -548
  27. data/test/rubycom/singleton_commands_test.rb +122 -0
  28. data/test/rubycom/sources_test.rb +59 -0
  29. data/test/rubycom/util_test_bin.rb +8 -0
  30. data/test/rubycom/util_test_composite.rb +23 -20
  31. data/test/rubycom/util_test_module.rb +142 -112
  32. data/test/rubycom/util_test_no_singleton.rb +2 -2
  33. data/test/rubycom/util_test_sub_module.rb +13 -0
  34. data/test/rubycom/yard_doc_test.rb +165 -0
  35. metadata +61 -24
  36. data/lib/rubycom/arguments.rb +0 -133
  37. data/lib/rubycom/commands.rb +0 -63
  38. data/lib/rubycom/documentation.rb +0 -212
  39. data/test/rubycom/arguments_test.rb +0 -289
  40. data/test/rubycom/commands_test.rb +0 -51
  41. data/test/rubycom/documentation_test.rb +0 -186
  42. data/test/rubycom/util_test_job.yaml +0 -21
  43. data/test/rubycom/utility_tester.rb +0 -17
@@ -0,0 +1,98 @@
1
+ module Rubycom
2
+ module Helpers
3
+
4
+ # Arranges each given tag hash such that all tags will line up nicely in vertical columns.
5
+ #
6
+ # @param [Array] tag_list an Array of Hashes which include keys: :name, :tag_name, :text, :types
7
+ # @param [Integer] desc_width the maximum width to use for the description column
8
+ # @return [Array] a list of strings comprised of types, separator, name||tag_name, separator, text
9
+ # formatted to line up vertically
10
+ def self.format_tags(tag_list, desc_width = 90)
11
+ tag_list = [] if tag_list.nil?
12
+ raise ArgumentError, "tag_list should be an Array but was a #{tag_list.class}" unless tag_list.class == Array
13
+ tag_list.each { |h|
14
+ raise ArgumentError, "tag #{h} should be a Hash but was a #{h.class}" unless h.class == Hash
15
+ [:name, :tag_name, :text, :types].each { |t| h.fetch(t) }
16
+ types = h[:types]
17
+ raise ArgumentError, "tag[:types] #{types} should be an Array but was #{types.class}" unless types.class == Array
18
+ }
19
+
20
+ longest_name = tag_list.map { |tag| (tag[:name].nil?) ? tag[:tag_name] : tag[:name] }.max
21
+ longest_types = tag_list.map { |tag| "#{tag[:types]}" }.max
22
+ longest_combo_name = tag_list.map { |tag| "#{tag[:tag_name]}#{tag[:name]}" }.max
23
+ {
24
+ others: tag_list.select { |tag| !["param", "return"].include?(tag[:tag_name]) }.map { |tag|
25
+ combo_name = (tag[:tag_name].nil? || tag[:tag_name].empty? || tag[:name].nil? || tag[:name].empty?) ? '' : "#{tag[:tag_name]}: #{tag[:name]}"
26
+ "#{(tag[:types].empty?) ? '' : tag[:types]}#{self.get_separator(tag[:types], longest_types, tag[:types].empty? ? nil : ' ')}#{self.format_command_summary(combo_name, tag[:text], self.get_separator(combo_name, longest_combo_name), desc_width)}"
27
+ },
28
+ params: tag_list.select { |tag| tag[:tag_name] == "param" }.map { |tag|
29
+ "#{tag[:types]}#{self.get_separator(tag[:types], longest_types, ' ')}#{self.format_command_summary(tag[:name], tag[:text], self.get_separator(tag[:name], longest_name), desc_width)}"
30
+ },
31
+ returns: tag_list.select { |tag| tag[:tag_name] == "return" }.map { |tag|
32
+ "#{tag[:types]}#{self.get_separator(tag[:types], longest_types, ' ')}#{self.format_command_summary(tag[:tag_name], tag[:text], self.get_separator(tag[:tag_name], longest_name), desc_width)}"
33
+ }
34
+ }
35
+ end
36
+
37
+ # Arranges each command_name => command_description in command_doc with a separator such that all command names and
38
+ # descriptions will line up nicely in vertical columns.
39
+ #
40
+ # @param [Hash] command_doc a mapping of command names to documentation summaries
41
+ # @param [Integer] desc_width the maximum width to use for the description column
42
+ # @return [Array] a list of strings comprised of command_name, separator, and description
43
+ # formatted to line up vertically
44
+ def self.format_command_list(command_doc, desc_width = 90, indent ='')
45
+ return [] if command_doc.nil?
46
+ raise ArgumentError, "command_doc should be a Hash but was a #{command_doc.class}" unless command_doc.class == Hash
47
+ command_doc = {} if command_doc.nil?
48
+ longest_command_name = command_doc.keys.max { |t, n| t.to_s.length <=> n.to_s.length }
49
+ command_doc.map { |command_name, doc|
50
+ self.format_command_summary("#{indent}#{command_name}", doc, self.get_separator(command_name, longest_command_name), desc_width)
51
+ }
52
+ end
53
+
54
+ # Creates a separator with the appropriate spacing to line up a command/description pair in a command list
55
+ #
56
+ # @param [String] name the command name to create a doc separator for
57
+ # @param [String] longest_name the longest name which will be shown above or below the given name
58
+ # @param [String] sep the separator to use
59
+ # @return [String] a spaced separator String for use in a command/description list
60
+ def self.get_separator(name, longest_name='', sep=' - ')
61
+ name = "#{name}" unless name.class == String
62
+ sep = '' if sep.nil?
63
+ longest_name = name if name.size > longest_name.size
64
+ (' ' * (longest_name.to_s.length - name.to_s.length)) << sep
65
+ end
66
+
67
+ # Arranges the given command_name and command_description with the separator in a standard format
68
+ #
69
+ # @param [String] command_name the command format
70
+ # @param [String] command_description the description for the given command
71
+ # @param [String] separator optional separator to use
72
+ def self.format_command_summary(command_name, command_description, separator = ' - ', max_width = 90)
73
+ command_name = '' if command_name.nil?
74
+ command_description = '' if command_description.nil?
75
+ separator = '' if separator == nil
76
+ raise ArgumentError, "command_name and separator #{command_name}#{separator} size should not be greater than max_width: #{max_width} but was #{separator.size + command_name.size}" if command_name.size+separator.size > max_width
77
+ $stdout.sync = true
78
+ prefix_space = (' ' * "#{command_name}#{separator}".length)
79
+ line_width = max_width - prefix_space.length
80
+ "#{command_name}#{separator}#{self.word_wrap(command_description, line_width, prefix_space)}\n"
81
+ end
82
+
83
+ # Converts a string longer than line_width to a multiline string where each line is at most line_width long.
84
+ #
85
+ # @param [String] text the text to be wrapped
86
+ # @param [Integer] line_width the maximum length any single line in the string should be, default: 80, minimum: 1
87
+ # @param [String] prefix a prefix to add the the front of any new lines created as a result of the wrap, default: ''
88
+ def self.word_wrap(text, line_width=80, prefix='')
89
+ text = "#{text}" unless text.class == String
90
+ prefix = "#{prefix}" unless prefix.class == String
91
+ line_width = 1 if line_width < 1
92
+ ([text.gsub("\n", ' ')].map { |line|
93
+ line.length > line_width ? line.gsub(/(.{1,#{line_width}})(\s+|$)/, "\\1\n").strip : line
94
+ } * "\n").gsub("\n", "\n#{prefix}")
95
+ end
96
+
97
+ end
98
+ end
@@ -0,0 +1,15 @@
1
+ module Rubycom
2
+ module OutputHandler
3
+
4
+ # Prints the command_result if it is a basic type or a Yaml representation of command_result if it is not
5
+ # basic types: String, NilClass, TrueClass, FalseClass, Fixnum, Float, Symbol
6
+ #
7
+ # @param [Object] command_result the result of a method call to be printed
8
+ def self.process_output(command_result)
9
+ std_output = nil
10
+ std_output = command_result.to_yaml unless [String, NilClass, TrueClass, FalseClass, Fixnum, Float, Symbol].include?(command_result.class)
11
+ $stdout.puts std_output || command_result
12
+ end
13
+
14
+ end
15
+ end
@@ -0,0 +1,262 @@
1
+ module Rubycom
2
+ module ParameterExtract
3
+
4
+ # Calls #resolve_params with the given parameters after calling #check to assert the state of the inputs
5
+ #
6
+ # @param [Method] command the method whose parameters should be resolved
7
+ # @param [Hash] parsed_command_line :args => array of arguments, :opts => { opt_key => opt_val }, :flags => { flag_key => flag_val }
8
+ # @param [Hash] command_doc :parameters => an array consisting of a hash for method parameter where
9
+ # :param_name => the param name as a string,
10
+ # :type => :req|:opt|:rest,
11
+ # :default => the default value for the param
12
+ # @return [Hash] command.parameters.each => the value for that parameter extracted from parsed_command_line or the default in command_doc
13
+ def self.extract_parameters(command, parsed_command_line, command_doc)
14
+ command, parsed_command_line, command_doc = self.check(command, parsed_command_line, command_doc)
15
+ self.resolve_params(command, parsed_command_line, command_doc)
16
+ end
17
+
18
+ # Provides upfront checking for this inputs to #extract_parameters and raises a ParameterExtractError if
19
+ # parsed_command_line includes a help argument, option, or flag
20
+ #
21
+ # @param [Method] command the method whose parameters should be resolved
22
+ # @param [Hash] parsed_command_line :args => array of arguments, :opts => { opt_key => opt_val }, :flags => { flag_key => flag_val }
23
+ # @param [Hash] command_doc :parameters => an array consisting of a hash for method parameter where
24
+ # :param_name => the param name as a string,
25
+ # :type => :req|:opt|:rest,
26
+ # :default => the default value for the param
27
+ # @return [Array] the given parameters if none of the checks raised an error
28
+ def self.check(command, parsed_command_line, command_doc)
29
+ has_help_optional = false
30
+ command.parameters.select { |type, _| type == :opt }.map { |_, name| name.to_s }.each { |param|
31
+ has_help_optional = ['help', 'h'].include?(param)
32
+ } if command.class == Method
33
+ help_opt = !parsed_command_line[:opts].nil? && [
34
+ parsed_command_line[:opts]['help'],
35
+ parsed_command_line[:opts]['h']
36
+ ].include?(true)
37
+ help_flag = !parsed_command_line[:flags].nil? && [
38
+ parsed_command_line[:flags]['help'],
39
+ parsed_command_line[:flags]['h']
40
+ ].include?(true)
41
+ if !has_help_optional && (help_opt || help_flag)
42
+ raise ParameterExtractError, 'Help Requested'
43
+ end
44
+
45
+ raise ParameterExtractError, "No command specified." if command.nil?
46
+ raise ParameterExtractError, "No command specified." if command.class == Module
47
+ raise ParameterExtractError, "Unrecognized command." unless [Method, Module].include?(command.class)
48
+ raise "#{parsed_command_line} should be a Hash but was #{parsed_command_line.class}" if parsed_command_line.class != Hash
49
+
50
+ raise ArgumentError, "command_doc should be a Hash but was #{command_doc.class}" unless command_doc.class == Hash
51
+ raise ArgumentError, "command_doc should have key :parameters" unless command_doc.has_key?(:parameters)
52
+ raise ArgumentError, "command_doc[:parameters] should be an array but was #{command_doc[:parameters].class}" unless command_doc[:parameters].class == Array
53
+ command_doc[:parameters].each { |param_hsh|
54
+ raise ArgumentError, "parameter #{param_hsh} should be a Hash but was #{param_hsh.class}" unless param_hsh.class == Hash
55
+ raise ArgumentError, "parameter #{param_hsh} should have key :param_name" unless param_hsh.has_key?(:param_name)
56
+ raise ArgumentError, "parameter #{param_hsh} should have key :type" unless param_hsh.has_key?(:type)
57
+ raise ArgumentError, "parameter #{param_hsh} should have key :default" unless param_hsh.has_key?(:default)
58
+ }
59
+ [command, parsed_command_line, command_doc]
60
+ end
61
+
62
+ # Matches parameter names in command.parameters to values from command_line or their default values in command_doc
63
+ #
64
+ # @param [Method] command the method whose parameters should be resolved
65
+ # @param [Hash] command_line :args => array of arguments, :opts => { opt_key => opt_val }, :flags => { flag_key => flag_val }
66
+ # @param [Hash] command_doc :parameters => an array consisting of a hash for method parameter where
67
+ # :param_name => the param name as a string,
68
+ # :type => :req|:opt|:rest,
69
+ # :default => the default value for the param
70
+ # @return [Hash] command.parameters.each => the value for that parameter extracted from command_line or the default in command_doc
71
+ def self.resolve_params(command, command_line, command_doc)
72
+ raise ArgumentError, "command should be a Method but was #{command.class}" unless command.class == Method
73
+ command_line = command_line.clone.map { |type, entry|
74
+ {type => entry.clone}
75
+ }.reduce({}, &:merge)
76
+ command_line = self.extract_command_args!(command.name.to_s, command_line)
77
+ params = command.parameters
78
+ param_names = self.get_param_names(params)
79
+ raise ArgumentError, "command_doc should have key :parameters but was #{command_doc}" unless command_doc.has_key?(:parameters)
80
+ param_docs = command_doc[:parameters].map { |param_hsh|
81
+ if param_hsh[:type] == :rest
82
+ {param_hsh.fetch(:param_name).reverse.chomp('*').reverse.to_sym => param_hsh.reject { |k, _| k == :param_name }}
83
+ else
84
+ {param_hsh.fetch(:param_name).to_sym => param_hsh.reject { |k, _| k == :param_name }}
85
+ end
86
+ }.reduce({}, &:merge)
87
+
88
+ params.map { |type, sym|
89
+ case type
90
+ when :opt
91
+ unless param_docs.has_key?(sym) && param_docs[sym].has_key?(:default)
92
+ raise ArgumentError, "#{sym} should exist in command_doc[:parameters] and have key :default but has values #{param_docs[sym]}"
93
+ end
94
+ self.resolve_opt!(sym, param_names[sym][:long], param_names[sym][:short], param_docs[sym][:default], command_line)
95
+ when :rest
96
+ self.resolve_rest!(sym, param_docs[sym][:default], command_line)
97
+ else
98
+ self.resolve_others!(sym, type, param_docs[sym][:default], command_line)
99
+ end
100
+ }.reduce({}, &:merge).reject { |_, val| val == :rubycom_no_value }
101
+ end
102
+
103
+ # Trims command_line[:args] down to the entries which occur after command_name
104
+ #
105
+ # @param [Object] command_name the entry in command_line[:args] which marks the start of the args to be returned
106
+ # @param [Hash] command_line :args => array of arguments
107
+ # @return [Hash] :args => array of arguments including only the entries which occur after the command_name
108
+ def self.extract_command_args!(command_name, command_line)
109
+ raise ArgumentError, "command_name should be a String|Symbol but was #{command_name}" unless [String, Symbol].include?(command_name.class)
110
+ raise ArgumentError, "command_line should be a hash but was #{command_line}" unless command_line.class == Hash
111
+ return command_line if command_line[:args].nil?
112
+
113
+ i = command_line[:args].index(command_name.to_s)
114
+ command_line[:args] = (i.nil?) ? [] : command_line[:args][i..-1]
115
+ command_line[:args].shift if command_line[:args].first == command_name.to_s
116
+ command_line
117
+ end
118
+
119
+ # Extracts the a value from command_line for the param_name or returns the default with command_line has no values
120
+ #
121
+ # @param [Object] param_name the key in the returned hash
122
+ # @param [Object] default_value the value in the returned hash if no value could be extracted from command_line
123
+ # @param [Hash] command_line :args => array of arguments, :opts => { opt_key => opt_val }, :flags => { flag_key => flag_val }
124
+ # @return [Hash] param_name => extracted_value|default_value
125
+ def self.resolve_opt!(param_name, long_name, short_name, default_value, command_line)
126
+ raise ArgumentError, "command_line should be a hash but was #{command_line}" unless command_line.class == Hash
127
+ extraction = self.extract!(long_name, short_name, command_line[:opts], command_line[:flags], command_line[:args])
128
+ if extraction == :rubycom_no_value
129
+ {param_name => default_value}
130
+ else
131
+ {param_name => extraction}
132
+ end
133
+ end
134
+
135
+ # Creates a long and short name for each symbol in the given params
136
+ #
137
+ # @param [Array] params a list of Symbols to create names for
138
+ # @return [Hash] params.each symbol => a Hash where long => string form of symbol and
139
+ # short => the first char in symbol if unique in params or the string form of symbol if not
140
+ def self.get_param_names(params)
141
+ first_char_map = params.group_by { |_, sym| sym.to_s[0] }
142
+ params.map { |_, sym|
143
+ {
144
+ sym => {
145
+ long: sym.to_s,
146
+ short: (first_char_map[sym.to_s[0]].size == 1) ? sym.to_s[0] : sym.to_s
147
+ }
148
+ }
149
+ }.reduce({}, &:merge)
150
+ end
151
+
152
+ # Extracts the remaining values from command_line as an Array or returns the default with command_line has no values
153
+ #
154
+ # @param [Object] param_name the key in the returned hash
155
+ # @param [Object] default_value the value in the returned hash if no value could be extracted from command_line
156
+ # @param [Hash] command_line :args => array of arguments, :opts => { opt_key => opt_val }, :flags => { flag_key => flag_val }
157
+ # @return [Array] the rest of the keys/values in command_line
158
+ def self.resolve_rest!(param_name, default_value, command_line)
159
+ args = command_line[:args] || []
160
+ opts = command_line[:opts] || {}
161
+ flags = command_line[:flags] || {}
162
+ # TODO seems like we still can not call out a rest param on the command line, not sure if that is a problem
163
+ rest_arr = {
164
+ param_name => if args.empty? && opts.empty? && flags.empty?
165
+ default_value
166
+ elsif !args.empty? && opts.empty? && flags.empty?
167
+ args
168
+ elsif args.empty? && (!opts.empty? || !flags.empty?)
169
+ joined = self.join(flags, opts)
170
+ keyed = joined[param_name] || []
171
+ keyed.to_a << joined.reject { |k, _| k == param_name }
172
+ else
173
+ joined = self.join(flags, opts)
174
+ keyed = joined[param_name] || []
175
+ rest = keyed.to_a << joined.reject { |k, _| k == param_name }
176
+ args + rest
177
+ end
178
+ }
179
+
180
+ command_line[:opts] = {} unless command_line[:opts].nil?
181
+ command_line[:flags] = {} unless command_line[:flags].nil?
182
+ command_line[:args] = [] unless command_line[:args].nil?
183
+
184
+ rest_arr
185
+ end
186
+
187
+ # Calls #update on left passing in right and resolving conflicts by combining left and right values in an array
188
+ #
189
+ # @param [Hash] left the base thing to be updated
190
+ # @param [Hash] right the thing whose keys will be added to left and values combined with left on key collisions
191
+ # @return [Hash] left.keys + right.keys where each key => values from left or right or combined in an array if both
192
+ def self.join(left, right)
193
+ left.update(right) { |_, left_val, right_val|
194
+ if left_val.class == Array
195
+ combined = left_val
196
+ else
197
+ combined = [left_val]
198
+ end
199
+
200
+ if right_val.class == Array
201
+ right_val.each { |rv|
202
+ combined << rv
203
+ }
204
+ else
205
+ combined << right_val
206
+ end
207
+
208
+ combined
209
+ }
210
+ end
211
+
212
+ # Extracts a value from the command_line or returns the default_value if the command_line has no values.
213
+ # Raises a ParameterExtractError if the type was :req and no value was found.
214
+ #
215
+ # @param [Object] param_name the key in the returned hash
216
+ # @param [Symbol] type :req if the parameter is required
217
+ # @param [Object] default_value the value in the returned hash if no value could be extracted from command_line
218
+ # @param [Hash] command_line :args => array of arguments
219
+ # @return [Hash] param_name => value extracted for param_name
220
+ def self.resolve_others!(param_name, type, default_value, command_line)
221
+ if command_line[:args].size > 0
222
+ {param_name => (command_line[:args].shift)}
223
+ else
224
+ raise ParameterExtractError, "Missing required argument: #{param_name}" if type == :req
225
+ {param_name => default_value}
226
+ end
227
+ end
228
+
229
+ # Searches opts, then flags, then args for a key matching the given long_name or short_name
230
+ # The first matched key will be removed from the set. The valued paired to the matched key will be returned along
231
+ # with a hash containing the remaining args, opts, and flags.
232
+ # !destructively modifies opts, flags, and args by deleting or shifting a matched value out of the Hash/Array
233
+ #
234
+ # @param [Object] long_name the first key to be searched for in each opts, flags, args
235
+ # @param [Object] short_name the key to be searched for if the long_key is not found in the group under search
236
+ # @param [Hash] opts long_key|short_key => option value for that key
237
+ # @param [Hash] flags long_key|short_key => true|false value for that key
238
+ # @param [Array] args if no matching keys for the long or short name exist in either opts or flags and at least one
239
+ # value is left is args then the first value in args will be pulled as the value to return
240
+ # @return [Hash] the extracted value or :rubycom_no_value if a value could not be matched and args was empty
241
+ def self.extract!(long_name, short_name, opts, flags, args)
242
+ opts = {} if opts.nil?
243
+ flags = {} if flags.nil?
244
+ args = [] if args.nil?
245
+
246
+ if opts.has_key?(long_name)
247
+ opts.delete(long_name)
248
+ elsif opts.has_key?(short_name)
249
+ opts.delete(short_name)
250
+ elsif flags.has_key?(long_name)
251
+ flags.delete(long_name)
252
+ elsif flags.has_key?(short_name)
253
+ flags.delete(short_name)
254
+ elsif args.size > 0
255
+ args.shift
256
+ else
257
+ :rubycom_no_value
258
+ end
259
+ end
260
+
261
+ end
262
+ end
@@ -0,0 +1,78 @@
1
+ module Rubycom
2
+ module SingletonCommands
3
+
4
+ # Uses #discover_commands to look up commands in parsed_command_line, filters the result to the last matched command
5
+ # object
6
+ #
7
+ # @param [Module] base_module the module in which to search for commands
8
+ # @param [Hash] parsed_command_line :args => an array of strings representing the search terms
9
+ # @return [Module|Method] the last matched module or method object
10
+ def self.discover_command(base_module, parsed_command_line)
11
+ self.discover_commands(base_module, parsed_command_line).select { |candidate|
12
+ candidate.class == Module || candidate.class == Method
13
+ }.last
14
+ end
15
+
16
+ # Performs a depth only search of included modules starting with the base_module. The first word which matches a
17
+ # singleton method in one of the sub modules will be a Method object in the returned array. All matched sub modules
18
+ # will be Module objects in the returned array. All words occurring after a method match will be returned as they
19
+ # appear in parsed_command_line[:args]
20
+ #
21
+ # @param [Module] base_module the module in which to search for commands
22
+ # @param [Hash] parsed_command_line :args => an array of strings representing the search terms
23
+ # @return [Array] consisting of the matched sub Modules followed by the matched Method followed by the remaining args
24
+ def self.discover_commands(base_module, parsed_command_line)
25
+ base_module, args = self.check(base_module, parsed_command_line)
26
+ args.reduce([base_module]) { |acc, arg|
27
+ if acc.last.class == Method || acc.last.class == String
28
+ acc << arg
29
+ else
30
+ arg_sym = arg.to_s.to_sym
31
+ if self.get_commands(acc.last, false)[acc.last.to_s.to_sym][arg_sym] == :method
32
+ acc << acc.last.public_method(arg_sym)
33
+ else
34
+ acc << acc.last.const_get(arg_sym) rescue (acc << arg)
35
+ end
36
+ end
37
+ }
38
+ end
39
+
40
+ # Provides upfront checking for this inputs to #discover_commands
41
+ def self.check(base_module, parsed_command_line)
42
+ raise ArgumentError, 'base_module should not be nil' if base_module.nil?
43
+ raise ArgumentError, 'parsed_command_line should not be nil' if parsed_command_line.nil?
44
+ raise ArgumentError, "parsed_command_line should be a Hash but was #{parsed_command_line.class}" if parsed_command_line.class != Hash
45
+ arguments = parsed_command_line[:args] || []
46
+ raise ArgumentError, "args should be an Array but was #{arguments.class}" unless arguments.class == Array
47
+ unless [Module, String, Symbol].include?(base_module.class)
48
+ raise ArgumentError, "base_module should be a Module, String, or Symbol but was #{base_module.class}"
49
+ end
50
+ base_module = Kernel.const_get(base_module) if base_module.class == Symbol
51
+ base_module = Kernel.const_get(base_module.to_sym) if base_module.class == String
52
+ [base_module, arguments.map { |arg| arg.to_s } ]
53
+ end
54
+
55
+ # Retrieves the singleton methods in the given base and included Modules
56
+ #
57
+ # @param [Module] base the module which invoked 'include Rubycom'
58
+ # @param [Boolean] all if true recursively search for included modules' commands, if false return only top level commands
59
+ # @return [Hash] a Hash of Symbols representing the command methods in the given base and it's included modules (if all=true)
60
+ def self.get_commands(base, all=true)
61
+ return {} if base.nil? || !base.respond_to?(:singleton_methods) || !base.respond_to?(:included_modules)
62
+ {
63
+ base.to_s.to_sym => base.singleton_methods(true).select { |sym| ![:included, :extended].include?(sym) }.map { |sym|
64
+ {
65
+ sym => :method
66
+ }
67
+ }.reduce({}, &:merge).merge(
68
+ base.included_modules.select { |mod| mod.name.to_sym != :Rubycom }.map { |mod|
69
+ {
70
+ mod.to_s.to_sym => (all ? self.get_commands(mod, all)[mod.to_s.to_sym] : :module)
71
+ }
72
+ }.reduce({}, &:merge)
73
+ )
74
+ }
75
+ end
76
+
77
+ end
78
+ end