rubycom 0.1.1

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,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ .idea/*
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in rubycom.gemspec
4
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Danny Purcell
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,61 @@
1
+ Rubycom
2
+ ---------------
3
+
4
+ © Danny Purcell 2013 | MIT license
5
+
6
+ Makes creating command line tools as easy as writing a function library.
7
+
8
+ When a module is run from the terminal and includes Rubycom, Rubycom will parse ARGV for a command name,
9
+ match the command name to a public singleton method (self.method_name()) in the including module, and run the method
10
+ with the given arguments.
11
+
12
+ Features
13
+ ---------------
14
+
15
+ Allows the user to write a properly documented module/class as a function library and convert it to a command line tool
16
+ by simply including Rubycom at the bottom.
17
+
18
+ * Provides a Command Line Interface for any function library simply by stating `include Rubycom` at the bottom.
19
+ * Public singleton methods are made accessible from the terminal. Usage documentation is pulled from method comments.
20
+ * Method parameters become required CLI arguments. Optional (defaulted) parameters become CLI options.
21
+ * Command consoles can be built up by including other modules before including Rubycom.
22
+ * Included modules become commands, their public singleton methods become sub-commands.
23
+
24
+
25
+ Raison d'etre
26
+ ---------------
27
+
28
+ * From scratch command line scripts often include redundant ARGV parsing code, little to no testing, slim documentation.
29
+ * OptionParser and the like help script authors define options for a script.
30
+ They provide structure to the redundant code and slightly easier argument parsing.
31
+ * Thor and the like provide a framework the script author will extend to create command line tools.
32
+ Prescriptive approach creates consistency but requires the script author to learn the framework and conform.
33
+
34
+ While these are things are nice, we are still writing redundant code and
35
+ tightly coupling the functional code to the interface which presents it.
36
+
37
+ At it's core a terminal command is a function. Rather than requiring the authors to make concessions for the presentation and
38
+ tightly couple the functional code to the interface, it would be nice if the author could simply write a function library
39
+ and attach the interface to it.
40
+
41
+ How it works
42
+ ---------------
43
+ Rubycom attaches the CLI to the functional code. The author is free to write the functional code as any other.
44
+ If a set of functions needs to be accessible from the terminal, just `include Rubycom` at the bottom and run the ruby file.
45
+
46
+ * Public singleton methods are made accessible from the terminal.
47
+ * ARGV is parsed for a method to run and arguments.
48
+ * Usage documentation is pulled from method comments.
49
+ * Method parameters become required CLI arguments.
50
+ * Optional (defaulted) parameters become CLI options.
51
+
52
+ The result is a function library which can be consumed easily from other classes/modules and which is accessible from the command line.
53
+
54
+ Coming Soon
55
+ ---------------
56
+ * Run Pre-configured sets of commands from a yaml file by calling <script.rb> job <job_yaml>
57
+ * Job help/usage output will include descriptions from command for each step
58
+ * Build a job yaml by running each command in sequence with a special option --job_add <path_to_yaml>
59
+ * Edit job files from the command line using special options.
60
+ * --job_update <path_to_yaml>
61
+ * --job_rm <path_to_yaml>
@@ -0,0 +1,31 @@
1
+ require "bundler/gem_tasks"
2
+ require 'rake/testtask'
3
+ require 'yard'
4
+
5
+ task :default => [:test, :yard, :package]
6
+
7
+ task :test do
8
+ test_files = Dir.glob("**/test/*/test_*.rb")
9
+ test_files.each { |test_case|
10
+ ruby test_case rescue SystemExit
11
+ if $?.exitstatus != 0; raise "Error during test phase\n Test: #{test_case}\n Error: #{$!}\n#{$@}" unless $!.nil? end
12
+ }
13
+ end
14
+
15
+ YARD::Rake::YardocTask.new
16
+
17
+ task :package => [:test, :yard] do
18
+ gem_specs = Dir.glob("**/*.gemspec")
19
+ gem_specs.each { |gem_spec|
20
+ system("gem build #{gem_spec}")
21
+ raise "Error during build phase" if $?.exitstatus != 0
22
+ }
23
+ end
24
+
25
+ task :install => :package do
26
+ system "gem install ./rubycom-#{Rubycom::VERSION}"
27
+ end
28
+
29
+ task :release => :package do
30
+ system "gem push rubycom-#{Rubycom::VERSION}"
31
+ end
@@ -0,0 +1,421 @@
1
+ require "#{File.expand_path(File.dirname(__FILE__))}/rubycom/version.rb"
2
+ require 'yaml'
3
+ require 'method_source'
4
+
5
+ # Upon inclusion in another Module, Rubycom will attempt to call a method in the including module by parsing
6
+ # ARGV for a method name and a list of arguments.
7
+ # If found Rubycom will call the method specified in ARGV with the parameters parsed from the remaining arguments
8
+ # If a Method match can not be made, Rubycom will print help instead by parsing source comments from the including
9
+ # module or it's included modules.
10
+ module Rubycom
11
+ class CLIError < StandardError;end
12
+
13
+ # Detects that Rubycom was included in another module and calls Rubycom#run
14
+ #
15
+ # @param [Module] base the module which invoked 'include Rubycom'
16
+ def self.included(base)
17
+ raise CLIError, 'base must be a module' if base.class != Module
18
+ base_file_path = caller.first.gsub(/:\d+:.+/, '')
19
+ if base_file_path == $0
20
+ base.module_eval {
21
+ Rubycom.run(base, ARGV)
22
+ }
23
+ end
24
+ end
25
+
26
+ # Looks up the command specified in the first arg and executes with the rest of the args
27
+ #
28
+ # @param [Module] base the module which invoked 'include Rubycom'
29
+ # @param [Array] args a String Array representing the command to run followed by arguments to be passed
30
+ def self.run(base, args=[])
31
+ begin
32
+ raise CLIError, "Invalid base class invocation: #{base}" if base.nil?
33
+ command = args[0] || nil
34
+ arguments = args[1..-1] || []
35
+
36
+ if command == 'help'
37
+ help_topic = arguments[0]
38
+ if help_topic.nil?
39
+ usage = self.get_usage(base)
40
+ puts usage
41
+ return usage
42
+ else
43
+ cmd_usage = self.get_command_usage(base, help_topic, arguments[1..-1])
44
+ puts cmd_usage
45
+ return cmd_usage
46
+ end
47
+ else
48
+ output = self.run_command(base, command, arguments)
49
+ std_output = nil
50
+ std_output = output.to_yaml unless [String, NilClass, TrueClass, FalseClass, Fixnum, Float, Symbol].include?(output.class)
51
+ puts std_output || output
52
+ return output
53
+ end
54
+
55
+ rescue CLIError => e
56
+ abort "#{e}\n#{self.get_summary(base)}"
57
+ end
58
+ end
59
+
60
+ # Calls the given Method#name on the given Module after parsing the given Array of arguments
61
+ #
62
+ # @param [Module] base the module which invoked 'include Rubycom'
63
+ # @param [String] command the name of the Method to call
64
+ # @param [Array] arguments a String Array representing the arguments for the given command
65
+ def self.run_command(base, command, arguments=[])
66
+ raise CLIError, 'No command specified.' if command.nil? || command.length == 0
67
+ command_sym = command.to_sym
68
+ valid_commands = self.get_top_level_commands(base)
69
+ raise CLIError, "Invalid Command: #{command}" unless valid_commands.include? command_sym
70
+ if base.included_modules.map { |mod| mod.name.to_sym }.include?(command.to_sym)
71
+ self.run_command(eval(command), arguments[0], arguments[1..-1])
72
+ else
73
+ method = base.public_method(command_sym)
74
+ raise CLIError, "No public method found for symbol: #{command_sym}" if method.nil?
75
+ parameters = self.get_param_definitions(method)
76
+ params_hash = self.parse_arguments(parameters, arguments)
77
+ params = []
78
+ method.parameters.each { |type, name|
79
+ if type == :rest
80
+ if params_hash[name].class == Array
81
+ params_hash[name].each { |arg|
82
+ params << arg
83
+ }
84
+ else
85
+ params << params_hash[name]
86
+ end
87
+ else
88
+ params << params_hash[name]
89
+ end
90
+ }
91
+ if arguments.nil? || arguments.empty?
92
+ output = method.call
93
+ else
94
+ output = method.call(*params)
95
+ end
96
+ output
97
+ end
98
+ end
99
+
100
+ # Parses the given arguments and matches them to the given parameters
101
+ #
102
+ # @param [Hash] parameters a Hash representing the parameters to match.
103
+ # Entries should match :param_name => { type: :req||:opt||:rest,
104
+ # def:(source_definition),
105
+ # default:(default_value || :nil_rubycom_required_param)
106
+ # }
107
+ # @param [Array] arguments an Array of Strings representing the arguments to be parsed
108
+ # @return [Hash] a Hash mapping the defined parameters to their matching argument values
109
+ def self.parse_arguments(parameters={}, arguments=[])
110
+ arguments = (!arguments.nil? && arguments.respond_to?(:each)) ? arguments : []
111
+ args_l = arguments.length
112
+ req_l = 0
113
+ opt_l = 0
114
+ has_rest_param = false
115
+ parameters.each_value { |def_hash|
116
+ req_l += 1 if def_hash[:type] == :req
117
+ opt_l += 1 if def_hash[:type] == :opt
118
+ has_rest_param = true if def_hash[:type] == :rest
119
+ def_hash[:default] = self.parse_arg(def_hash[:default])[:arg] unless def_hash[:default] == :nil_rubycom_required_param
120
+ }
121
+ raise CLIError, "Wrong number of arguments. Expected at least #{req_l}, received #{args_l}" if args_l < req_l
122
+ unless has_rest_param
123
+ raise CLIError, "Wrong number of arguments. Expected at most #{req_l + opt_l}, received #{args_l}" if args_l > (req_l + opt_l)
124
+ end
125
+
126
+ args = []
127
+ arguments.each { |arg|
128
+ args << self.parse_arg(arg)
129
+ }
130
+
131
+ parsed_args = []
132
+ parsed_options = {}
133
+ args.each { |item|
134
+ key = item.keys.first
135
+ val = item.values.first
136
+ if key == :arg
137
+ parsed_args << val
138
+ else
139
+ parsed_options[key]=val
140
+ end
141
+ }
142
+
143
+ result_hash = {}
144
+ parameters.each { |param_name, def_hash|
145
+ if def_hash[:type] == :req
146
+ raise CLIError, "No argument available for #{param_name}" if parsed_args.length == 0
147
+ result_hash[param_name] = parsed_args.shift
148
+ elsif def_hash[:type] == :opt
149
+ result_hash[param_name] = parsed_options[param_name]
150
+ result_hash[param_name] = parsed_args.shift if result_hash[param_name].nil?
151
+ result_hash[param_name] = parameters[param_name][:default] if result_hash[param_name].nil?
152
+ elsif def_hash[:type] == :rest
153
+ if parsed_options[param_name].nil?
154
+ result_hash[param_name] = parsed_args
155
+ parsed_args = []
156
+ else
157
+ result_hash[param_name] = parsed_options[param_name]
158
+ end
159
+ end
160
+ }
161
+ result_hash
162
+ end
163
+
164
+ # Uses YAML.load to parse the given String
165
+ #
166
+ # @param [String] arg a String representing the argument to be parsed
167
+ # @return [Object] the result of parsing the given arg with YAML.load
168
+ def self.parse_arg(arg)
169
+ param_name = 'arg'
170
+ arg_val = "#{arg}"
171
+ result = {}
172
+ return result[param_name.to_sym]=nil if arg.nil?
173
+ if arg.is_a? String
174
+ raise CLIError, "Improper option specification, options must start with one or two dashes. Received: #{arg}" if (arg.match(/^[-]{3,}\w+/) != nil)
175
+ if arg.match(/^[-]{1,}\w+/) == nil
176
+ raise CLIError, "Improper option specification, options must start with one or two dashes. Received: #{arg}" if (arg.match(/^\w+=/) != nil)
177
+
178
+ else
179
+ if arg.match(/^--/) != nil
180
+ arg = arg.reverse.chomp('--').reverse
181
+ elsif arg.match(/^-/) != nil
182
+ arg = arg.reverse.chomp('-').reverse
183
+ end
184
+
185
+ if arg.match(/^\w+=/) != nil
186
+ arg_arr = arg.split('=')
187
+ param_name = arg_arr.shift.strip
188
+ arg_val = arg_arr.join('=').lstrip
189
+ elsif arg.match(/^\w+\s+\S+/) != nil
190
+ arg_arr = arg.split(' ')
191
+ param_name = arg_arr.shift
192
+ arg_val = arg_arr.join(' ')
193
+ end
194
+ end
195
+ end
196
+
197
+ val = YAML.load(arg_val) rescue nil
198
+ if val.nil?
199
+ result[param_name.to_sym] = "#{arg_val}"
200
+ else
201
+ result[param_name.to_sym] = val
202
+ end
203
+ result
204
+ end
205
+
206
+ # Retrieves the summary for each command method in the given Module
207
+ #
208
+ # @param [Module] base the module which invoked 'include Rubycom'
209
+ # @return [String] the summary for each command method in the given Module
210
+ def self.get_summary(base)
211
+ longest_name_length = self.get_longest_command_name(base).length
212
+ self.get_top_level_commands(base).each_with_index.map { |sym, index|
213
+ separator = self.get_separator(sym, longest_name_length)
214
+ if index == 0
215
+ "Commands:\n" << self.get_command_summary(base, sym, separator)
216
+ else
217
+ self.get_command_summary(base, sym, separator)
218
+ end
219
+ }.reduce(:+) or "No Commands found for #{base}."
220
+ end
221
+
222
+ def self.get_separator(sym, spacer_length=0)
223
+ cmd_name = sym.to_s
224
+ sep_length = spacer_length - cmd_name.length
225
+ separator = ""
226
+ sep_length.times {
227
+ separator << " "
228
+ }
229
+ separator << " - "
230
+ end
231
+
232
+ # Retrieves the summary for the given command_name
233
+ #
234
+ # @param [Module] base the module which invoked 'include Rubycom'
235
+ # @param [String] command_name the command to retrieve usage for
236
+ # @return [String] a summary of the given command_name
237
+ def self.get_command_summary(base, command_name, separator = ' - ')
238
+ raise CLIError, "Can not get usage for #{command_name} with base: #{base||"nil"}" if base.nil? || !base.respond_to?(:included_modules)
239
+ return 'No command specified.' if command_name.nil? || command_name.length == 0
240
+ if base.included_modules.map { |mod| mod.name.to_sym }.include?(command_name.to_sym)
241
+ desc = "Sub-Module-Command"
242
+ else
243
+ raise CLIError, "Invalid command for #{base}, #{command_name}" unless base.public_methods.include?(command_name.to_sym)
244
+ m = base.public_method(command_name.to_sym)
245
+ method_doc = self.get_doc(m)
246
+ desc = method_doc[:desc].join("\n")
247
+ end
248
+ (desc.nil?||desc=='nil'||desc.length==0) ? "#{command_name}\n" : self.get_formatted_summary(command_name, desc, separator)
249
+ end
250
+
251
+ def self.get_formatted_summary(command_name, command_description, separator = ' - ')
252
+ width = 95
253
+ spacer = ""
254
+ command_name.to_s.split(//).each {
255
+ spacer << " "
256
+ }
257
+ sep_space = ""
258
+ separator.split(//).each {
259
+ sep_space << " "
260
+ }
261
+ prefix = "#{spacer}#{sep_space}"
262
+ line_width = width - prefix.length
263
+ description_msg = command_description.gsub(/(.{1,#{line_width}})(?: +|$)\n?|(.{#{line_width}})/, "#{prefix}"+'\1\2'+"\n")
264
+ "#{command_name}#{separator}#{description_msg.lstrip}"
265
+ end
266
+
267
+ # Retrieves the usage description for the given Module with a list of command methods
268
+ #
269
+ # @param [Module] base the module which invoked 'include Rubycom'
270
+ # @return [String] the usage description for the module with a list of command methods
271
+ def self.get_usage(base)
272
+ return '' if base.nil? || !base.respond_to?(:included_modules)
273
+ return '' if self.get_top_level_commands(base).size == 0
274
+ "Usage:\n #{base} <command> [args]\n\n" << self.get_summary(base)
275
+ end
276
+
277
+ # Retrieves the usage description for the given command_name
278
+ #
279
+ # @param [Module] base the module which invoked 'include Rubycom'
280
+ # @param [String] command_name the command to retrieve usage for
281
+ # @param [Array] args the remaining args other than the command_name, used of sub-command look-ups
282
+ # @return [String] the detailed usage description for the given command_name
283
+ def self.get_command_usage(base, command_name, args=[])
284
+ raise CLIError, "Can not get usage for #{command_name} with base: #{base||"nil"}" if base.nil? || !base.respond_to?(:included_modules)
285
+ return 'No command specified.' if command_name.nil? || command_name.length == 0
286
+ if base.included_modules.map { |mod| mod.name.to_sym }.include?(command_name.to_sym)
287
+ if args.empty?
288
+ self.get_usage(eval(command_name.to_s))
289
+ else
290
+ self.get_command_usage(eval(command_name.to_s), args[0], args[1..-1])
291
+ end
292
+ else
293
+ raise CLIError, "Invalid command for #{base}, #{command_name}" unless base.public_methods.include?(command_name.to_sym)
294
+ m = base.public_method(command_name.to_sym)
295
+ method_doc = self.get_doc(m)
296
+
297
+ <<-END.gsub(/^ {6}/, '')
298
+ Usage: #{m.name} #{self.get_param_usage(m)}
299
+ #{"Parameters:" unless m.parameters.empty?}
300
+ #{method_doc[:param].join("\n ") unless method_doc[:param].nil?}
301
+ Returns:
302
+ #{method_doc[:return].join("\n ") rescue 'void'}
303
+ END
304
+ end
305
+ end
306
+
307
+ def self.get_param_usage(method)
308
+ method.parameters.map { |type, param| {type => param}
309
+ }.group_by { |entry| entry.keys.first
310
+ }.map { |key, val| Hash[key, val.map { |param| param.values.first }]
311
+ }.reduce(&:merge).map { |type, arr|
312
+ if type == :req
313
+ Hash[type, arr.map { |param| " <#{param.to_s}>" }.reduce(:+)]
314
+ elsif type == :opt
315
+ Hash[type, "[#{arr.map { |param| "-#{param}=val" }.join("|")}]"]
316
+ else
317
+ Hash[type, "[&#{arr.join(',')}]"]
318
+ end
319
+ }.reduce(&:merge).values.join(" ")
320
+ end
321
+
322
+ # Builds a hash mapping parameter names (as symbols) to their
323
+ # :type (:req,:opt,:rest), :def (source_definition), :default (default_value || :nil_rubycom_required_param)
324
+ # for each parameter defined by the given method.
325
+ #
326
+ # @param [Method] method the Method who's parameter hash should be built
327
+ # @return [Hash] a Hash representing the given Method's parameters
328
+ def self.get_param_definitions(method)
329
+ raise CLIError, 'method must be an instance of the Method class' unless method.class == Method
330
+ source = method.source
331
+ method_name = method.name.to_s
332
+ source_lines = source.split("\n")
333
+ param_names = method.parameters.map { |param| param[1].to_s }
334
+ param_types = {}
335
+ method.parameters.each { |type, name| param_types[name] = type }
336
+ param_def_lines = {}
337
+ param_names.each { |name| param_def_lines[name] = source_lines.select { |line| line.include?(name) }.first }
338
+ param_definitions = {}
339
+ param_def_lines.each { |name, param_def_line|
340
+ param_candidates = param_def_line.gsub(/(def\s+self\.#{method_name}|def\s+#{method_name})/, '').lstrip.chomp.chomp(')').reverse.chomp('(').reverse
341
+ param_definitions[name.to_sym] = {}
342
+ param_definitions[name.to_sym][:def] = param_candidates.split(',').select { |candidate| candidate.include?(name) }.first
343
+ param_definitions[name.to_sym][:type] = param_types[name.to_sym]
344
+ if param_definitions[name.to_sym][:def].include?('=')
345
+ param_definitions[name.to_sym][:default] = param_definitions[name.to_sym][:def].split('=')[1..-1].join('=')
346
+ else
347
+ param_definitions[name.to_sym][:default] = :nil_rubycom_required_param
348
+ end
349
+ }
350
+ param_definitions
351
+ end
352
+
353
+ # Retrieves the given method's documentation from it's source code.
354
+ #
355
+ # @param [Method] method the Method who's documentation should be retrieved
356
+ # @return [Hash] a Hash representing the given Method's documentation, documentation parsed as follows:
357
+ # :desc = the first general method comment, :params = each @param comment, :return = each @return comment,
358
+ # :extended = all other general method comments and unrecognized annotations
359
+ def self.get_doc(method)
360
+ method.comment.split("\n").map { |line|
361
+ line.gsub(/#\s*/, '') }.group_by { |doc|
362
+ if doc.match(/^@\w+/).nil?
363
+ :desc
364
+ else
365
+ doc.match(/^@\w+/).to_s.gsub('@', '').to_sym
366
+ end
367
+ }.map { |key, val|
368
+ Hash[key, val.map { |val_line| val_line.gsub(/^@\w+/, '').lstrip }.select { |line| line != '' }]
369
+ }.reduce(&:merge)
370
+ end
371
+
372
+ def self.get_longest_command_name(base)
373
+ return '' if base.nil?
374
+ self.get_commands(base, false).map { |_, mod_hash|
375
+ mod_hash[:commands] + mod_hash[:inclusions].flatten }.flatten.max_by(&:size) or ''
376
+ end
377
+
378
+ # Retrieves the singleton methods in the given base
379
+ #
380
+ # @param [Module] base the module which invoked 'include Rubycom'
381
+ # @param [Boolean] all if true recursively search for included modules' commands, if false return only top level commands.
382
+ # @return [Hash] a Hash of Symbols representing the command methods in the given base and it's included modules (if all=true)
383
+ def self.get_commands(base, all=true)
384
+ return {} if base.nil? || !base.respond_to?(:singleton_methods) || !base.respond_to?(:included_modules)
385
+ excluded_commands = [:included, :extended]
386
+ excluded_modules = [:Rubycom]
387
+ {
388
+ base.name.to_sym => {
389
+ commands: base.singleton_methods(true).select { |sym| !excluded_commands.include?(sym) },
390
+ inclusions: base.included_modules.select { |mod| !excluded_modules.include?(mod.name.to_sym) }.map { |mod|
391
+ if all
392
+ self.get_commands(mod)
393
+ else
394
+ mod.name.to_sym
395
+ end
396
+ }
397
+ }
398
+ }
399
+ end
400
+
401
+ def self.get_top_level_commands(base)
402
+ return {} if base.nil? || !base.respond_to?(:singleton_methods) || !base.respond_to?(:included_modules)
403
+ excluded_commands = [:included, :extended]
404
+ excluded_modules = [:Rubycom]
405
+ base.singleton_methods(true).select { |sym| !excluded_commands.include?(sym) } +
406
+ base.included_modules.select { |mod| !excluded_modules.include?(mod.name.to_sym) }.map { |mod| mod.name.to_sym }.flatten
407
+ end
408
+
409
+ def self.index_commands(base)
410
+ excluded_commands = [:included, :extended]
411
+ excluded_modules = [:Rubycom]
412
+ Hash[base.singleton_methods(true).select { |sym| !excluded_commands.include?(sym) }.map { |sym|
413
+ [sym, base]
414
+ }].merge(
415
+ base.included_modules.select { |mod| !excluded_modules.include?(mod.name.to_sym) }.map { |mod|
416
+ self.index_commands(mod)
417
+ }.reduce(&:merge) || {}
418
+ )
419
+ end
420
+
421
+ end