commandable 0.2.0.beta01

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. data/.gitignore +8 -0
  2. data/Gemfile +2 -0
  3. data/LICENCE +19 -0
  4. data/README.markdown +409 -0
  5. data/Rakefile +29 -0
  6. data/_testing/alias_trap.rb +14 -0
  7. data/autotest/discover.rb +2 -0
  8. data/bin/commandable +18 -0
  9. data/commandable.gemspec +24 -0
  10. data/lib/commandable.rb +4 -0
  11. data/lib/commandable/app_controller.rb +47 -0
  12. data/lib/commandable/commandable.rb +394 -0
  13. data/lib/commandable/exceptions.rb +61 -0
  14. data/lib/commandable/version.rb +4 -0
  15. data/lib/monkey_patch/file_utils.rb +11 -0
  16. data/spec/commandable/command_line_execution_spec.rb +154 -0
  17. data/spec/commandable/commandable_spec.rb +245 -0
  18. data/spec/commandable/help_generator_spec.rb +169 -0
  19. data/spec/commandable/helpers_spec.rb +17 -0
  20. data/spec/commandable/reset_spec.rb +26 -0
  21. data/spec/commandable/xor_groups_spec.rb +43 -0
  22. data/spec/source_code_examples/class_command_no_command.rb +27 -0
  23. data/spec/source_code_examples/class_methods.rb +20 -0
  24. data/spec/source_code_examples/class_methods_nested.rb +31 -0
  25. data/spec/source_code_examples/command_no_command.rb +27 -0
  26. data/spec/source_code_examples/deep_class.rb +14 -0
  27. data/spec/source_code_examples/default_method.rb +17 -0
  28. data/spec/source_code_examples/default_method_no_params.rb +17 -0
  29. data/spec/source_code_examples/multi_line_description.rb +17 -0
  30. data/spec/source_code_examples/multi_line_description_no_params.rb +17 -0
  31. data/spec/source_code_examples/no_description.rb +10 -0
  32. data/spec/source_code_examples/parameter_class.rb +27 -0
  33. data/spec/source_code_examples/parameter_free.rb +22 -0
  34. data/spec/source_code_examples/required_methods.rb +18 -0
  35. data/spec/source_code_examples/super_deep_class.rb +28 -0
  36. data/spec/source_code_examples/test_class.rb +13 -0
  37. data/spec/source_code_examples/xor_class.rb +37 -0
  38. data/spec/source_code_for_errors/class_bad.rb +7 -0
  39. data/spec/source_code_for_errors/default_method_bad.rb +17 -0
  40. data/spec/source_code_for_errors/private_methods_bad.rb +10 -0
  41. data/spec/spec_helper.rb +55 -0
  42. metadata +140 -0
