rubycom 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +18 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +61 -0
- data/Rakefile +31 -0
- data/lib/rubycom.rb +421 -0
- data/lib/rubycom/version.rb +3 -0
- data/rubycom.gemspec +26 -0
- data/test/rubycom/test_rubycom.rb +630 -0
- data/test/rubycom/util_test_composite.rb +16 -0
- data/test/rubycom/util_test_module.rb +96 -0
- data/test/rubycom/util_test_no_singleton.rb +10 -0
- data/test/rubycom/utility_tester.rb +17 -0
- metadata +148 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -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.
|
data/README.md
ADDED
@@ -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>
|
data/Rakefile
ADDED
@@ -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
|
data/lib/rubycom.rb
ADDED
@@ -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
|