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