rubycom 0.1.1 → 0.2.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.
data/README.md CHANGED
@@ -21,6 +21,89 @@ by simply including Rubycom at the bottom.
21
21
  * Command consoles can be built up by including other modules before including Rubycom.
22
22
  * Included modules become commands, their public singleton methods become sub-commands.
23
23
 
24
+ Usage
25
+ ---------------
26
+
27
+ Write your module of methods, document them as you normally would. `include Rubycom` at the bottom.
28
+ Optionally `#!/usr/bin/env ruby` at the top.
29
+
30
+ Now any singleton methods `def self.method_name` will be available to call from the terminal.
31
+
32
+ Calling `ruby ./path/to/module.rb <command_name>` will automatically discover and run your `<command_name>` singleton method.
33
+ If no method is found by the given name, a usage print out will be given including a summary of each command available
34
+ and it's description from the corresponding method's comments.
35
+
36
+ Calling a valid command with incorrect arguments will produce a usage print out for the matched method.
37
+ Rubycom will include as much documentation on the command line as you provide in your method comments. Currently Rubycom
38
+ only handles @param and @return annotation style comments for discovering the method comments regarding params and return values.
39
+ All other commentary will be included as part of the command description. In the absence of @param or @return comments,
40
+ Rubycom will also leave off the corresponding Param: and Return: markers in the usage output.
41
+
42
+ #####Special commands
43
+
44
+ | Command | Description | Options |
45
+ | ------- |:-----------:| -------:|
46
+ | `ruby ./path/to/module.rb help [command_name]` | Will print out usage for the module or optionally the specified command.||
47
+ | `ruby ./path/to/module.rb job </path/to/job.yaml>` | Runs the specified job file. See: "Jobs" below. | `--test` Prints the steps and context without running the commands. |
48
+
49
+
50
+ ###Arguments
51
+
52
+ * Arguments are automatically parsed from the command line using Ruby's core Yaml module.
53
+ * Arguments will be passed to your method in order of their appearance on the command line.
54
+ * If you specify a default value for a parameter in your method, then Rubycom will look for a named argument matching
55
+ the parameter's name.
56
+ * Users may call out option parameters in any order using `--<param_name>=<value>` or `--<param_name> <value>`
57
+ * Currently Rubycom does not yet support sort names for optional parameters so specifying `-<param_name>`
58
+ is equivalent to `--<param_name>`
59
+ * In the absence of a named option, any optional parameters still unfilled will be filled by unnamed arguments in
60
+ order of appearance.
61
+ * Optional parameters which do not get overridden either by a named option specification or an available unnamed
62
+ argument will be filled by their default as usual.
63
+ * If a rest parameter `*param_name` is defined in the method being called, any remaining arguments will be passed to the
64
+ rest parameter after the required and optional parameters are filled.
65
+
66
+ ###Jobs
67
+
68
+ Jobs are a higher order orchestration mechanism for command line utilities. Rubycom provides a simple job runner to every
69
+ command line utility. by calling `ruby ./path/to/module.rb job </path/to/job.yaml>` with a valid job yaml. Rubycom will
70
+ run your job.
71
+
72
+ * A valid job file is a Yaml file which specifies a `steps` node and any number of valid numbered child nodes
73
+ * Optionally, an `env` node may specified.
74
+ * If specified, `env` should include child nodes which are `key: value` pairs Ex: `working_dir: ./test/rubycom`
75
+ * If an `env` is specified, values may be inserted into commands in the `steps` node as such: `env['key']`
76
+ * Ex: `ruby env[working_dir]/util_test_composite.rb test_composite_command env[test_msg]`
77
+ * A valid `steps` child node is a numbered node `1:` with a `cmd:` child node and optionally several context `desc:`
78
+ child nodes.
79
+ * A `cmd:` node should specify the command string to run. Ex: `cmd: ls ./test_folder`
80
+ * A context node should specify some text to be placed with the node's key in a formatted
81
+ logging context Ex: `desc: Run test_composite_command`
82
+
83
+ Below is an example job file which demonstrates the format Rubycom supports.
84
+
85
+ ---
86
+ env:
87
+ test_msg: Hello World
88
+ test_arg: 123
89
+ working_dir: ./test/rubycom
90
+ steps:
91
+ 1:
92
+ desc: Run test_composite_command with environment variable
93
+ cmd: ruby env[working_dir]/util_test_composite.rb test_composite_command env[test_msg]
94
+ 2:
95
+ Var: Run UtilTestModule/test_command_options_arr with environment variable
96
+ cmd: ruby env[working_dir]/util_test_composite.rb UtilTestModule test_command_options_arr '["Hello World", world2]'
97
+ 3:
98
+ Context: Run test_command_with_args with environment variable
99
+ cmd: ruby env[working_dir]/util_test_module.rb test_command_with_args env[test_msg] env[test_arg]
100
+ 4:
101
+ Cmd: Run ls for arbitrary command support
102
+ cmd: ls
103
+ 5:
104
+ Arbitrary_Context: Run ls with environment variable
105
+ cmd: ls env[working_dir]
106
+
24
107
 
25
108
  Raison d'etre
26
109
  ---------------
@@ -53,9 +136,7 @@ The result is a function library which can be consumed easily from other classes
53
136
 
54
137
  Coming Soon
55
138
  ---------------
56
- * Run Pre-configured sets of commands from a yaml file by calling <script.rb> job <job_yaml>
57
- * Job help/usage output will include descriptions from command for each step
58
- * Build a job yaml by running each command in sequence with a special option --job_add <path_to_yaml>
139
+ * Build a job yaml by running each command in sequence with a special option --job_add <path_to_yaml>[:step_number]
59
140
  * Edit job files from the command line using special options.
60
- * --job_update <path_to_yaml>
61
- * --job_rm <path_to_yaml>
141
+ * --job_update <path_to_yaml>[:step_number]
142
+ * --job_remove <path_to_yaml>[:step_number]
data/Rakefile CHANGED
@@ -1,8 +1,14 @@
1
- require "bundler/gem_tasks"
2
- require 'rake/testtask'
1
+ require "#{File.expand_path(File.dirname(__FILE__))}/lib/rubycom/version.rb"
3
2
  require 'yard'
