commandable 0.2.3 → 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,139 @@
1
+ require 'set'
2
+
3
+ module Commandable
4
+
5
+ class << self
6
+
7
+ # An array of methods that can be executed from the command line
8
+ def commands
9
+ @@commands.dup
10
+ end
11
+
12
+ # A hash of instances created when calling instance methods
13
+ # It's keyed using the class name: {"ClassName"=>#<ClassName:0x00000100b1f188>}
14
+ def class_cache
15
+ @@class_cache
16
+ end
17
+
18
+ # Access the command array using the method name (symbol or string)
19
+ def [](index)
20
+ raise AccessorError unless index.is_a? String or index.is_a? Symbol
21
+ @@commands[index.to_sym]
22
+ end
23
+
24
+ # Clears all methods from the list of available commands
25
+ # This is mostly useful for testing.
26
+ def clear_commands
27
+ @@command_options = nil
28
+ @@commands = HELP_COMMAND.dup
29
+ end
30
+
31
+ # Convenience method to iterate over the array of commands using the Commandable module
32
+ def each(&block)
33
+ @@commands.each do |key, value|
34
+ yield key => value
35
+ end
36
+ end
37
+
38
+ end
39
+
40
+ private
41
+
42
+ # This is where the magic happens!
43
+ # It lets you add a method to the list of command line methods
44
+ def command(*cmd_parameters)
45
+
46
+ @@attribute = nil
47
+ @@method_file = nil
48
+ @@method_line = nil
49
+ @@command_options = {}
50
+
51
+ # Include Commandable in singleton classes so class level methods work
52
+ include Commandable unless self.include? Commandable
53
+
54
+ # parse command parameters
55
+ while (param = cmd_parameters.shift)
56
+ case param
57
+ when Symbol
58
+ if param == :xor
59
+ @@command_options.merge!(param=>:xor)
60
+ else
61
+ @@command_options.merge!(param=>true)
62
+ end
63
+ when Hash
64
+ @@command_options.merge!(param)
65
+ when String
66
+ @@command_options.merge!(:description=>param)
67
+ end
68
+ end
69
+ @@command_options[:priority] ||= 0
70
+
71
+ # only one default allowed
72
+ raise ConfigurationError, "Only one default method is allowed." if @@default_method and @@command_options[:default]
73
+
74
+ set_trace_func proc { |event, file, line, id, binding, classname|
75
+
76
+ @@attribute = id if [:attr_accessor, :attr_writer].include?(id)
77
+
78
+ # Traps the line where the method is defined so we can look up
79
+ # the method source code later if there are optional parameters
80
+ if event == "line" and !@@method_file
81
+ @@method_file = file
82
+ @@method_line = line
83
+ end
84
+
85
+ # Raise an error if there is no method following a command definition
86
+ if event == "end"
87
+ set_trace_func(nil)
88
+ raise SyntaxError, "A command was specified but no method follows"
89
+ end
90
+ }
91
+ end
92
+
93
+ # Add a method to the list of available command line methods
94
+ def add_command(meth)
95
+ @@commands.delete(:help)
96
+
97
+ if @@attribute
98
+ argument_list = "value"
99
+ meth = meth.to_s.delete("=").to_sym if @@attribute == :attr_writer
100
+ else
101
+ argument_list = parse_arguments(@@command_options[:parameters])
102
+ end
103
+ @@command_options.merge!(:argument_list=>argument_list,:class => self.name)
104
+
105
+ @@commands.merge!(meth => @@command_options)
106
+ @@default_method = {meth => @@command_options} if @@command_options[:default]
107
+
108
+ @@commands.sort.each {|com| @@commands.merge!(com[0]=>@@commands.delete(com[0]))}
109
+
110
+ @@commands.merge!(HELP_COMMAND.dup) # makes sure the help command is always last
111
+ @@command_options = nil
112
+ @@attribute = nil
113
+ end
114
+
115
+ # Trap method creation after a command call
116
+ def method_added(meth)
117
+ set_trace_func(nil)
118
+ return super(meth) if meth == :initialize || @@command_options == nil
119
+
120
+ if @@attribute
121
+ #synthesize parameter
122
+ @@command_options.merge!(:parameters=>[[:writer, :value]],:class_method=>false)
123
+ else
124
+ # create parameter
125
+ @@command_options.merge!(:parameters=>self.instance_method(meth).parameters,:class_method=>false)
126
+ end
127
+
128
+ add_command(meth)
129
+ end
130
+
131
+ # Trap class methods too
132
+ def singleton_method_added(meth)
133
+ set_trace_func(nil)
134
+ return super(meth) if meth == :initialize || @@command_options == nil
135
+ @@command_options.merge!(:parameters=>method(meth).parameters, :class_method=>true)
136
+ add_command(meth)
137
+ end
138
+
139
+ end
@@ -0,0 +1,24 @@
1
+ module Commandable
2
+
3
+ class << self
4
+
5
+ # Resets the class to default values clearing any commands
6
+ # and setting the colors back to their default values.
7
+ def reset_all
8
+ clear_commands
9
+ reset_colors
10
+ reset_screen_clearing
11
+
12
+ @app_info = nil
13
+ @app_exe = nil
14
+ @verbose_parameters = true
15
+ @@default_method = nil
16
+ @@class_cache = {}
17
+ end
18
+
19
+ end
20
+
21
+ # Inititializes Commandable's settings when it's first loaded
22
+ reset_all
23
+
24
+ end
@@ -75,4 +75,4 @@ module Commandable
75
75
  end
