commandable 0.2.0.beta01

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