rubycom 0.1.1 → 0.2.0

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