76
76
  end
77
77
 
78
- end
78
+ end
@@ -0,0 +1,122 @@
1
+ require 'set'
2
+
3
+ # Extending your class with this module allows you to
4
+ # use the #command method above your method.
5
+ # This makes them executable from the command line.
6
+ module Commandable
7
+
8
+ class << self
9
+
10
+ # A wrapper for the execution_queue that runs the queue and traps errors.
11
+ # If an error occurs inside this method it will print out a complete list
12
+ # of availavle methods with usage instructions and exit gracefully.
13
+ #
14
+ # If you do not want the output from your methods to be printed out automatically
15
+ # run the execute command with silent set to anything other than false or nil.
16
+ def execute(argv=ARGV.clone, silent=false)
17
+ begin
18
+ command_queue = execution_queue(argv)
19
+ command_queue.each do |com|
20
+ return_value = com[:proc].call
21
+ puts return_value if !silent || com[:method] == :help
22
+ end
23
+ rescue SystemExit => kernel_exit
24
+ Kernel.exit kernel_exit.status
25
+ rescue Exception => exception
26
+ if exception.respond_to?(:friendly_name)
27
+ set_colors
28
+ puts help("\n #{@c_error_word}Error:#{@c_reset} #{@c_error_name}#{exception.friendly_name}#{@c_reset}\n #{@c_error_description}#{exception.message}#{@c_reset}\n\n")
29
+ else
30
+ puts exception.inspect
31
+ puts exception.backtrace.collect{|line| " #{line}"}
32
+ end
33
+ end
34
+ end
35
+
36
+ # Returns an array of executable procs based on the given array of commands and parameters
37
+ # Normally this would come from the command line parameters in the ARGV array.
38
+ def execution_queue(argv)
39
+ arguments = argv.dup
40
+ method_hash = {}
41
+ last_method = nil
42
+
43
+ if arguments.empty?
44
+ arguments << @@default_method.keys[0] if @@default_method and !@@default_method.values[0][:parameters].flatten.include?(:req)
45
+ end
46
+ arguments << "help" if arguments.empty?
47
+
48
+ # Parse the command line into methods and their parameters
49
+
50
+ arguments.each do |arg|
51
+ if Commandable[arg]
52
+ last_method = arg.to_sym
53
+ method_hash.merge!(last_method=>[])
54
+ else
55
+ unless last_method
56
+ default = find_by_subkey(:default)
57
+
58
+ # Raise an error if there is no default method and the first item isn't a method
59
+ raise UnknownCommandError, arguments.first if default.empty?
60
+
61
+ # Raise an error if there is a default method but it doesn't take any parameters
62
+ raise UnknownCommandError, (arguments.first) if default.values[0][:argument_list] == ""
63
+
64
+ last_method = default.keys.first
65
+ method_hash.merge!(last_method=>[])
66
+ end
67
+ method_hash[last_method] << arg
68
+ end
69
+ # Test for missing required switches
70
+ @@commands.select do |key, value|
71
+ if value[:required] and method_hash[key].nil?
72
+ # If the required switch is also a default have the error be a missing parameter instead of a missing command
73
+ if value[:default]
74
+ method_hash.merge!(key=>[])
75
+ else
76
+ raise MissingRequiredCommandError, key
77
+ end
78
+ end
79
+ end
80
+ end
81
+
82
+ # Build an array of procs to be called for each method and its given parameters
83
+ proc_array = []
84
+ method_hash.each do |meth, params|
85
+ command = @@commands[meth]
86
+
87
+ if command[:parameters] && !command[:parameters].empty?
88
+
89
+ #Change the method name for attr_writers
90
+ meth = "#{meth}=" if command[:parameters][0][0] == :writer
91
+
92
+ # Get a list of required parameters and make sure all of them were provided
93
+ required = command[:parameters].select{|param| [:req, :writer].include?(param[0])}
94
+ required.shift(params.count)
95
+ raise MissingRequiredParameterError, {:method=>meth, :parameters=>required.collect!{|meth| meth[1]}.to_s[1...-1].gsub(":",""), :default=>command[:default]} unless required.empty?
96
+ end
97
+
98
+ # Test for duplicate XORs
99
+ proc_array.select{|x| x[:xor] and x[:xor]==command[:xor] }.each {|bad| raise ExclusiveMethodClashError, "#{meth}, #{bad[:method]}"}
100
+
101
+ klass = Object
102
+ command[:class].split(/::/).each { |name| klass = klass.const_get(name) }
103
+ ## Look for class in class cache
104
+ unless command[:class_method]
105
+ klass = (@@class_cache[klass.name] ||= klass.new)
106
+ end
107
+ proc_array << {:method=>meth, :xor=>command[:xor], :parameters=>params, :priority=>command[:priority], :proc=>lambda{klass.send(meth, *params)}}
108
+ end
109
+ proc_array.sort{|a,b| a[:priority] <=> b[:priority]}.reverse
110
+
111
+ end
112
+
113
+ private
114
+
115
+ # Look through commands for a specific subkey
116
+ def find_by_subkey(key, value=true)
117
+ @@commands.select {|meth, meth_value| meth_value[key]==value}
118
+ end
119
+
120
+ end
121
+
122
+ end
@@ -0,0 +1,73 @@
1
+ module Commandable
2
+
3
+ # Default command that always gets added to end of the command list
4
+ HELP_COMMAND = {:help => {:description => "you're looking at it now", :argument_list => "", :class=>"Commandable", :class_method=>true}}
5
+
6
+ class << self
7
+
8
+ # Describes your application, printed at the top of help/usage messages
9
+ attr_accessor :app_info
10
+
11
+ # Used when building the usage line, e.g. Usage: app_exe [command] [parameters]
12
+ attr_accessor :app_exe
13
+
14
+ # If optional parameters show default values, true by default
15
+ attr_accessor :verbose_parameters
16
+
17
+ # Generates an array of the available commands with a
18
+ # list of their parameters and the method's description.
19
+ # This includes the applicaiton info and app name if given.
20
+ # It's meant to be printed to the command line.
21
+ def help(additional_info=nil)
22
+
23
+ set_colors
24
+ set_screen_clear
25
+
26
+ cmd_length = "Command".length
27
+ parm_length = "Parameters".length
28
+ max_command = [(@@commands.keys.max_by{|key| key.to_s.length }).to_s.length, cmd_length].max
29
+ max_parameter = @@commands[@@commands.keys.max_by{|key| @@commands[key][:argument_list].length }][:argument_list].length
30
+ max_parameter = [parm_length, max_parameter].max if max_parameter > 0
31
+
32
+ usage_text = " #{@c_usage}Usage:#{@c_reset} "
33
+
34
+ if Commandable.app_exe
35
+ cmd_text = "<#{@c_command + @c_bold}command#{@c_reset}>"
36
+ parm_text = " [#{@c_parameter + @c_bold}parameters#{@c_reset}]" if max_parameter > 0
37
+ usage_text += "#{@c_app_exe + app_exe + @c_reset} #{cmd_text}#{parm_text} [#{cmd_text}#{parm_text}...]"
38
+ end
39
+
40
+ array = [usage_text, ""]
41
+
42
+ array.unshift additional_info if additional_info
43
+ array.unshift (@c_app_info + Commandable.app_info + @c_reset) if Commandable.app_info
44
+ array.unshift @s_clear_screen_code
45
+
46
+ header_text = " #{" "*(max_command-cmd_length)}#{@c_command + @c_bold}Command#{@c_reset} "
47
+ header_text += "#{@c_parameter + @c_bold}Parameters #{@c_reset}#{" "*(max_parameter-parm_length)}" if max_parameter > 0
48
+ header_text += "#{@c_description + @c_bold}Description#{@c_reset}"
49
+
50
+ array << header_text
51
+
52
+ array += @@commands.keys.collect do |key|
53
+ is_default = (@@default_method and key == @@default_method.keys[0])
54
+ default_color = is_default ? @c_bold : ""
55
+
56
+ help_line = " #{" "*(max_command-key.length)}#{@c_command + default_color + key.to_s + @c_reset}"+
57
+ " #{default_color + @c_parameter + @@commands[key][:argument_list] + @c_reset}"
58
+ help_line += "#{" "*(max_parameter-@@commands[key][:argument_list].length)} " if max_parameter > 0
59
+
60
+ # indent new lines
61
+ description = @@commands[key][:description].gsub("\n", "\n" + (" "*(max_command + max_parameter + (max_parameter > 0 ? 1 : 0) + 4)))
62
+
63
+ help_line += ": #{default_color + @c_description}#{"<#{@@commands[key][:xor]}> " if @@commands[key][:xor]}" +
64
+ "#{description}" +
65
+ "#{" (default)" if is_default}#{@c_reset}"
66
+ end
67
+ array << nil
68
+ end
69
+
70
+
71
+ end
72
+
73
+ end
@@ -1,8 +1,9 @@
1
+ # :nodoc:
1
2
  module Commandable
