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.
Files changed (43) hide show
  1. checksums.yaml +8 -8
  2. data/README.md +162 -146
  3. data/Rakefile +12 -12
  4. data/lib/rubycom.rb +156 -226
  5. data/lib/rubycom/arg_parse.rb +252 -0
  6. data/lib/rubycom/command_interface.rb +97 -0
  7. data/lib/rubycom/completions.rb +62 -0
  8. data/lib/rubycom/error_handler.rb +15 -0
  9. data/lib/rubycom/executor.rb +23 -0
  10. data/lib/rubycom/helpers.rb +98 -0
  11. data/lib/rubycom/output_handler.rb +15 -0
  12. data/lib/rubycom/parameter_extract.rb +262 -0
  13. data/lib/rubycom/singleton_commands.rb +78 -0
  14. data/lib/rubycom/sources.rb +99 -0
  15. data/lib/rubycom/version.rb +1 -1
  16. data/lib/rubycom/yard_doc.rb +146 -0
  17. data/rubycom.gemspec +14 -16
  18. data/test/rubycom/arg_parse_test.rb +247 -0
  19. data/test/rubycom/command_interface_test.rb +293 -0
  20. data/test/rubycom/completions_test.rb +94 -0
  21. data/test/rubycom/error_handler_test.rb +72 -0
  22. data/test/rubycom/executor_test.rb +64 -0
  23. data/test/rubycom/helpers_test.rb +467 -0
  24. data/test/rubycom/output_handler_test.rb +76 -0
  25. data/test/rubycom/parameter_extract_test.rb +141 -0
  26. data/test/rubycom/rubycom_test.rb +290 -548
  27. data/test/rubycom/singleton_commands_test.rb +122 -0
  28. data/test/rubycom/sources_test.rb +59 -0
  29. data/test/rubycom/util_test_bin.rb +8 -0
  30. data/test/rubycom/util_test_composite.rb +23 -20
  31. data/test/rubycom/util_test_module.rb +142 -112
  32. data/test/rubycom/util_test_no_singleton.rb +2 -2
  33. data/test/rubycom/util_test_sub_module.rb +13 -0
  34. data/test/rubycom/yard_doc_test.rb +165 -0
  35. metadata +61 -24
  36. data/lib/rubycom/arguments.rb +0 -133
  37. data/lib/rubycom/commands.rb +0 -63
  38. data/lib/rubycom/documentation.rb +0 -212
  39. data/test/rubycom/arguments_test.rb +0 -289
  40. data/test/rubycom/commands_test.rb +0 -51
  41. data/test/rubycom/documentation_test.rb +0 -186
  42. data/test/rubycom/util_test_job.yaml +0 -21
  43. data/test/rubycom/utility_tester.rb +0 -17
