rubycom 0.3.2 → 0.4.0
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.
- 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
|