rubycom 0.3.2 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
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