commandable 0.2.3 → 0.3.1

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.
@@ -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