2
- module VERSION # :nodoc:
3
+ module VERSION
3
4
  MAJOR = 0
4
- MINOR = 2
5
- TINY = 3
5
+ MINOR = 3
6
+ TINY = 1
6
7
  PRE = nil
7
8
 
8
9
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.')
@@ -18,37 +18,9 @@ describe Commandable do
18
18
  # or it won't be able to use the settings
19
19
  load 'commandable/app_controller.rb'
20
20
  }
21
-
22
- context "when running the widget command" do
23
21
 
24
- context "when git isn't installed" do
25
-
26
- it "should inform them they need Git" do
27
- Commandable::AppController.stub(:git_installed?){false}
28
- execute_output_s(["widget"]).should match(/Git must be installed/)
29
- end
30
-
31
- end
32
- context "when git is installed" do
33
-
34
- context "and it's able to install the files" do
35
- it "should download Widget from github" do
36
- Commandable::AppController.stub(:download_widget){0}
37
- execute_output_s(["widget"]).should_not include("Unable to download")
38
- end
39
- end
40
-
41
- context "but it's not able to install the files" do
42
- it "should download Widget from github" do
43
- Commandable::AppController.stub(:download_widget){1}
44
- execute_output_s(["widget"]).should include("Unable to download")
45
- end
46
- end
47
-
48
-
49
- end
22
+ it "should have a test for examples"
50
23
 