@@ -0,0 +1,14 @@
1
+ require 'pp'
2
+ # hash1 = {
3
+ # :c=>{:foo=>"a c command", :description=>"c description"},
4
+ # :a=>{:foo=>"z a command", :description=>"c description"},
5
+ # :b=>{:foo=>" buh command", :description=>"c description"}
6
+ # }
7
+
8
+ hash1 = [
9
+ {:foo=>"a c command", :description=>"c description"},
10
+ {:foo=>"z a command", :description=>"c description"},
11
+ {:foo=>" buh command", :description=>"c description"}
12
+ ]
13
+
14
+ puts hash1.shift[:foo]
@@ -0,0 +1,2 @@
1
+ #:nodoc:
2
+ Autotest.add_discovery {"rspec2"}
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env ruby
2
+ $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + "/../lib")
3
+ require 'commandable'
4
+ Commandable.color_output = true
5
+ Commandable.verbose_parameters = false
6
+ Commandable.app_name = "commandable"
7
+ Commandable.app_info =
8
+ """
9
+ \e[92mCommandable\e[0m - The easiest way to add command line control to your app.
10
+ Copyrighted free software - Copyright (c) 2011 Mike Bethany.
11
+ Version: #{Commandable::VERSION}
12
+ """
13
+
14
+ # App controller has to be loaded after commandable settings
15
+ # or it won't be able to use the settings
16
+ require 'commandable/app_controller'
17
+
18
+ Commandable.execute(ARGV)
@@ -0,0 +1,24 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "commandable/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "commandable"
7
+ s.required_ruby_version = "~>1.9.2"
8
+ s.version = Commandable::VERSION
9
+ s.platform = Gem::Platform::RUBY
10
+ s.authors = ["Mike Bethany"]
11
+ s.email = ["mikbe.tk@gmail.com"]
12
+ s.homepage = "http://mikbe.tk"
13
+ s.summary = %q{The easiest way to add command line control to your app.}
14
+ s.description = %q{Adding command line control to your app is as easy as putting 'command "this command does xyz"' above a method. Parameter lists and a help command are automatically built for you.}
15
+
16
+ s.add_dependency("term-ansicolor-hi", "~>1.0.6")
17
+
18
+ s.add_development_dependency("rspec", "~>2.5")
19
+
20
+ s.files = `git ls-files`.split("\n")
21
+ s.test_files = `git ls-files -- {spec,autotest}/*`.split("\n")
22
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
23
+ s.require_paths = ["lib"]
24
+ end
@@ -0,0 +1,4 @@
1
+ require 'monkey_patch/file_utils'
2
+ require 'commandable/version'
3
+ require 'commandable/exceptions'
4
+ require 'commandable/commandable'
@@ -0,0 +1,47 @@
1
+ module Commandable
2
+
3
+ # A helper to display the read me file and generate an example app
4
+ class AppController
5
+
6
+ class << self
7
+ extend Commandable
8
+
9
+ # Displays the readme file
10
+ command "displays the readme file", :default
11
+ def readme
12
+ `open #{File.expand_path((File.dirname(__FILE__) + '/../../readme.markdown'))}`
13
+ end
14
+
15
+ command "Copies a fully working app demonstrating how\n to use Commandable with RSpec and Cucumber"
16
+ # Creates a simple example app demonstrating a fully working app
17
+ def widget(path="./widget")
18
+ puts "This feature hasn't been added yet. I'm working on it now and it will be in the release version."
19
+ end
20
+
21
+ command "Copies the test classes to a folder so\n you can see a bunch of small examples"
22
+ # Copies the test classes to a folder so you can see a bunch of small examples
23
+ def examples(path="./example_classes")
24
+ FileUtils.copy_dir(File.expand_path(File.dirname(__FILE__) + '/../../spec/source_code_examples'),path)
25
+ end
26
+
27
+ command "Will raise a programmer error, not a user error\nso you see what happens when you have bad code"
28
+ # Causes an error so you can see what it will look like if you have an error in your code.
29
+ def error
30
+ raise Exception, "An example of a non-user error caused by your bad code trapped in Commandable.execute()"
31
+ end
32
+
33
+ command "Application Version", :xor
34
+ # Version
35
+ def v
36
+ puts "Commandable: #{Commandable::VERSION}"
37
+ end
38
+ command "Application Version", :xor
39
+ alias :version :v
40
+
41
+ end
42
+
43
+ end
44
+
45
+ end
46
+
47
+
@@ -0,0 +1,394 @@
1
+ require 'term/ansicolor'
2
+ require 'set'
3
+
4
+ # This library allows you to incredibly easily make
5
+ # your methods directly available from the command line.
6
+ #
7
+ # Author:: Mike Bethany (mailto:mikbe.tk@gmail.com)
8
+ # Copyright:: Copyright (c) 2011 Mike Bethany
9
+ # License:: Distributed under the MIT licence (See LICENCE file)
10
+
11
+ # Extending your class with this module allows you to use the #command
12
+ # method above your method. This makes them executable from the command line.
13
+ module Commandable
14
+
15
+ # Default command that always gets added to end of the command list
16
+ HELP_COMMAND = {:help => {:description => "you're looking at it now", :argument_list => "", :class=>"Commandable", :class_method=>true}}
17
+
18
+ class << self
19
+
20
+ # Describes your application, printed at the top of help/usage messages
21
+ attr_accessor :app_info
22
+
23
+ # Used when building the usage line, e.g. Usage: app_name [command] [parameters]
24
+ attr_accessor :app_name
25
+
26
+ # If optional parameters show default values, true by default
27
+ attr_accessor :verbose_parameters
28
+
29
+ # Boolean: If help/usage messages will print in color
30
+ attr_accessor :color_output
31
+ # What color the app_info text will be in the help message
32
+ attr_accessor :color_app_info
33
+ # What color the app_name will be in the usage line in the help message
34
+ attr_accessor :color_app_name
35
+ # What color the word "command" and the commands themselves will be in the help message
36
+ attr_accessor :color_command
37
+ # What color the description column header and text will be in the help message
38
+ attr_accessor :color_description
39
+ # What color the word "parameter" and the parameters themselves will be in the help message
40
+ attr_accessor :color_parameter
41
+ # What color the word "Usage:" will be in the help message
42
+ attr_accessor :color_usage
43
+
44
+ # What color the word "Error:" text will be in error messages
45
+ attr_accessor :color_error_word
46
+ # What color the friendly name of the error will be in error messages
47
+ attr_accessor :color_error_name
48
+ # What color the error description will be in error messages
49
+ attr_accessor :color_error_description
50
+
51
+ # An array of methods that can be executed from the command line
52
+ def commands
53
+ @@commands.dup
54
+ end
55
+
56
+ # Access the command array using the method name (symbol or string)
57
+ def [](index)
58
+ raise AccessorError unless index.is_a? String or index.is_a? Symbol
59
+ @@commands[index.to_sym]
60
+ end
61
+
62
+ # Resets the class to default values clearing any commands
63
+ # and setting the color back to their default values.
64
+ def reset_all
65
+ clear_commands
66
+ reset_colors
67
+ @app_info = nil
68
+ @app_name = nil
69
+ @verbose_parameters = true
70
+ @@default_method = nil
71
+ end
72
+ alias :init :reset_all
73
+
74
+ # Clears all methods from the list of available commands
75
+ # This is mostly useful for testing.
76
+ def clear_commands
77
+ @@commands = HELP_COMMAND.dup
78
+ end
79
+
80
+ # Convenience method to iterate over the array of commands using the Commandable module
81
+ def each(&block)
82
+ @@commands.each do |key, value|
83
+ yield key => value
84
+ end
85
+ end
86
+
87
+ # Generates an array of the available commands with a
88
+ # list of their parameters and the method's description.
89
+ # This includes the applicaiton info and app name if given.
90
+ # It's meant to be printed to the command line.
91
+ def help(additional_info=nil)
92
+
93
+ set_colors
94
+
95
+ cmd_length = "Command".length
96
+ parm_length = "Parameters".length
97
+ max_command = [(@@commands.keys.max_by{|key| key.to_s.length }).to_s.length, cmd_length].max
98
+ max_parameter = @@commands[@@commands.keys.max_by{|key| @@commands[key][:argument_list].length }][:argument_list].length
99
+ max_parameter = [parm_length, max_parameter].max if max_parameter > 0
100
+
101
+ usage_text = " #{@c_usage}Usage:#{@c_reset} "
102
+
103
+ if Commandable.app_name
104
+ cmd_text = "<#{@c_command + @c_bold}command#{@c_reset}>"
105
+ parm_text = " [#{@c_parameter + @c_bold}parameters#{@c_reset}]" if max_parameter > 0
106
+ usage_text += "#{@c_app_name + app_name + @c_reset} #{cmd_text}#{parm_text} [#{cmd_text}#{parm_text}...]"
107
+ end
108
+
109
+ array = [usage_text, ""]
110
+
111
+ array.unshift additional_info if additional_info
112
+ array.unshift ("\e[2A" + @c_app_info + Commandable.app_info + @c_reset) if Commandable.app_info
113
+ array.unshift "\e[H\e[2J"
114
+
115
+ header_text = " #{" "*(max_command-cmd_length)}#{@c_command + @c_bold}Command#{@c_reset} "
116
+ header_text += "#{@c_parameter + @c_bold}Parameters #{@c_reset}#{" "*(max_parameter-parm_length)}" if max_parameter > 0
117
+ header_text += "#{@c_description + @c_bold}Description#{@c_reset}"
118
+
119
+ array << header_text
120
+
121
+ array += @@commands.keys.collect do |key|
122
+ default = (@@default_method and key == @@default_method.keys[0]) ? @color_bold : ""
123
+
124
+ help_line = " #{" "*(max_command-key.length)}#{@c_command + default + key.to_s + @c_reset}"+
125
+ " #{default + @c_parameter + @@commands[key][:argument_list] + @c_reset}"
126
+ help_line += "#{" "*(max_parameter-@@commands[key][:argument_list].length)} " if max_parameter > 0
127
+
128
+ # indent new lines
129
+ description = @@commands[key][:description].gsub("\n", "\n" + (" "*(max_command + max_parameter + (max_parameter > 0 ? 1 : 0) + 4)))
130
+
131
+ help_line += ": #{default + @c_description}#{"<#{@@commands[key][:xor]}> " if @@commands[key][:xor]}" +
132
+ "#{description}" +
133
+ "#{" (default)" unless default == ""}#{@c_reset}"
134
+ end
135
+ array << nil
136
+ end
137
+
138
+ # A wrapper for the execution_queue that runs the queue and traps errors.
139
+ # If an error occurs inside this method it will print out a complete.
140
+ # of availavle methods with usage instructios and exit gracefully.
141
+ def execute(argv)
142
+ begin
143
+ command_queue = execution_queue(argv)
144
+ command_queue.each do |com|
145
+ puts com[:proc].call
146
+ end
147
+ rescue Exception => exception
148
+ if exception.respond_to?(:friendly_name)
149
+ set_colors
150
+ puts help(" #{@c_error_word}Error:#{@c_reset} #{@c_error_name}#{exception.friendly_name}#{@c_reset}\n #{@c_error_description}#{exception.message}#{@c_reset}\n\n")
151
+ else
152
+ #rescue Exception => exception
153
+
154
+ puts "\n Bleep, bloop, bleep! Danger Will Robinson! Danger!"
155
+ puts "\n Error: #{exception.inspect}"
156
+ puts "\n Backtrace:"
157
+ puts exception.backtrace.collect{|line| " #{line}"}
158
+ puts
159
+ end
160
+ end
161
+ end
162
+
163
+ # Returns an array of executable procs based on the given array of commands and parameters
164
+ # Normally this would come from the command line parameters in the ARGV array.
165
+ def execution_queue(argv)
166
+ arguments = argv.dup
167
+ method_hash = {}
168
+ last_method = nil
169
+
170
+ if arguments.empty?
171
+ arguments << @@default_method.keys[0] if @@default_method and !@@default_method.values[0][:parameters].flatten.include?(:req)
172
+ end
173
+ arguments << "help" if arguments.empty?
174
+
175
+ # Parse the commad line into methods and their parameters
176
+ arguments.each do |arg|
177
+ if Commandable[arg]
178
+ last_method = arg.to_sym
179
+ method_hash.merge!(last_method=>[])
180
+ else
181
+ unless last_method
182
+ default = find_by_subkey(:default)
183
+
184
+ # Raise an error if there is no default method and the first item isn't a method
185
+ raise UnknownCommandError, arguments.first if default.empty?
186
+
187
+ # Raise an error if there is a default method but it doesn't take any parameters
188
+ raise UnknownCommandError, (arguments.first) if default.values[0][:argument_list] == ""
189
+
190
+ last_method = default.keys.first
191
+ method_hash.merge!(last_method=>[])
192
+ end
193
+ method_hash[last_method] << arg
194
+ end
195
+ @@commands.select do |key, value|
196
+ raise MissingRequiredCommandError, key if value[:required] and method_hash[key].nil?
197
+ end
198
+ end
199
+
200
+ # Build an array of procs to be called for each method and its given parameters
201
+ proc_array = []
202
+ method_hash.each do |meth, params|
203
+ command = @@commands[meth]
204
+
205
+ # Test for duplicate XORs
206
+ proc_array.select{|x| x[:xor] and x[:xor]==command[:xor] }.each {|bad| raise ExclusiveMethodClashError, "#{meth}, #{bad[:method]}"}
207
+
208
+ klass = Object
209
+ command[:class].split(/::/).each { |name| klass = klass.const_get(name) }
210
+ klass = klass.new unless command[:class_method]
211
+ proc_array << {:method=>meth, :xor=>command[:xor], :parameters=>params, :priority=>command[:priority], :proc=>lambda{klass.send(meth, *params)}}
212
+ end
213
+ proc_array.sort{|a,b| a[:priority] <=> b[:priority]}.reverse
214
+ end
215
+
216
+ # Set colors to their default values
217
+ def reset_colors
218
+ # Colors - off by default
219
+ @color_output ||= false
220
+ # Build the default colors
221
+ Term::ANSIColor.coloring = true
222
+ c = Term::ANSIColor
223
+ @color_app_info = c.intense_white + c.bold
224
+ @color_app_name = c.intense_green + c.bold
225
+ @color_command = c.intense_yellow
226
+ @color_description = c.intense_white
227
+ @color_parameter = c.intense_cyan
228
+ @color_usage = c.intense_black + c.bold
229
+
230
+ @color_error_word = c.intense_black + c.bold
231
+ @color_error_name = c.intense_red + c.bold
232
+ @color_error_description = c.intense_white + c.bold
233
+
234
+ @color_bold = c.bold
235
+ @color_reset = c.reset
236
+ @screen_clear = "\e[H\e[2J"
237
+ end
238
+
239
+ private
240
+
241
+ # Look through commands for a specific subkey
242
+ def find_by_subkey(key, value=true)
243
+ @@commands.select {|meth, meth_value| meth_value[key]==value}
244
+ end
245
+
246
+ # Changes the colors used when print the help/usage instructions to those set by the user.
247
+ def set_colors
248
+ if color_output
249
+ @c_app_info = @color_app_info
250
+ @c_app_name = @color_app_name
251
+ @c_command = @color_command
252
+ @c_description = @color_description
253
+ @c_parameter = @color_parameter
254
+ @c_usage = @color_usage
255
+
256
+ @c_error_word = @color_error_word
257
+ @c_error_name = @color_error_name
258
+ @c_error_description = @color_error_description
259
+
260
+ @c_bold = @color_bold
261
+ @c_reset = @color_reset
262
+ else
263
+ @c_app_info, @c_app_name, @c_command, @c_description,
264
+ @c_parameter, @c_usage, @c_bold, @c_reset, @c_error_word,
265
+ @c_error_name, @c_error_description = [""]*12
266
+ end
267
+ end
268
+
269
+ end
270
+ init # automatically configure the module when it's loaded
271
+
272
+ private
273
+
274
+ # This is where the magic happens!
275
+ # It lets you add a method to the list of command line methods
276
+ def command(*cmd_parameters)
277
+
278
+ @@method_file = nil
279
+ @@method_line = nil
280
+ @@command_options = {}
281
+
282
+ # Include Commandable in singleton classes so class level methods work
283
+ include Commandable unless self.include? Commandable
284
+
285
+ # parse command parameters
286
+ while (param = cmd_parameters.shift)
287
+ case param
288
+ when Symbol
289
+ if param == :xor
290
+ @@command_options.merge!(param=>:xor)
291
+ else
292
+ @@command_options.merge!(param=>true)
293
+ end
294
+ when Hash
295
+ @@command_options.merge!(param)
296
+ when String
297
+ @@command_options.merge!(:description=>param)
298
+ end
299
+ end
300
+ @@command_options[:priority] ||= 0
301
+
302
+ # only one default allowed
303
+ raise ConfigurationError, "Only one default method is allowed." if @@default_method and @@command_options[:default]
304
+
305
+ set_trace_func proc { |event, file, line, id, binding, classname|
306
+
307
+ # Traps the line where the method is defined so we can look up
308
+ # the method source code later if there are optional parameters
309
+ if event == "line" and !@@method_file
310
+ @@method_file = file
311
+ @@method_line = line
312
+ end
313
+
314
+ # Raise an error if there is no method following a command definition
315
+ if event == "end"
316
+ set_trace_func(nil)
317
+ raise SyntaxError, "A command was specified but no method follows"
318
+ end
319
+ }
320
+ end
321
+
322
+ # Add a method to the list of available command line methods
323
+ def add_command(meth)
324
+ @@commands.delete(:help)
325
+ argument_list = parse_arguments(@@command_options[:parameters])
326
+ @@command_options.merge!(:argument_list=>argument_list,:class => self.name)
327
+ @@commands.merge!(meth => @@command_options)
328
+ @@default_method = {meth => @@command_options} if @@command_options[:default]
329
+
330
+ @@commands.sort.each {|com| @@commands.merge!(com[0]=>@@commands.delete(com[0]))}
331
+
332
+ @@commands.merge!(HELP_COMMAND.dup) # makes sure the help command is always last
333
+ @@command_options = nil
334
+ end
335
+
336
+ # Trap method creation after a command call
337
+ def method_added(meth)
338
+ set_trace_func(nil)
339
+ return super(meth) if meth == :initialize || @@command_options == nil
340
+ @@command_options.merge!(:parameters=>self.instance_method(meth).parameters,:class_method=>false)
341
+ add_command(meth)
342
+ end
343
+
344
+ # Trap class methods too
345
+ def singleton_method_added(meth)
346
+ set_trace_func(nil)
347
+ return super(meth) if meth == :initialize || @@command_options == nil
348
+ @@command_options.merge!(:parameters=>method(meth).parameters, :class_method=>true)
349
+ add_command(meth)
350
+ end
351
+
352
+ # Parse a method's parameters building the argument list for printing help/usage
353
+ def parse_arguments(parameters)
354
+ parameter_string = ""
355
+ method_definition = nil
356
+ parameters.each do |parameter|
357
+ arg_type = parameter[0]
358
+ arg = parameter[1]
359
+ case arg_type
360
+ when :req
361
+ parameter_string += " #{arg}"
362
+ when :opt
363
+ if Commandable.verbose_parameters
364
+ # figure out what the default value is
365
+ method_definition ||= readline(@@method_file, @@method_line)
366
+ default = parse_optional(method_definition, arg)
367
+ parameter_string += " [#{arg}=#{default}]"
368
+ else
369
+ parameter_string += " [#{arg}]"
370
+ end
371
+ when :rest
372
+ parameter_string += " *#{arg}"
373
+ when :block
374
+ parameter_string += " &#{arg}"
375
+ end
376
+ end
377
+ parameter_string.strip
378
+ end
379
+
380
+ # Reads a line from a source code file.
381
+ def readline(file, line_number)
382
+ current_line = 0
383
+ File.open(file).each { |line_text|
384
+ current_line += 1
385
+ return line_text.strip if current_line == line_number
386
+ }
387
+ end
388
+
389
+ # Parses a method defition for the optional values of given argument.
390
+ def parse_optional(method_def, argument)
391
+ method_def.scan(/#{argument}\s*=\s*("[^"\r\n]*"|'[^'\r\n]*'|[0-9]*)/)[0][0]
392
+ end
393
+
394
+ end