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.
- 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
|