51
-
52
- end
24
+ it "should have a test for the readme file"
53
25
 
54
26
  end
@@ -0,0 +1,96 @@
1
+ require 'spec_helper'
2
+
3
+ describe Commandable do
4
+
5
+ before(:each) do
6
+ Commandable.reset_all
7
+ load 'private_methods_bad.rb'
8
+ Commandable.app_exe = "mycoolapp"
9
+ Commandable.app_info =
10
+ """ My Cool App - It does stuff and things!
11
+ Copyright (c) 2011 Acme Inc."""
12
+ end
13
+
14
+ let(:c) {Term::ANSIColor}
15
+
16
+ context "when screen clearing" do
17
+
18
+ it "should clear the screen if screen clearing is on" do
19
+ Commandable.clear_screen = true
20
+ clear_screen_code = Regexp.escape("#{Commandable.clear_screen_code}")
21
+ Commandable.help.join.should match(clear_screen_code)
22
+ end
23
+
24
+ it "should not clear the screen if screen clearing is off" do
25
+ Commandable.clear_screen = true
26
+ clear_screen_code = Regexp.escape("#{Commandable.clear_screen_code}")
27
+ Commandable.clear_screen = false
28
+ Commandable.help.join.should_not match(clear_screen_code)
29
+ end
30
+
31
+ it "should change how the screen is cleared" do
32
+ Commandable.clear_screen = true
33
+ clear_code = "FlabbityJibbity"
34
+ Commandable.clear_screen_code = clear_code
35
+ Commandable.help.join.should match(clear_code)
36
+ end
37
+
38
+ it "should not be disabled when color output is disabled" do
39
+ Commandable.clear_screen = true
40
+ clear_screen_code = Regexp.escape("#{Commandable.clear_screen_code}")
41
+ Commandable.color_output = false
42
+ Commandable.help.join.should match(clear_screen_code)
43
+ end
44
+
45
+ end
46
+
47
+ context "when a setting color is changed" do
48
+
49
+ before(:each) { Commandable.color_output = true }
50
+
51
+ it "should include colors if colored output is enabled" do
52
+ Commandable.color_output = true
53
+ Commandable.help.join.should match(Regexp.escape(Commandable.color_app_info))
54
+ end
55
+
56
+ it "should not include colors if colored output is disabled" do
57
+ Commandable.color_output = false
58
+ Commandable.help.join.should_not match(Regexp.escape(Commandable.color_app_info))
59
+ end
60
+
61
+ # This seems ripe for meta-zation
62
+ context "and app_info is changed" do
63
+ specify {lambda {Commandable.color_app_info = c.black}.should change{Commandable.help}}
64
+ end
65
+
66
+ context "and app_exe is changed" do
67
+ specify {lambda {Commandable.color_app_exe = c.black}.should change{Commandable.help}}
68
+ end
69
+
70
+ context "and color_command is changed" do
71
+ specify {lambda {Commandable.color_command = c.black}.should change{Commandable.help}}
72
+ end
73
+
74
+ context "and color_description is changed" do
75
+ specify {lambda {Commandable.color_description = c.black}.should change{Commandable.help}}
76
+ end
77
+
78
+ context "and color_parameter is changed" do
79
+ specify {lambda {Commandable.color_parameter = c.black}.should change{Commandable.help}}
80
+ end
81
+
82
+ context "and color_usage is changed" do
83
+ specify {lambda {Commandable.color_usage = c.black}.should change{Commandable.help}}
84
+ end
85
+
86
+ context "and there is an error" do
87
+
88
+ specify { lambda {Commandable.color_error_word = c.magenta}.should change{capture_output{Commandable.execute(["fly", "navy"])}}}
89
+ specify { lambda {Commandable.color_error_name = c.intense_red}.should change{capture_output{Commandable.execute(["fly", "navy"])}}}
90
+ specify { lambda {Commandable.color_error_description = c.black + c.bold}.should change{capture_output{Commandable.execute(["fly", "navy"])}}}
91
+
92
+ end
93
+
94
+ end
95
+
96
+ end