rubycom 0.1.1

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