@@ -0,0 +1,252 @@
1
+ module Rubycom
2
+ module ArgParse
3
+ require 'parslet'
4
+ require 'yaml'
5
+
6
+ # Runs a parser against the given Array of arguments to match command argument, option, and flag patterns.
7
+ #
8
+ # @param [Array] command_line an array of strings representing the arguments taken from the command line
9
+ # @return [Hash] :args => Array of arguments, :opts => Hash mapping each unique option/flag to their values
10
+ def self.parse_command_line(command_line)
11
+ raise ArgumentError, "command_line should be String or Array but was #{command_line.class}" unless [String, Array].include?(command_line.class)
12
+ command_line = command_line.dup
13
+ command_line = [command_line] if command_line.class == String
14
+ command_line = self.combine_options(command_line)
15
+ begin
16
+ command_line.map { |word|
17
+ ArgTransform.new.apply(ArgParser.new.parse(word))
18
+ }.reduce({}) { |acc, n|
19
+ # the handlers for opt and flag accumulate all unique mentions of an option name
20
+ if n.has_key?(:opt)
21
+ acc[:opts] = {} unless acc.has_key?(:opts)
22
+ acc[:opts] = acc[:opts].update(n[:opt]) { |key, old, new|
23
+ if old.class == Array
24
+ acc[:opts][key] = old << new
25
+ else
26
+ acc[:opts][key] = [old] << new
27
+ end
28
+ }
29
+ elsif n.has_key?(:flag)
30
+ acc[:opts] = {} unless acc.has_key?(:opts)
31
+ acc[:opts] = acc[:opts].update(n[:flag]) { |key, old, new|
32
+ if old.class == Array
33
+ combined = old
34
+ else
35
+ combined = [old]
36
+ end
37
+
38
+ if new.class == Array
39
+ new.each { |new_flag|
40
+ combined << new_flag
41
+ }
42
+ else
43
+ combined << new
44
+ end
45
+
46
+ acc[:opts][key] = combined
47
+ }
48
+ else
49
+ acc[:args] = [] unless acc.has_key?(:args)
50
+ acc[:args] << n[:arg]
51
+ end
52
+ acc
53
+ }
54
+ rescue Parslet::ParseFailed => failure
55
+ raise ArgParseError, "Arguments could not be parsed.", failure
56
+ end
57
+ end
58
+
59
+ # Matches a word representing an optional key to the separator and/or value which goes with the key
60
+ #
61
+ # @param [Array] command_line an array of strings representing the arguments taken from the command line
62
+ # @return [Array] an Array of Strings with matched items combined into one entry per matched set
63
+ def self.combine_options(command_line)
64
+ command_line.reduce([]) { |acc, next_word|
65
+ if next_word == '=' || (!next_word.start_with?('-') && acc.last.to_s.start_with?('-') && acc.last.to_s.end_with?('='))
66
+ acc[-1] = acc[-1].dup << next_word
67
+ acc
68
+ elsif next_word == '=' || (!next_word.start_with?('-') && acc.last.to_s.start_with?('-') && !(acc.last.to_s.include?('=') || acc.last.to_s.include?(' ')))
69
+ acc[-1] = acc[-1].dup << ' ' << next_word
70
+ acc
71
+ else
72
+ acc << next_word
73
+ end
74
+ }
75
+ end
76
+
77
+ # Comprised of grammar rules which determine whether a given string is an argument, option, or flag.
78
+ # Calling #parse with a String on an instance of this class will return a nested hash structure which identifies the
79
+ # patterns recognized in the given string.
80
+ # In order to use this parser on a command line argument array, it may be necessary to pre-join optional argument
81
+ # keys to their values such that each item in the array is either a complete argument, complete optional-argument,
82
+ # or a flag.
83
+ #
84
+ # Example:
85
+ # Rubycom::ArgParse::ArgParser.new.parse("-test_arg = test")
86
+ # => {:opt=>{:key=>"-test_arg"@0, :sep=>" = "@9, :val=>"test"@12}}
87
+ class ArgParser < Parslet::Parser
88
+ rule(:space) { match('\s').repeat(1) }
89
+ rule(:eq) { match('=') }
90
+ rule(:separator) { (eq | (space >> eq >> space) | (space >> eq) | (eq >> space) | space) }
91
+ rule(:escape_char) { match(/\\/) }
92
+ rule(:escaped_char) { escape_char >> any }
93
+ rule(:d_quote) { escape_char.absent? >> match(/"/) }
94
+ rule(:s_quote) { escape_char.absent? >> match(/'/) }
95
+
96
+ rule(:double_escaped) { d_quote >> (escape_char.absent? >> match(/[^"]/)).repeat >> d_quote }
97
+ rule(:single_escaped) { s_quote >> (escape_char.absent? >> match(/[^']/)).repeat >> s_quote }
98
+ rule(:escaped_word) { single_escaped | double_escaped }
99
+ rule(:raw_word) { (escaped_char | (separator.absent? >> any)).repeat(1) }
100
+ rule(:word) { raw_word | single_escaped | double_escaped }
101
+ rule(:list) { word >> (match(',') >> word).repeat(1) }
102
+
103
+ rule(:short) { match('-') }
104
+ rule(:long) { short >> short }
105
+ rule(:neg_opt_prefix) { (long | short) >> str('-').absent? >> (str('no-') | str('NO-')) >> word }
106
+ rule(:opt_prefix) { (long | short) >> str('-').absent? >> word }
107
+
108
+ rule(:arg) { any.repeat }
109
+ rule(:flag) { (neg_opt_prefix | opt_prefix) }
110
+ rule(:opt) { opt_prefix.as(:key) >> separator.as(:sep) >> any.repeat.as(:val) }
111
+
112
+ rule(:expression) { opt.as(:opt) | flag.as(:flag) | arg.as(:arg) }
113
+
114
+ root :expression
115
+ end
116
+
117
+ # Parslet transformer intended for use with ArgParser. Uses functions in Rubycom::ArgParse to clean up a structure
118
+ # identified by the parser and convert values to basic types.
119
+ #
120
+ # Example:
121
+ # ArgTransform.new.apply(ArgParser.new.parse("-test_arg = test"))
122
+ # => {:opt=>{"test_arg"=>"test"}}
123
+ class ArgTransform < Parslet::Transform
124
+ rule(:arg => simple(:arg)) { Rubycom::ArgParse.transform(:arg, arg) }
125
+ rule(:opt => subtree(:opt)) { Rubycom::ArgParse.transform(:opt, opt) }
126
+ rule(:flag => simple(:flag)) { Rubycom::ArgParse.transform(:flag, flag) }
127
+ end
128
+
129
+ # Calls one of the transform functions according to the matched_type
130
+ #
131
+ # @param [Symbol] matched_type :arg, :opt, :flag will transform the value according to the corresponding transform function.
132
+ # anything else will extract the value as a string
133
+ # @param [Hash|Parslet::Slice] val a possibly nested Hash structure or a Slice. Hashes are returned by the parser when
134
+ # it matches a complex pattern, a Slice will be returned when the matched pattern is not tree like.
135
+ # @return [Hash] :arg => value | :opt|:flag => a Hash mapping keys to values
136
+ def self.transform(matched_type, val, loaders={}, transformers={})
137
+ loader_methods = {
138
+ arg: Rubycom::ArgParse.public_method(:load_string),
139
+ opt: Rubycom::ArgParse.public_method(:load_opt_value),
140
+ flag: Rubycom::ArgParse.public_method(:load_flag_value)
141
+ }.merge(loaders)
142
+ transforms = {
143
+ arg: Rubycom::ArgParse.public_method(:transform_arg),
144
+ opt: Rubycom::ArgParse.public_method(:transform_opt),
145
+ flag: Rubycom::ArgParse.public_method(:transform_flag)
146
+ }.merge(transformers)
147
+
148
+ {
149
+ matched_type => if [:arg,:opt,:flag].include?(matched_type)
150
+ transforms[matched_type].call(val, loader_methods[matched_type])
151
+ else
152
+ val.str.strip
153
+ end
154
+ }
155
+ end
156
+
157
+ # Uses the given arg_loader to resolve the ruby type for the given string
158
+ #
159
+ # @param [String|Parslet::Slice] match_string a string identified as an argument
160
+ # @param [Method|Proc] arg_loader called to load the value(s)
161
+ # @return [Object] the result of a call to the given arg_loader
162
+ def self.transform_arg(match_string, arg_loader=Rubycom::ArgParse.public_method(:load_string))
163
+ match_string = match_string.str.strip if match_string.class == Parslet::Slice
164
+ arg_loader.call(match_string)
165
+ end
166
+
167
+ # Uses the given opt_loader to resolve the ruby type for the value in the given Hash
168
+ #
169
+ # @param [Hash] subtree a structure identified as an option, must have keys :key, :sep, :val
170
+ # @param [Method|Proc] opt_loader called to load the option value(s)
171
+ # @return [Hash] mapping the option key to it's loaded value
172
+ def self.transform_opt(subtree, opt_loader=Rubycom::ArgParse.public_method(:load_opt_value))
173
+ val = subtree[:val].str
174
+ val = val.split(',') unless (val.start_with?('[') && val.end_with?(']'))
175
+ value = opt_loader.call(val)
176
+ {
177
+ subtree[:key].str.reverse.chomp('-').chomp('-').reverse => value
178
+ }
179
+ end
180
+
181
+ # Calls the given loader to load a single string or array of strings
182
+ #
183
+ # @param [Array] value containing the string(s) to be loaded
184
+ # @param [Method|Proc] loader called to load the value(s)
185
+ # @return [Object] the result of a call to #load_string
186
+ def self.load_opt_value(value, loader=Rubycom::ArgParse.public_method(:load_string))
187
+ if value.class == Array
188
+ (value.length == 1) ? loader.call(value.first) : value.map { |v| loader.call(v) }
189
+ else
190
+ loader.call(value)
191
+ end
192
+ end
193
+
194
+ # Uses the given loader to resolve the ruby type for the given string
195
+ #
196
+ # @param [String] string to be loaded
197
+ # @param [Method|Proc] loader called to load the string
198
+ # @return [Object] the result of a call to the loader or the given string if it could not be parsed
199
+ def self.load_string(string, loader=YAML.public_method(:load))
200
+ if string.start_with?('#') || string.start_with?('!')
201
+ result = string
202
+ else
203
+ begin
204
+ result = loader.call(string)
205
+ rescue Exception
206
+ result = string
207
+ end
208
+ end
209
+ result
210
+ end
211
+
212
+ # Resolves the type and values for the given flag string
213
+ #
214
+ # @param [String|Parslet::Slice] match_string a string identified as a flag, should start with a - or -- and contain no spaces
215
+ # @return [Hash] flag_key(s) => true|false | an array of true|false if there were multiple mentions of the same short flag key
216
+ def self.transform_flag(match_string, loader=Rubycom::ArgParse.public_method(:load_flag_value))
217
+ match_string = match_string.str.strip if match_string.class == Parslet::Slice
218
+ if match_string.start_with?('--')
219
+ long_flag = match_string.reverse.chomp('-').chomp('-').reverse
220
+ long_flag_key = long_flag.sub(/no-|NO-/, '')
221
+ {
222
+ long_flag_key => loader.call(long_flag)
223
+ }
224
+ else
225
+ short_flag = match_string.reverse.chomp('-').reverse
226
+ short_flag_key = short_flag.sub(/no-|NO-/, '')
227
+ short_flag_key.split(//).map { |k|
228
+ {
229
+ k => loader.call(short_flag)
230
+ }
231
+ }.reduce({}) { |acc, n|
232
+ acc.update(n) { |_, old, new|
233
+ if old.class == Array
234
+ old << new
235
+ else
236
+ [old] << new
237
+ end
238
+ }
239
+ }
240
+ end
241
+ end
242
+
243
+ # Resolves the given flag to true or false as appropriate.
244
+ #
245
+ # @param [String] flag string representing a flag without the proceeding dashes
246
+ # @return [Boolean] FalseClass if the flag starts with a no- TrueClass otherwise
247
+ def self.load_flag_value(flag)
248
+ (flag.start_with?('no-') || flag.start_with?('NO-')) ? false : true
249
+ end
250
+
251
+ end
252
+ end
@@ -0,0 +1,97 @@
1
+ module Rubycom
2
+ module CommandInterface
3
+ require "#{File.dirname(__FILE__)}/helpers.rb"
4
+
5
+ # Uses #build_usage and #build_details to create a structured text output from the given command and doc hash
6
+ #
7
+ # @param [Module|Method|String] command the command to be named in the output
8
+ # @param [Hash] command_doc keys should include :full_doc and any keys required by #build_usage and #build_details
9
+ # @return [String] a structured string suitable for printing to the console as a command usage document
10
+ def self.build_interface(command, command_doc)
11
+ raise ArgumentError, "command should not be nil" if command.nil?
12
+ raise ArgumentError, "command_doc should not be nil" if command_doc.nil?
13
+ "#{self.build_usage(command, command_doc)}\n"+
14
+ "Description:\n"+
15
+ "#{command_doc.fetch(:full_doc, '').split("\n").map{|line| " #{line}"}.join("\n").chomp}\n"+
16
+ "#{self.build_details(command, command_doc)}"
17
+ end
18
+
19
+ # Uses #build_options to create a usage banner for use in a command usage document
20
+ #
21
+ # @param [Module|Method|String] command the command to be named in the output
22
+ # @param [Hash] command_doc keys should include any keys required by #build_options
23
+ # @return [String] a structured text representation of usage patterns for the given command and doc hash
24
+ def self.build_usage(command, command_doc)
25
+ return '' if command.nil?
26
+ command_use = if File.basename($0, File.extname($0)).gsub("_", '') == command.name.to_s.downcase ||
27
+ File.read($0).match(/(class|module)\s+#{command.name}/)
28
+ File.basename($0)
29
+ else
30
+ command.name.to_s
31
+ end
32
+ "Usage: #{command_use} #{self.build_options(command, command_doc)}"
33
+ end
34
+
35
+ # Creates a structured text representation of usage patterns for the given command and doc hash
36
+ #
37
+ # @param [Module|Method|String] command the class will be used to determine the overall usage pattern
38
+ # @param [Hash] command_doc keys should include :parameters and each parameter should be a hash including keys :type, :param_name, :default
39
+ # @return [String] a structured text representation of usage patterns for the given command and doc hash
40
+ def self.build_options(command, command_doc)
41
+ return '' if command_doc.nil?
42
+ if command.class == Module
43
+ "<command> [args]"
44
+ elsif command.class == Method
45
+ args, opts = command_doc.fetch(:parameters).map { |param|
46
+ if param.fetch(:type) == :req
47
+ "<#{param[:param_name]}>"
48
+ else
49
+ "[--#{param[:param_name]} #{param[:default]}]"
50
+ end
51
+ }.group_by { |p| p.start_with?('<') }.map { |_, group| group.join(' ') }
52
+ "#{args} #{opts}"
53
+ else
54
+ ""
55
+ end
56
+ end
57
+
58
+ # Creates a structured list of either sub commands or parameters based on the class of command
59
+ # Calls #build_tags if command is a Method
60
+ #
61
+ # @param [Module|Method|String] command the class will be used to determine the usage structure
62
+ # @param [Hash] command_doc keys should include :parameters and each parameter should be a hash including keys :type, :param_name, :default
63
+ # @return [String] a structured text representing a list of sub commands or a list of parameters
64
+ def self.build_details(command, command_doc)
65
+ if command.class == Module
66
+ sub_commands = Rubycom::Helpers.format_command_list(command_doc[:sub_command_docs], 90, ' ').join()
67
+ (sub_commands.empty?)? '' : "Sub Commands:\n#{sub_commands}"
68
+ elsif command.class == Method
69
+ tags = self.build_tags(Rubycom::Helpers.format_tags(command_doc[:tags]))
70
+ "#{tags[:others]}#{tags[:params]}#{tags[:returns]}"
71
+ else
72
+ ""
73
+ end
74
+ end
75
+
76
+ # Creates a structured text representing the given documentation tags
77
+ #
78
+ # @param [Hash] tags :params|:returns|:others => [Strings]
79
+ # @return [Hash] the given key => String representing the list of tags
80
+ def self.build_tags(tags)
81
+ return '' if tags.nil? || tags.empty?
82
+ tags.map{|k,val_arr|
83
+ val = val_arr.map { |line| " #{line}" }.join.chomp
84
+ {
85
+ k => if k == :params
86
+ (val.empty?)? '' : "\nParameters:\n#{val}"
87
+ elsif k == :returns
88
+ (val.empty?)? '' : "\nReturns:\n#{val}"
89
+ else
90
+ (val.empty?)? '' : "\nTags:\n#{val}"
91
+ end
92
+ }
93
+ }.reduce({}, &:merge)
94
+ end
95
+
96
+ end
97
+ end
@@ -0,0 +1,62 @@
1
+ module Rubycom
2
+ module Completions
3
+
4
+ # Discovers a list of possible matches to the given arguments
5
+ # Intended for use with bash tab completion
6
+ #
7
+ # @param [Module] base the module which invoked 'include Rubycom'
8
+ # @param [Array] arguments a String Array representing the arguments to be matched
9
+ # @param [Module] command_plugin the plugin to use for retrieving commands
10
+ # @return [Array] a String Array including the possible matches for the given arguments
11
+ def self.tab_complete(base, arguments, command_plugin)
12
+ return [] unless base.class == Module
13
+ return [] unless command_plugin.class == Module
14
+ arguments = [] if arguments.nil?
15
+ args = (arguments.include?('tab_complete')) ? arguments[2..-1] : arguments
16
+ matches = %w()
17
+ if args.nil? || args.empty?
18
+ matches = command_plugin.get_commands(base, false)[base.to_s.to_sym].map { |sym,_| sym.to_s }
19
+ elsif args.length == 1
20
+ matches = command_plugin.get_commands(base, false)[base.to_s.to_sym].map { |sym,_| sym.to_s }.select { |word| !word.match(/^#{args[0]}/).nil? }
21
+ if matches.size == 1 && matches[0] == args[0]
22
+ matches = self.tab_complete(Kernel.const_get(args[0].to_sym), args[1..-1], command_plugin)
23
+ end
24
+ elsif args.length > 1
25
+ begin
26
+ matches = self.tab_complete(Kernel.const_get(args[0].to_sym), args[1..-1], command_plugin)
27
+ rescue Exception
28
+ matches = %w()
29
+ end
30
+ end unless base.nil?
31
+ matches = %w() if matches.nil? || matches.include?(args[0])
32
+ matches
33
+ end
34
+
35
+ # Inserts a tab completion into the current user's .bash_profile with a command entry to register the function for
36
+ # the current running ruby file
37
+ #
38
+ # @param [Module] base the module which invoked 'include Rubycom'
39
+ # @return [String] a message indicating the result of the command
40
+ def self.register_completions(base)
41
+ completion_function = <<-END.gsub(/^ {6}/, '')
42
+
43
+ _#{base}_complete() {
44
+ COMPREPLY=()
45
+ local completions="$(ruby #{File.absolute_path($0)} tab_complete ${COMP_WORDS[*]} 2>/dev/null)"
46
+ COMPREPLY=( $(compgen -W "$completions") )
47
+ }
48
+ complete -o bashdefault -o default -o nospace -F _#{base}_complete #{$0.split('/').last}
49
+ END
50
+
51
+ already_registered = File.readlines("#{Dir.home}/.bash_profile").map { |line| line.include?("_#{base}_complete()") }.reduce(:|) rescue false
52
+ if already_registered
53
+ "Completion function for #{base} already registered."
54
+ else
55
+ File.open("#{Dir.home}/.bash_profile", 'a+') { |file|
56
+ file.write(completion_function)
57
+ }
58
+ "Registration complete, run 'source #{Dir.home}/.bash_profile' to enable auto-completion."
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,15 @@
1
+ module Rubycom
2
+ module ErrorHandler
3
+
4
+ # Prints the error followed by the command line interface text
5
+ #
6
+ # @param [Error] e the error to be printed
7
+ # @param [String] cli_output the command line interface text to be printed
8
+ def self.handle_error(e, cli_output)
9
+ $stderr.puts e
10
+ $stderr.puts
11
+ $stderr.puts cli_output
12
+ end
13
+
14
+ end
15
+ end
@@ -0,0 +1,23 @@
1
+ module Rubycom
2
+ module Executor
3
+
4
+ # Calls the given method with the given parameters
5
+ #
6
+ # @param [Method] method the Method to call
7
+ # @param [Hash] parameters a Hash mapping parameter names to their intended values
8
+ # @return the result of the Method call
9
+ def self.execute_command(method, parameters={})
10
+ raise "#{method} should be a Method but was #{method.class}" if method.class != Method
11
+ raise "#{parameters} should be a Hash but was #{parameters.class}" if parameters.class != Hash
12
+ params = method.parameters.reject{|type,_|type == :rest}.map { |_, sym|
13
+ raise ExecutorError, "parameters should include values for all non * method parameters. Missing value for #{sym.to_s}" unless parameters.has_key?(sym)
14
+ parameters[sym]
15
+ }
16
+ unless method.parameters.select{|type,_|type == :rest}.first.nil? #if there is a * param
17
+ params = params + parameters[method.parameters.select{|type,_|type == :rest}.first[1]] #add in the values which were marked for the * param
18
+ end
19
+ (parameters.nil? || parameters.empty?) ? method.call : method.call(*params)
20
+ end
21
+
22
+ end
23
+ end