4
3
 
5
- task :default => [:test, :yard, :package]
4
+ task :default => [:package]
5
+
6
+ task :clean do
7
+ FileUtils.rm_rf('./doc')
8
+ FileUtils.rm_rf('./.yardoc')
9
+ FileUtils.rm_rf('./pkg')
10
+ FileUtils.rm(Dir.glob('./rubycom-*.gem'))
11
+ end
6
12
 
7
13
  task :test do
8
14
  test_files = Dir.glob("**/test/*/test_*.rb")
@@ -14,7 +20,7 @@ end
14
20
 
15
21
  YARD::Rake::YardocTask.new
16
22
 
17
- task :package => [:test, :yard] do
23
+ task :package => [:clean, :test, :yard] do
18
24
  gem_specs = Dir.glob("**/*.gemspec")
19
25
  gem_specs.each { |gem_spec|
20
26
  system("gem build #{gem_spec}")
@@ -23,9 +29,9 @@ task :package => [:test, :yard] do
23
29
  end
24
30
 
25
31
  task :install => :package do
26
- system "gem install ./rubycom-#{Rubycom::VERSION}"
32
+ system("gem install rubycom-#{Rubycom::VERSION}")
27
33
  end
28
34
 
29
35
  task :release => :package do
30
- system "gem push rubycom-#{Rubycom::VERSION}"
36
+ system("gem push ./rubycom-#{Rubycom::VERSION}.gem")
31
37
  end
@@ -1,3 +1,3 @@
1
1
  module Rubycom
2
- VERSION = "0.1.1"
2
+ VERSION = "0.2.0"
3
3
  end
data/lib/rubycom.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  require "#{File.expand_path(File.dirname(__FILE__))}/rubycom/version.rb"
2
2
  require 'yaml'
3
+ require 'find'
3
4
  require 'method_source'
4
5
 
5
6
  # Upon inclusion in another Module, Rubycom will attempt to call a method in the including module by parsing
@@ -8,7 +9,8 @@ require 'method_source'
8
9
  # If a Method match can not be made, Rubycom will print help instead by parsing source comments from the including
9
10
  # module or it's included modules.
10
11
  module Rubycom
11
- class CLIError < StandardError;end
12
+ class CLIError < StandardError;
13
+ end
12
14
 
13
15
  # Detects that Rubycom was included in another module and calls Rubycom#run
14
16
  #
@@ -44,6 +46,36 @@ module Rubycom
44
46
  puts cmd_usage
45
47
  return cmd_usage
46
48
  end
49
+ elsif command == 'job'
50
+ begin
51
+ raise CLIError, 'No job specified' if arguments[0].nil? || arguments[0].empty?
52
+ job_hash = YAML.load_file(arguments[0])
53
+ STDOUT.sync = true
54
+ if arguments.delete('-test') || arguments.delete('--test')
55
+ puts "[Test Job #{arguments[0]}]"
56
+ job_hash['steps'].each { |step, step_hash|
57
+ step = "[Step: #{step}/#{job_hash['steps'].length}]"
58
+ context = step_hash.select{|key| key!="cmd"}.map{|key,val| "[#{key}: #{val}]"}.join(' ')
59
+ env = job_hash['env'] || {}
60
+ env.map { |key, val| step_hash['cmd'].gsub!("env[#{key}]", "#{((val.class == String)&&(val.match(/\w+/))) ? "\"#{val}\"" : val}") }
61
+ cmd = "[cmd: #{step_hash['cmd']}]"
62
+ puts "#{[step,context,cmd].join(' ')}"
63
+ }
64
+ else
65
+ puts "[Job #{arguments[0]}]"
66
+ job_hash['steps'].each { |step, step_hash|
67
+ step = "[Step: #{step}/#{job_hash['steps'].length}]"
68
+ context = step_hash.select{|key| key!="cmd"}.map{|key,val| "[#{key}: #{val}]"}.join(' ')
69
+ env = job_hash['env'] || {}
70
+ env.map { |key, val| step_hash['cmd'].gsub!("env[#{key}]", "#{((val.class == String)&&(val.match(/\w+/))) ? "\"#{val}\"" : val}") }
71
+ cmd = "[cmd: #{step_hash['cmd']}]"
72
+ puts "#{[step,context,cmd].join(' ')}"
73
+ system(step_hash['cmd'])
74
+ }
75
+ end
76
+ rescue CLIError => e
77
+ $stderr.puts e
78
+ end
47
79
  else
48
80
  output = self.run_command(base, command, arguments)
49
81
  std_output = nil
@@ -53,7 +85,8 @@ module Rubycom
53
85
  end
54
86
 
55
87
  rescue CLIError => e
56
- abort "#{e}\n#{self.get_summary(base)}"
88
+ $stderr.puts e
89
+ $stderr.puts self.get_summary(base)
57
90
  end
58
91
  end
59
92
 
@@ -64,36 +97,20 @@ module Rubycom
64
97
  # @param [Array] arguments a String Array representing the arguments for the given command
65
98
  def self.run_command(base, command, arguments=[])
66
99
  raise CLIError, 'No command specified.' if command.nil? || command.length == 0
67
- command_sym = command.to_sym
68
- valid_commands = self.get_top_level_commands(base)
69
- raise CLIError, "Invalid Command: #{command}" unless valid_commands.include? command_sym
70
- if base.included_modules.map { |mod| mod.name.to_sym }.include?(command.to_sym)
71
- self.run_command(eval(command), arguments[0], arguments[1..-1])
72
- else
73
- method = base.public_method(command_sym)
74
- raise CLIError, "No public method found for symbol: #{command_sym}" if method.nil?
75
- parameters = self.get_param_definitions(method)
76
- params_hash = self.parse_arguments(parameters, arguments)
77
- params = []
78
- method.parameters.each { |type, name|
79
- if type == :rest
80
- if params_hash[name].class == Array
81
- params_hash[name].each { |arg|
82
- params << arg
83
- }
84
- else
85
- params << params_hash[name]
86
- end
87
- else
88
- params << params_hash[name]
89
- end
90
- }
91
- if arguments.nil? || arguments.empty?
92
- output = method.call
100
+ begin
101
+ raise CLIError, "Invalid Command: #{command}" unless self.get_top_level_commands(base).include? command.to_sym
102
+ if base.included_modules.map { |mod| mod.name.to_sym }.include?(command.to_sym)
103
+ self.run_command(eval(command), arguments[0], arguments[1..-1])
93
104
  else
94
- output = method.call(*params)
105
+ method = base.public_method(command.to_sym)
106
+ raise CLIError, "No public method found for symbol: #{command.to_sym}" if method.nil?
107
+ param_defs = self.get_param_definitions(method)
108
+ args = self.parse_arguments(param_defs, arguments)
109
+ (arguments.nil? || arguments.empty?) ? method.call : method.call(*method.parameters.map { |arr| args[arr[1]]}.flatten)
95
110
  end
96
- output
111
+ rescue CLIError => e
112
+ $stderr.puts e
113
+ $stderr.puts self.get_command_usage(base, command, arguments)
97
114
  end
98
115
  end
99
116
 
@@ -107,58 +124,32 @@ module Rubycom
107
124
  # @param [Array] arguments an Array of Strings representing the arguments to be parsed
108
125
  # @return [Hash] a Hash mapping the defined parameters to their matching argument values
109
126
  def self.parse_arguments(parameters={}, arguments=[])
110
- arguments = (!arguments.nil? && arguments.respond_to?(:each)) ? arguments : []
111
- args_l = arguments.length
112
- req_l = 0
113
- opt_l = 0
114
- has_rest_param = false
115
- parameters.each_value { |def_hash|
116
- req_l += 1 if def_hash[:type] == :req
117
- opt_l += 1 if def_hash[:type] == :opt
118
- has_rest_param = true if def_hash[:type] == :rest
119
- def_hash[:default] = self.parse_arg(def_hash[:default])[:arg] unless def_hash[:default] == :nil_rubycom_required_param
120
- }
121
- raise CLIError, "Wrong number of arguments. Expected at least #{req_l}, received #{args_l}" if args_l < req_l
122
- unless has_rest_param
123
- raise CLIError, "Wrong number of arguments. Expected at most #{req_l + opt_l}, received #{args_l}" if args_l > (req_l + opt_l)
124
- end
125
-
126
- args = []
127
- arguments.each { |arg|
128
- args << self.parse_arg(arg)
129
- }
127
+ raise CLIError, 'parameters may not be nil' if parameters.nil?
128
+ raise CLIError, 'arguments may not be nil' if arguments.nil?
129
+ types = parameters.values.group_by { |hsh| hsh[:type] }.map { |type, defs_arr| Hash[type, defs_arr.length] }.reduce(&:merge) || {}
130
+ raise CLIError, "Wrong number of arguments. Expected at least #{types[:req]}, received #{arguments.length}" if arguments.length < (types[:req]||0)
131
+ raise CLIError, "Wrong number of arguments. Expected at most #{(types[:req]||0) + (types[:opt]||0)}, received #{arguments.length}" if types[:rest].nil? && (arguments.length > ((types[:req]||0) + (types[:opt]||0)))
130
132
 
131
- parsed_args = []
132
- parsed_options = {}
133
- args.each { |item|
134
- key = item.keys.first
135
- val = item.values.first
136
- if key == :arg
137
- parsed_args << val
138
- else
139
- parsed_options[key]=val
140
- end
141
- }
133
+ sorted_args = arguments.map { |arg|
134
+ Rubycom.parse_arg(arg)
135
+ }.group_by { |hsh|
136
+ hsh.keys.first
137
+ }.map { |key, arr|
138
+ (key == :rubycom_non_opt_arg) ? Hash[key, arr.map { |hsh| hsh.values }.flatten] : Hash[key, arr.map { |hsh| hsh.values.first }.reduce(&:merge)]
139
+ }.reduce(&:merge) || {}
142
140
 
143
- result_hash = {}
144
- parameters.each { |param_name, def_hash|
141
+ parameters.map { |param_sym, def_hash|
145
142
  if def_hash[:type] == :req
146
- raise CLIError, "No argument available for #{param_name}" if parsed_args.length == 0
147
- result_hash[param_name] = parsed_args.shift
143
+ raise CLIError, "No argument available for #{param_sym}" if sorted_args[:rubycom_non_opt_arg].nil? || sorted_args[:rubycom_non_opt_arg].length == 0
144
+ Hash[param_sym, sorted_args[:rubycom_non_opt_arg].shift]
148
145
  elsif def_hash[:type] == :opt
149
- result_hash[param_name] = parsed_options[param_name]
150
- result_hash[param_name] = parsed_args.shift if result_hash[param_name].nil?
151
- result_hash[param_name] = parameters[param_name][:default] if result_hash[param_name].nil?
146
+ Hash[param_sym, ((sorted_args[param_sym]) ? sorted_args[param_sym] : ((sorted_args[:rubycom_non_opt_arg].shift || parameters[param_sym][:default]) rescue parameters[param_sym][:default]))]
152
147
  elsif def_hash[:type] == :rest
153
- if parsed_options[param_name].nil?
154
- result_hash[param_name] = parsed_args
155
- parsed_args = []
156
- else
157
- result_hash[param_name] = parsed_options[param_name]
158
- end
148
+ ret = Hash[param_sym, ((sorted_args[param_sym]) ? sorted_args[param_sym] : sorted_args[:rubycom_non_opt_arg])]
149
+ sorted_args[:rubycom_non_opt_arg] = []
150
+ ret
159
151
  end
160
- }
161
- result_hash
152
+ }.reduce(&:merge)
162
153
  end
163
154
 
164
155
  # Uses YAML.load to parse the given String
@@ -166,41 +157,19 @@ module Rubycom
166
157
  # @param [String] arg a String representing the argument to be parsed
167
158
  # @return [Object] the result of parsing the given arg with YAML.load
168
159
  def self.parse_arg(arg)
169
- param_name = 'arg'
170
- arg_val = "#{arg}"
171
- result = {}
172
- return result[param_name.to_sym]=nil if arg.nil?
173
- if arg.is_a? String
174
- raise CLIError, "Improper option specification, options must start with one or two dashes. Received: #{arg}" if (arg.match(/^[-]{3,}\w+/) != nil)
175
- if arg.match(/^[-]{1,}\w+/) == nil
176
- raise CLIError, "Improper option specification, options must start with one or two dashes. Received: #{arg}" if (arg.match(/^\w+=/) != nil)
177
-
178
- else
179
- if arg.match(/^--/) != nil
180
- arg = arg.reverse.chomp('--').reverse
181
- elsif arg.match(/^-/) != nil
182
- arg = arg.reverse.chomp('-').reverse
183
- end
184
-
185
- if arg.match(/^\w+=/) != nil
186
- arg_arr = arg.split('=')
187
- param_name = arg_arr.shift.strip
188
- arg_val = arg_arr.join('=').lstrip
189
- elsif arg.match(/^\w+\s+\S+/) != nil
190
- arg_arr = arg.split(' ')
191
- param_name = arg_arr.shift
192
- arg_val = arg_arr.join(' ')
193
- end
194
- end
195
- end
196
-
197
- val = YAML.load(arg_val) rescue nil
198
- if val.nil?
199
- result[param_name.to_sym] = "#{arg_val}"
160
+ return Hash[:rubycom_non_opt_arg, nil] if arg.nil?
161
+ if arg.is_a?(String) && ((arg.match(/^[-]{3,}\w+/) != nil) || ((arg.match(/^[-]{1,}\w+/) == nil) && (arg.match(/^\w+=/) != nil)))
162
+ raise CLIError, "Improper option specification, options must start with one or two dashes. Received: #{arg}"
163
+ elsif arg.is_a?(String) && arg.match(/^(-|--)\w+[=|\s]{1}/) != nil
164
+ k, v = arg.partition(/^(-|--)\w+[=|\s]{1}/).select { |part|
165
+ !part.empty?
166
+ }.each_with_index.map { |part, index|
167
+ index == 0 ? part.chomp('=').gsub(/^--/, '').gsub(/^-/, '').strip.to_sym : (YAML.load(part) rescue "#{part}")
168
+ }
169
+ Hash[k, v]
200
170
  else
201
- result[param_name.to_sym] = val
171
+ Hash[:rubycom_non_opt_arg, (YAML.load("#{arg}") rescue "#{arg}")]
202
172
  end
203
- result
204
173
  end
205
174
 
206
175
  # Retrieves the summary for each command method in the given Module
@@ -208,25 +177,13 @@ module Rubycom
208
177
  # @param [Module] base the module which invoked 'include Rubycom'
209
178
  # @return [String] the summary for each command method in the given Module
210
179
  def self.get_summary(base)
211
- longest_name_length = self.get_longest_command_name(base).length
212
180
  self.get_top_level_commands(base).each_with_index.map { |sym, index|
213
- separator = self.get_separator(sym, longest_name_length)
214
- if index == 0
215
- "Commands:\n" << self.get_command_summary(base, sym, separator)
216
- else
217
- self.get_command_summary(base, sym, separator)
218
- end
181
+ "#{"Commands:\n" if index == 0}" << self.get_command_summary(base, sym, self.get_separator(sym, self.get_longest_command_name(base).length))
219
182
  }.reduce(:+) or "No Commands found for #{base}."
220
183
  end
221
184
 
222
185
  def self.get_separator(sym, spacer_length=0)
223
- cmd_name = sym.to_s
224
- sep_length = spacer_length - cmd_name.length
225
- separator = ""
226
- sep_length.times {
227
- separator << " "
228
- }
229
- separator << " - "
186
+ [].unshift(" " * (spacer_length - sym.to_s.length)).join << " - "
230
187
  end
231
188
 
232
189
  # Retrieves the summary for the given command_name
@@ -238,27 +195,23 @@ module Rubycom
238
195
  raise CLIError, "Can not get usage for #{command_name} with base: #{base||"nil"}" if base.nil? || !base.respond_to?(:included_modules)
239
196
  return 'No command specified.' if command_name.nil? || command_name.length == 0
240
197
  if base.included_modules.map { |mod| mod.name.to_sym }.include?(command_name.to_sym)
241
- desc = "Sub-Module-Command"
198
+ mod_const = Kernel.const_get(command_name.to_sym)
199
+ desc = File.read(mod_const.public_method(mod_const.singleton_methods().first).source_location.first).split(//).reduce(""){|str,c|
200
+ unless str.gsub("\n",'').gsub(/\s+/,'').include?("module#{mod_const}")
201
+ str << c
202
+ end
203
+ str
204
+ }.split("\n").select{|line| line.strip.match(/^#/)}.map{|line| line.strip.gsub(/^#+/,'')}.join("\n")
242
205
  else
243
206
  raise CLIError, "Invalid command for #{base}, #{command_name}" unless base.public_methods.include?(command_name.to_sym)
244
- m = base.public_method(command_name.to_sym)
245
- method_doc = self.get_doc(m)
246
- desc = method_doc[:desc].join("\n")
207
+ desc = self.get_doc(base.public_method(command_name.to_sym))[:desc].join("\n") rescue nil
247
208
  end
248
209
  (desc.nil?||desc=='nil'||desc.length==0) ? "#{command_name}\n" : self.get_formatted_summary(command_name, desc, separator)
249
210
  end
250
211
 
251
212
  def self.get_formatted_summary(command_name, command_description, separator = ' - ')
252
213
  width = 95
253
- spacer = ""
254
- command_name.to_s.split(//).each {
255
- spacer << " "
256
- }
257
- sep_space = ""
258
- separator.split(//).each {
259
- sep_space << " "
260
- }
261
- prefix = "#{spacer}#{sep_space}"
214
+ prefix = command_name.to_s.split(//).map { " " }.join + separator.split(//).map { " " }.join
262
215
  line_width = width - prefix.length
263
216
  description_msg = command_description.gsub(/(.{1,#{line_width}})(?: +|$)\n?|(.{#{line_width}})/, "#{prefix}"+'\1\2'+"\n")
264
217
  "#{command_name}#{separator}#{description_msg.lstrip}"
@@ -294,17 +247,17 @@ module Rubycom
294
247
  m = base.public_method(command_name.to_sym)
295
248
  method_doc = self.get_doc(m)
296
249
 
297
- <<-END.gsub(/^ {6}/, '')
298
- Usage: #{m.name} #{self.get_param_usage(m)}
299
- #{"Parameters:" unless m.parameters.empty?}
300
- #{method_doc[:param].join("\n ") unless method_doc[:param].nil?}
301
- Returns:
302
- #{method_doc[:return].join("\n ") rescue 'void'}
303
- END
250
+ msg = "Usage: #{m.name} #{self.get_param_usage(m)}\n"
251
+ msg << "#{"Parameters:"}\n" unless m.parameters.empty?
252
+ msg << "#{method_doc[:param].join("\n ")}\n" unless method_doc[:param].nil?
253
+ msg << "#{"Returns:"}\n" unless method_doc[:return].nil?
254
+ msg << "#{method_doc[:return].join("\n ")}\n" unless method_doc[:return].nil?
255
+ msg
304
256
  end
305
257
  end
306
258
 
307
259
  def self.get_param_usage(method)
260
+ return "" if method.parameters.nil? || method.parameters.empty?
308
261
  method.parameters.map { |type, param| {type => param}
309
262
  }.group_by { |entry| entry.keys.first
310
263
  }.map { |key, val| Hash[key, val.map { |param| param.values.first }]
@@ -327,35 +280,29 @@ module Rubycom
327
280
  # @return [Hash] a Hash representing the given Method's parameters
328
281
  def self.get_param_definitions(method)
329
282
  raise CLIError, 'method must be an instance of the Method class' unless method.class == Method
330
- source = method.source
331
- method_name = method.name.to_s
332
- source_lines = source.split("\n")
333
- param_names = method.parameters.map { |param| param[1].to_s }
334
- param_types = {}
335
- method.parameters.each { |type, name| param_types[name] = type }
336
- param_def_lines = {}
337
- param_names.each { |name| param_def_lines[name] = source_lines.select { |line| line.include?(name) }.first }
338
- param_definitions = {}
339
- param_def_lines.each { |name, param_def_line|
340
- param_candidates = param_def_line.gsub(/(def\s+self\.#{method_name}|def\s+#{method_name})/, '').lstrip.chomp.chomp(')').reverse.chomp('(').reverse
341
- param_definitions[name.to_sym] = {}
342
- param_definitions[name.to_sym][:def] = param_candidates.split(',').select { |candidate| candidate.include?(name) }.first
343
- param_definitions[name.to_sym][:type] = param_types[name.to_sym]
344
- if param_definitions[name.to_sym][:def].include?('=')
345
- param_definitions[name.to_sym][:default] = param_definitions[name.to_sym][:def].split('=')[1..-1].join('=')
346
- else
347
- param_definitions[name.to_sym][:default] = :nil_rubycom_required_param
348
- end
349
- }
350
- param_definitions
283
+ method.parameters.map { |param|
284
+ param[1].to_s
285
+ }.map { |param_name|
286
+ {param_name.to_sym => method.source.split("\n").select { |line| line.include?(param_name) }.first}
287
+ }.map { |param_hash|
288
+ param_def = param_hash.flatten[1].gsub(/(def\s+self\.#{method.name.to_s}|def\s+#{method.name.to_s})/, '').lstrip.chomp.chomp(')').reverse.chomp('(').reverse.split(',').map { |param_n| param_n.lstrip }.select { |candidate| candidate.include?(param_hash.flatten[0].to_s) }.first
289
+ Hash[
290
+ param_hash.flatten[0],
291
+ Hash[
292
+ :def, param_def,
293
+ :type, method.parameters.select { |arr| arr[1] == param_hash.flatten[0] }.flatten[0],
294
+ :default, (param_def.include?('=') ? YAML.load(param_def.split('=')[1..-1].join('=')) : :nil_rubycom_required_param)
295
+ ]
296
+ ]
297
+ }.reduce(&:merge) || {}
351
298
  end
352
299
 
353
300
  # Retrieves the given method's documentation from it's source code.
354
301
  #
355
302
  # @param [Method] method the Method who's documentation should be retrieved
356
303
  # @return [Hash] a Hash representing the given Method's documentation, documentation parsed as follows:
357
- # :desc = the first general method comment, :params = each @param comment, :return = each @return comment,
358
- # :extended = all other general method comments and unrecognized annotations
304
+ # :desc = the first general method comment lines,
305
+ # :word = each @word comment (i.e.- a line starting with @param will be saved as :param => ["line"])
359
306
  def self.get_doc(method)
360
307
  method.comment.split("\n").map { |line|
361
308
  line.gsub(/#\s*/, '') }.group_by { |doc|
@@ -382,17 +329,13 @@ module Rubycom
382
329
  # @return [Hash] a Hash of Symbols representing the command methods in the given base and it's included modules (if all=true)
383
330
  def self.get_commands(base, all=true)
384
331
  return {} if base.nil? || !base.respond_to?(:singleton_methods) || !base.respond_to?(:included_modules)
385
- excluded_commands = [:included, :extended]
386
- excluded_modules = [:Rubycom]
387
332
  {
388
333
  base.name.to_sym => {
389
- commands: base.singleton_methods(true).select { |sym| !excluded_commands.include?(sym) },
390
- inclusions: base.included_modules.select { |mod| !excluded_modules.include?(mod.name.to_sym) }.map { |mod|
391
- if all
392
- self.get_commands(mod)
393
- else
394
- mod.name.to_sym
395
- end
334
+ commands: base.singleton_methods(true).select { |sym| ![:included, :extended].include?(sym) },
335
+ inclusions: base.included_modules.select { |mod|
336
+ ![:Rubycom].include?(mod.name.to_sym)
337
+ }.map { |mod|
338
+ all ? self.get_commands(mod) : mod.name.to_sym
396
339
  }
397
340
  }
398
341
  }
data/rubycom.gemspec CHANGED
@@ -7,7 +7,7 @@ Gem::Specification.new do |spec|
7
7
  spec.name = "rubycom"
8
8
  spec.version = Rubycom::VERSION
9
9
  spec.authors = ["Danny Purcell"]
10
- spec.email = ["dpurcelljr@gmail.com"]
10
+ spec.email = ["d.purcell.jr+rubycom@gmail.com"]
11
11
  spec.description = %q{Allows command-line access for all singleton methods in an including class. Reads Yard style documentation for command line help output. Uses Yaml for parsing options. Allows the user to make a command-line tool by simply including Rubycom at the bottom.}
12
12
  spec.summary = %q{Converts singleton methods to command-line functions upon inclusion.}
13
13
  spec.homepage = "http://dannypurcell.github.io/Rubycom"
@@ -113,19 +113,20 @@ class TestRubycom < Test::Unit::TestCase
113
113
  UtilTestModule <command> [args]
114
114
 
115
115
  Commands:
116
- test_command - A basic test command
117
- test_command_with_arg - A test_command with one arg
118
- test_command_with_args - A test_command with two args
119
- test_command_with_options - A test_command with an optional argument
120
- test_command_all_options - A test_command with all optional arguments
121
- test_command_options_arr - A test_command with an options array
122
- test_command_with_return - A test_command with a return argument
123
- test_command_arg_timestamp - A test_command with a Timestamp argument and an unnecessarily
124
- long description which should overflow when
125
- it tries to line up with other descriptions.
126
- test_command_arg_false - A test_command with a Boolean argument
127
- test_command_arg_arr - A test_command with an array argument
128
- test_command_arg_hash - A test_command with an Hash argument
116
+ test_command - A basic test command
117
+ test_command_with_arg - A test_command with one arg
118
+ test_command_arg_named_arg - A test_command with an arg named arg
119
+ test_command_with_args - A test_command with two args
120
+ test_command_with_options - A test_command with an optional argument
121
+ test_command_all_options - A test_command with all optional arguments
122
+ test_command_options_arr - A test_command with an options array
123
+ test_command_with_return - A test_command with a return argument
124
+ test_command_arg_timestamp - A test_command with a Timestamp argument and an unnecessarily
125
+ long description which should overflow when
126
+ it tries to line up with other descriptions.
127
+ test_command_arg_false - A test_command with a Boolean argument
128
+ test_command_arg_arr - A test_command with an array argument
129
+ test_command_arg_hash - A test_command with an Hash argument
129
130
 
130
131
  END
131
132
  assert_equal(expected.gsub(/\n|\r|\s/, ''), result.gsub(/\n|\r|\s/, ''))
@@ -150,14 +151,15 @@ class TestRubycom < Test::Unit::TestCase
150
151
  Commands:
151
152
  test_command - A basic test command
152
153
  test_command_with_arg - A test_command with one arg
154
+ test_command_arg_named_arg - A test_command with an arg named arg
153
155
  test_command_with_args - A test_command with two args
154
156
  test_command_with_options - A test_command with an optional argument
155
157
  test_command_all_options - A test_command with all optional arguments
156
158
  test_command_options_arr - A test_command with an options array
157
159
  test_command_with_return - A test_command with a return argument
158
160
  test_command_arg_timestamp - A test_command with a Timestamp argument and an unnecessarily
159
- long description which should overflow when it tries to line up
160
- with other descriptions.
161
+ long description which should overflow when
162
+ it tries to line up with other descriptions.
161
163
  test_command_arg_false - A test_command with a Boolean argument
162
164
  test_command_arg_arr - A test_command with an array argument
163
165
  test_command_arg_hash - A test_command with an Hash argument
@@ -178,7 +180,7 @@ class TestRubycom < Test::Unit::TestCase
178
180
  end
179
181
 
180
182
  def test_get_top_level_commands
181
- test_command_list = [:test_command, :test_command_with_arg, :test_command_with_args, :test_command_with_options,
183
+ test_command_list = [:test_command, :test_command_with_arg, :test_command_arg_named_arg, :test_command_with_args, :test_command_with_options,
182
184
  :test_command_all_options, :test_command_options_arr, :test_command_with_return,
183
185
  :test_command_arg_timestamp, :test_command_arg_false, :test_command_arg_arr,
184
186
  :test_command_arg_hash]
@@ -219,21 +221,21 @@ class TestRubycom < Test::Unit::TestCase
219
221
  def test_parse_arg_string
220
222
  test_arg = "test_arg"
221
223
  result = Rubycom.parse_arg(test_arg)
222
- expected = {arg: "test_arg"}
224
+ expected = {rubycom_non_opt_arg: "test_arg"}
223
225
  assert_equal(expected, result)
224
226
  end
225
227
 
226
228
  def test_parse_arg_fixnum
227
229
  test_arg = "1234512415435"
228
230
  result = Rubycom.parse_arg(test_arg)
229
- expected = {arg: 1234512415435}
231
+ expected = {rubycom_non_opt_arg: 1234512415435}
230
232
  assert_equal(expected, result)
231
233
  end
232
234
 
233
235
  def test_parse_arg_float
234
236
  test_arg = "12345.67890"
235
237
  result = Rubycom.parse_arg(test_arg)
236
- expected = {arg: 12345.67890}
238
+ expected = {rubycom_non_opt_arg: 12345.67890}
237
239
  assert_equal(expected, result)
238
240
  end
239
241
 
@@ -241,8 +243,8 @@ class TestRubycom < Test::Unit::TestCase
241
243
  time = Time.new
242
244
  test_arg = time.to_s
243
245
  result = Rubycom.parse_arg(test_arg)
244
- expected = {arg: time}
245
- assert_equal(expected[:arg].to_i, result[:arg].to_i)
246
+ expected = {rubycom_non_opt_arg: time}
247
+ assert_equal(expected[:rubycom_non_opt_arg].to_i, result[:rubycom_non_opt_arg].to_i)
246
248
  end
247
249
 
248
250
  def test_parse_arg_datetime
@@ -250,14 +252,14 @@ class TestRubycom < Test::Unit::TestCase
250
252
  date = Date.new(time.year, time.month, time.day)
251
253
  test_arg = date.to_s
252
254
  result = Rubycom.parse_arg(test_arg)
253
- expected = {arg: date}
255
+ expected = {rubycom_non_opt_arg: date}
254
256
  assert_equal(expected, result)
255
257
  end
256
258
 
257
259
  def test_parse_arg_array
258
260
  test_arg = ["1", 2, "a", 'b']
259
261
  result = Rubycom.parse_arg(test_arg.to_s)
260
- expected = {arg: test_arg}
262
+ expected = {rubycom_non_opt_arg: test_arg}
261
263
  assert_equal(expected, result)
262
264
  end
263
265
 
@@ -265,28 +267,91 @@ class TestRubycom < Test::Unit::TestCase
265
267
  time = Time.new.to_s
266
268
  test_arg = ":a: \"#{time}\""
267
269
  result = Rubycom.parse_arg(test_arg.to_s)
268
- expected = {arg: {a: time}}
270
+ expected = {rubycom_non_opt_arg: {a: time}}
269
271
  assert_equal(expected, result)
270
272
  end
271
273
 
272
274
  def test_parse_arg_hash_group
273
275
  test_arg = ":a: [1,2]\n:b: 1\n:c: test\n:d: 1.0\n:e: \"2013-05-08 23:21:52 -0500\"\n"
274
276
  result = Rubycom.parse_arg(test_arg.to_s)
275
- expected = {arg: {:a => [1, 2], :b => 1, :c => "test", :d => 1.0, :e => "2013-05-08 23:21:52 -0500"}}
277
+ expected = {rubycom_non_opt_arg: {:a => [1, 2], :b => 1, :c => "test", :d => 1.0, :e => "2013-05-08 23:21:52 -0500"}}
276
278
  assert_equal(expected, result)
277
279
  end
278
280
 
279
281
  def test_parse_arg_yaml
280
282
  test_arg = {:a => ["1", 2, "a", 'b'], :b => 1, :c => "test", :d => "#{Time.now.to_s}"}
281
283
  result = Rubycom.parse_arg(test_arg.to_yaml)
282
- expected = {arg: test_arg}
284
+ expected = {rubycom_non_opt_arg: test_arg}
283
285
  assert_equal(expected, result)
284
286
  end
285
287
 
286
288
  def test_parse_arg_code
287
289
  test_arg = 'def self.test_code_method; raise "Fail - test_parse_arg_code";end'
288
290
  result = Rubycom.parse_arg(test_arg.to_s)
289
- expected = {arg: 'def self.test_code_method; raise "Fail - test_parse_arg_code";end'}
291
+ expected = {rubycom_non_opt_arg: 'def self.test_code_method; raise "Fail - test_parse_arg_code";end'}
292
+ assert_equal(expected, result)
293
+ end
294
+
295
+ def test_parse_opt_string_eq
296
+ test_arg = "-test_arg=\"test\""
297
+ result = Rubycom.parse_arg(test_arg)
298
+ expected = {test_arg: "test"}
299
+ assert_equal(expected, result)
300
+ end
301
+
302
+ def test_parse_opt_string_sp
303
+ test_arg = "-test_arg \"test\""
304
+ result = Rubycom.parse_arg(test_arg)
305
+ expected = {test_arg: "test"}
306
+ assert_equal(expected, result)
307
+ end
308
+
309
+ def test_parse_opt_long_string_eq
310
+ test_arg = "--test_arg=\"test\""
311
+ result = Rubycom.parse_arg(test_arg)
312
+ expected = {test_arg: "test"}
313
+ assert_equal(expected, result)
314
+ end
315
+
316
+ def test_parse_opt_long_string_sp
317
+ test_arg = "--test_arg \"test\""
318
+ result = Rubycom.parse_arg(test_arg)
319
+ expected = {test_arg: "test"}
320
+ assert_equal(expected, result)
321
+ end
322
+
323
+ def test_get_param_definitions
324
+ test_method = UtilTestModule.public_method('test_command_with_args')
325
+ expected = {:test_arg=>{:def=>"test_arg", :type=>:req, :default=>:nil_rubycom_required_param}, :another_test_arg=>{:def=>"another_test_arg", :type=>:req, :default=>:nil_rubycom_required_param}}
326
+ result = Rubycom.get_param_definitions(test_method)
327
+ assert_equal(expected, result)
328
+ end
329
+
330
+ def test_get_param_definitions_no_args
331
+ test_method = UtilTestModule.public_method('test_command')
332
+ expected = {}
333
+ result = Rubycom.get_param_definitions(test_method)
334
+ assert_equal(expected, result)
335
+ end
336
+
337
+ def test_get_param_definitions_arr_param
338
+ test_method = UtilTestModule.public_method('test_command_options_arr')
339
+ expected = {:test_option=>{:def=>"test_option=\"test_option_default\"", :type=>:opt, :default=>"test_option_default"}, :test_options=>{:def=>"*test_options", :type=>:rest, :default=>:nil_rubycom_required_param}}
340
+ result = Rubycom.get_param_definitions(test_method)
341
+ assert_equal(expected, result)
342
+ end
343
+
344
+ def test_get_param_definitions_all_options
345
+ test_method = UtilTestModule.public_method('test_command_all_options')
346
+ expected = {:test_arg=>{:def=>"test_arg='test_arg_default'", :type=>:opt, :default=>"test_arg_default"}, :test_option=>{:def=>"test_option='test_option_default'", :type=>:opt, :default=>"test_option_default"}}
347
+ result = Rubycom.get_param_definitions(test_method)
348
+ assert_equal(expected, result)
349
+ end
350
+
351
+ def test_get_param_definitions_mixed
352
+ test_method = UtilTestModule.public_method('test_command_with_options')
353
+ expected = {:test_arg=>{:def=>"test_arg", :type=>:req, :default=>:nil_rubycom_required_param}, :test_option=>{:def=>"test_option='option_default'", :type=>:opt, :default=>"option_default"}}
354
+ result = Rubycom.get_param_definitions(test_method)
290
355
  assert_equal(expected, result)
291
356
  end
292
357
 
@@ -334,6 +399,7 @@ class TestRubycom < Test::Unit::TestCase
334
399
  Commands:
335
400
  test_command - A basic test command
336
401
  test_command_with_arg - A test_command with one arg
402
+ test_command_arg_named_arg - A test_command with an arg named arg
337
403
  test_command_with_args - A test_command with two args
338
404
  test_command_with_options - A test_command with an optional argument
339
405
  test_command_all_options - A test_command with all optional arguments
@@ -347,7 +413,7 @@ class TestRubycom < Test::Unit::TestCase
347
413
  test_command_arg_hash - A test_command with an Hash argument
348
414
  END
349
415
  expected_out = expected
350
- assert_equal(expected, result)
416
+ assert_equal(expected.gsub(/\n|\r|\s/, ''), result.gsub(/\n|\r|\s/, ''))
351
417
  assert_equal(expected_out.gsub(/\n|\r|\s/, ''), tst_out.gsub(/\n|\r|\s/, ''))
352
418
  ensure
353
419
  $stdout = o_stdout
@@ -596,9 +662,13 @@ class TestRubycom < Test::Unit::TestCase
596
662
  result = Rubycom.run(base, args)
597
663
 
598
664
  expected = nil
599
- expected_out = "No argument available for test_arg"
665
+ expected_out = "No argument available for test_arg\n"
666
+
600
667
  assert_equal(expected, result)
601
- assert_equal(expected_out, tst_out.split(/\n|\r|\r\n/).first)
668
+ assert_equal(expected_out, tst_out.lines.first)
669
+ Rubycom.get_command_usage(base,args[0],args[1..-1]).each_line{|expected_line|
670
+ assert_equal(true, tst_out.lines.include?(expected_line))
671
+ }
602
672
  ensure
603
673
  $stdout = o_stdout
604
674
  $stderr = o_stderr
@@ -0,0 +1,21 @@
1
+ ---
2
+ env:
3
+ test_msg: Hello World
4
+ test_arg: 123
5
+ working_dir: ./test/rubycom
6
+ steps:
7
+ 1:
8
+ desc: Run test_composite_command with environment variable
9
+ cmd: ruby env[working_dir]/util_test_composite.rb test_composite_command env[test_msg]
10
+ 2:
11
+ Var: Run UtilTestModule/test_command_options_arr with environment variable
12
+ cmd: ruby env[working_dir]/util_test_composite.rb UtilTestModule test_command_options_arr '["Hello World 1", world2]'
13
+ 3:
14
+ Cntxt: Run test_command_with_args with environment variable
15
+ cmd: ruby env[working_dir]/util_test_module.rb test_command_with_args env[test_msg] env[test_arg]
16
+ 4:
17
+ Cmd: Run ls for arbitrary command support
18
+ cmd: ls
19
+ 5:
20
+ Arbitrary_Context: Run ls with environment variable
21
+ cmd: ls env[working_dir]
@@ -1,5 +1,5 @@
1
1
  require "#{File.expand_path(File.dirname(__FILE__))}/../../lib/rubycom.rb"
2
-
2
+ # A command module used for testing
3
3
  module UtilTestModule
4
4
 
5
5
  # A test non-command method
@@ -19,6 +19,13 @@ module UtilTestModule
19
19
  "test_arg=#{test_arg}"
20
20
  end
21
21
 
22
+ # A test_command with an arg named arg
23
+ #
24
+ # @param [String] arg a test argument whose parameter name is arg
25
+ def self.test_command_arg_named_arg(arg)
26
+ "arg=#{arg}"
27
+ end
28
+
22
29
  # A test_command with two args
23
30
  # @param [String] test_arg a test argument
24
31
  # @param [String] another_test_arg another test argument
@@ -1,10 +1,10 @@
1
- require "#{File.expand_path(File.dirname(__FILE__))}/../../lib/rubycom.rb"
2
- require "#{File.expand_path(File.dirname(__FILE__))}/util_test_composite.rb"
3
-
4
- module UtilTestNoSingleton
5
- def test_method
6
- "TEST_NON_SINGLETON_METHOD"
7
- end
8
-
9
- include Rubycom
1
+ require "#{File.expand_path(File.dirname(__FILE__))}/../../lib/rubycom.rb"
2
+ require "#{File.expand_path(File.dirname(__FILE__))}/util_test_composite.rb"
3
+
4
+ module UtilTestNoSingleton
5
+ def test_method
6
+ "TEST_NON_SINGLETON_METHOD"
7
+ end
8
+
9
+ include Rubycom
10
10
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rubycom
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-06-03 00:00:00.000000000 Z
12
+ date: 2013-06-14 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: bundler
@@ -96,7 +96,7 @@ description: Allows command-line access for all singleton methods in an includin
96
96
  parsing options. Allows the user to make a command-line tool by simply including
97
97
  Rubycom at the bottom.
98
98
  email:
99
- - dpurcelljr@gmail.com
99
+ - d.purcell.jr+rubycom@gmail.com
100
100
  executables: []
101
101
  extensions: []
102
102
  extra_rdoc_files: []
@@ -111,6 +111,7 @@ files:
111
111
  - rubycom.gemspec
112
112
  - test/rubycom/test_rubycom.rb
113
113
  - test/rubycom/util_test_composite.rb
114
+ - test/rubycom/util_test_job.yaml
114
115
  - test/rubycom/util_test_module.rb
115
116
  - test/rubycom/util_test_no_singleton.rb
116
117
  - test/rubycom/utility_tester.rb
@@ -142,6 +143,7 @@ summary: Converts singleton methods to command-line functions upon inclusion.
142
143
  test_files:
143
144
  - test/rubycom/test_rubycom.rb
144
145
  - test/rubycom/util_test_composite.rb
146
+ - test/rubycom/util_test_job.yaml
145
147
  - test/rubycom/util_test_module.rb
146
148
  - test/rubycom/util_test_no_singleton.rb
147
149
  - test/rubycom/utility_tester.rb