escort 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (76) hide show
  1. checksums.yaml +15 -0
  2. data/.gitignore +1 -0
  3. data/.irbrc +1 -0
  4. data/.rspec +3 -0
  5. data/.rvmrc +22 -0
  6. data/README.md +31 -56
  7. data/TODO.md +152 -0
  8. data/escort.gemspec +6 -2
  9. data/examples/1_1_basic.rb +15 -0
  10. data/examples/1_2_basic_requires_arguments.rb +15 -0
  11. data/examples/2_2_command.rb +18 -0
  12. data/examples/2_2_command_requires_arguments.rb +20 -0
  13. data/examples/2_3_nested_commands.rb +26 -0
  14. data/examples/3_validations.rb +31 -0
  15. data/examples/4_1_config_file.rb +42 -0
  16. data/examples/argument_handling/basic.rb +12 -0
  17. data/examples/argument_handling/basic_command.rb +18 -0
  18. data/examples/argument_handling/no_arguments.rb +14 -0
  19. data/examples/argument_handling/no_arguments_command.rb +20 -0
  20. data/examples/basic/app.rb +16 -0
  21. data/examples/command_aliases/app.rb +31 -0
  22. data/examples/config_file/.apprc2 +16 -0
  23. data/examples/config_file/app.rb +78 -0
  24. data/examples/config_file/sub_commands.rb +35 -0
  25. data/examples/default_command/app.rb +20 -0
  26. data/examples/sub_commands/app.rb +18 -0
  27. data/examples/validation_basic/app.rb +31 -0
  28. data/lib/escort.rb +51 -4
  29. data/lib/escort/action_command/base.rb +79 -0
  30. data/lib/escort/action_command/escort_utility_command.rb +53 -0
  31. data/lib/escort/app.rb +89 -36
  32. data/lib/escort/arguments.rb +20 -0
  33. data/lib/escort/auto_options.rb +71 -0
  34. data/lib/escort/error/error.rb +50 -0
  35. data/lib/escort/formatter/borderless_table.rb +102 -0
  36. data/lib/escort/formatter/common.rb +58 -0
  37. data/lib/escort/formatter/default_help_formatter.rb +106 -0
  38. data/lib/escort/formatter/options.rb +13 -0
  39. data/lib/escort/formatter/string_splitter.rb +30 -0
  40. data/lib/escort/formatter/terminal.rb +22 -0
  41. data/lib/escort/formatter/terminal_formatter.rb +52 -0
  42. data/lib/escort/global_pre_parser.rb +43 -0
  43. data/lib/escort/logger.rb +75 -0
  44. data/lib/escort/option_parser.rb +145 -0
  45. data/lib/escort/setup/configuration/generator.rb +75 -0
  46. data/lib/escort/setup/configuration/instance.rb +33 -0
  47. data/lib/escort/setup/configuration/loader.rb +37 -0
  48. data/lib/escort/setup/configuration/locator/base.rb +19 -0
  49. data/lib/escort/setup/configuration/locator/descending_to_home.rb +23 -0
  50. data/lib/escort/setup/configuration/merge_tool.rb +38 -0
  51. data/lib/escort/setup/configuration/reader.rb +36 -0
  52. data/lib/escort/setup/configuration/writer.rb +44 -0
  53. data/lib/escort/setup/dsl/action.rb +17 -0
  54. data/lib/escort/setup/dsl/command.rb +74 -0
  55. data/lib/escort/setup/dsl/config_file.rb +13 -0
  56. data/lib/escort/setup/dsl/global.rb +84 -0
  57. data/lib/escort/setup/dsl/options.rb +25 -0
  58. data/lib/escort/setup/dsl/validations.rb +25 -0
  59. data/lib/escort/setup_accessor.rb +194 -0
  60. data/lib/escort/trollop.rb +15 -4
  61. data/lib/escort/utils.rb +21 -0
  62. data/lib/escort/validator.rb +42 -0
  63. data/lib/escort/version.rb +1 -1
  64. data/spec/helpers/execute_action_matcher.rb +21 -0
  65. data/spec/helpers/exit_with_code_matcher.rb +21 -0
  66. data/spec/helpers/give_option_to_action_with_value_matcher.rb +22 -0
  67. data/spec/integration/basic_options_spec.rb +53 -0
  68. data/spec/integration/basic_spec.rb +24 -0
  69. data/spec/lib/escort/formatter/string_splitter_spec.rb +38 -0
  70. data/spec/lib/escort/setup_accessor_spec.rb +42 -0
  71. data/spec/lib/escort/utils_spec.rb +30 -0
  72. data/spec/spec_helper.rb +22 -0
  73. metadata +128 -16
  74. data/lib/escort/command.rb +0 -23
  75. data/lib/escort/dsl.rb +0 -20
  76. data/lib/escort/global_dsl.rb +0 -11
@@ -0,0 +1,25 @@
1
+ module Escort
2
+ module Setup
3
+ module Dsl
4
+ class Options
5
+ class << self
6
+ def options(command_name, instance, &block)
7
+ block.call(instance) if block_given?
8
+ rescue => e
9
+ raise Escort::ClientError.new("Problem with syntax of #{instance.instance_variable_get(:"@command_name")} options block", e)
10
+ end
11
+ end
12
+
13
+ def initialize(command_name = :global)
14
+ @command_name = command_name
15
+ @options = {}
16
+ end
17
+
18
+ def opt(name, desc="", opts={})
19
+ opts[:desc] ||= desc
20
+ @options[name] ||= opts
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,25 @@
1
+ module Escort
2
+ module Setup
3
+ module Dsl
4
+ class Validations
5
+ class << self
6
+ def validations(command_name, instance, &block)
7
+ block.call(instance) if block_given?
8
+ rescue => e
9
+ raise Escort::ClientError.new("Problem with syntax of #{instance.instance_variable_get(:"@command_name")} validations block", e)
10
+ end
11
+ end
12
+
13
+ def initialize(command_name = :global)
14
+ @command_name = command_name
15
+ @validations = {}
16
+ end
17
+
18
+ def validate(name, description, &block)
19
+ @validations[name] ||= []
20
+ @validations[name] << {:desc => description, :block => block}
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,194 @@
1
+ module Escort
2
+ class SetupAccessor
3
+ attr_reader :global_instance
4
+
5
+ def initialize(global_instance)
6
+ @global_instance = global_instance
7
+ end
8
+
9
+ def options_for(context = [])
10
+ with_context(context) do |current_context|
11
+ options_hash_from(current_context)
12
+ end
13
+ end
14
+
15
+ def conflicting_options_for(context = [])
16
+ with_context(context) do |current_context|
17
+ conflict_lists_for(current_context)
18
+ end
19
+ end
20
+
21
+
22
+ def validations_for(context = [])
23
+ with_context(context) do |current_context|
24
+ validations_hash_from(current_context)
25
+ end
26
+ end
27
+
28
+ def command_names_for(context = [])
29
+ with_context(context) do |current_context|
30
+ command_names_from(current_context)
31
+ end
32
+ end
33
+
34
+ def canonical_command_names_for(context = [])
35
+ with_context(context) do |current_context|
36
+ canonical_command_names_from(current_context)
37
+ end
38
+ end
39
+
40
+ def action_for(context = [])
41
+ with_context(context) do |current_context|
42
+ action_block_from(current_context)
43
+ end
44
+ end
45
+
46
+ def arguments_required_for(context = [])
47
+ with_context(context) do |current_context|
48
+ context_requires_arguments(current_context)
49
+ end
50
+ end
51
+
52
+ def has_config_file?
53
+ config_file_object != nil
54
+ end
55
+
56
+ def config_file_autocreatable?
57
+ autocreatable = fetch_instance_variable_from(config_file_object, :autocreate)
58
+ end
59
+
60
+ def config_file
61
+ name = fetch_instance_variable_from(config_file_object, :name)
62
+ end
63
+
64
+ def version
65
+ version_string = fetch_instance_variable_from(global_instance, :version)
66
+ end
67
+
68
+ def summary_for(context = [])
69
+ with_context(context) do |current_context|
70
+ fetch_instance_variable_from(current_context, :summary)
71
+ end
72
+ end
73
+
74
+ def description_for(context = [])
75
+ with_context(context) do |current_context|
76
+ fetch_instance_variable_from(current_context, :description)
77
+ end
78
+ end
79
+
80
+ def command_description_for(command_name, context = [])
81
+ with_context(context) do |current_context|
82
+ commands = fetch_instance_variable_from(current_context, :commands)
83
+ fetch_instance_variable_from(commands[command_name], :description)
84
+ end
85
+ end
86
+
87
+ def command_summary_for(command_name, context = [])
88
+ with_context(context) do |current_context|
89
+ commands = fetch_instance_variable_from(current_context, :commands)
90
+ fetch_instance_variable_from(commands[command_name], :summary)
91
+ end
92
+ end
93
+
94
+ def command_aliases_for(command_name, context = [])
95
+ with_context(context) do |current_context|
96
+ commands = fetch_instance_variable_from(current_context, :commands)
97
+ description = fetch_instance_variable_from(commands[command_name], :aliases)
98
+ end
99
+ end
100
+
101
+ def add_global_option(name, desc, options = {})
102
+ with_context([]) do |current_context|
103
+ options_object = options_object_from(current_context)
104
+ options_object.opt name, desc, options
105
+ end
106
+ end
107
+
108
+ def add_global_command(name, options = {}, &block)
109
+ global_instance.command(name, options, &block)
110
+
111
+ #with_context([]) do |current_context|
112
+ #commands = fetch_instance_variable_from(current_context, :commands)
113
+ #options_object.opt name, desc, options
114
+ #end
115
+ end
116
+
117
+ private
118
+
119
+ def config_file_object
120
+ config_file = fetch_instance_variable_from(global_instance, :config_file)
121
+ end
122
+
123
+ def with_context(context = [], &block)
124
+ context = [] if context.nil? || context.empty?
125
+ context = [context] unless context.kind_of?(Array)
126
+ current_context = global_instance
127
+ context.each do |command_name|
128
+ commands = fetch_instance_variable_from(current_context, :commands)
129
+ current_context = commands[command_name.to_sym]
130
+ end
131
+ block.call(current_context)
132
+ end
133
+
134
+ def context_requires_arguments(context_object)
135
+ requires_arguments = fetch_instance_variable_from(context_object, :requires_arguments)
136
+ end
137
+
138
+ def action_block_from(context_object)
139
+ action_object = fetch_instance_variable_from(context_object, :action)
140
+ block = fetch_instance_variable_from(action_object, :block)
141
+ #TODO make sure that if there is no block we exit with a client error
142
+ end
143
+
144
+ def command_names_from(context_object)
145
+ commands = fetch_instance_variable_from(context_object, :commands)
146
+ commands.keys
147
+ #TODO make sure there can be no errors here and at worst it is an empty array
148
+ end
149
+
150
+ def canonical_command_names_from(context_object)
151
+ commands = fetch_instance_variable_from(context_object, :commands)
152
+ commands.select do |key, command|
153
+ aliases = fetch_instance_variable_from(command, :aliases)
154
+ !aliases.include?(key)
155
+ end.keys
156
+ #TODO make sure there can be no errors here and at worst it is an empty array
157
+ end
158
+
159
+ def options_hash_from(context_object)
160
+ ensure_context_object(context_object, {}) do
161
+ options_object = options_object_from(context_object)
162
+ fetch_instance_variable_from(options_object, :options)
163
+ end
164
+ end
165
+
166
+ def options_object_from(context_object)
167
+ ensure_context_object(context_object, nil) do
168
+ fetch_instance_variable_from(context_object, :options)
169
+ end
170
+ end
171
+
172
+ def conflict_lists_for(context_object)
173
+ fetch_instance_variable_from(context_object, :conflicts)
174
+ end
175
+
176
+ def validations_hash_from(context_object)
177
+ validations_object = fetch_instance_variable_from(context_object, :validations)
178
+ fetch_instance_variable_from(validations_object, :validations)
179
+ end
180
+
181
+ def fetch_instance_variable_from_setup(instance_variable)
182
+ fetch_instance_variable_from(global_instance, instance_variable)
183
+ end
184
+
185
+ def fetch_instance_variable_from(instance, instance_variable)
186
+ instance_variable_symbol = :"@#{instance_variable.to_s}"
187
+ instance.instance_variable_get(instance_variable_symbol)
188
+ end
189
+
190
+ def ensure_context_object(context_object, default_value, &block)
191
+ context_object ? block.call : default_value
192
+ end
193
+ end
194
+ end
@@ -66,6 +66,8 @@ class Parser
66
66
  ## for testing.)
67
67
  attr_reader :specs
68
68
 
69
+ attr_reader :order
70
+
69
71
  ## Initializes the parser, and instance-evaluates any block given.
70
72
  def initialize *a, &b
71
73
  @version = nil
@@ -77,6 +79,7 @@ class Parser
77
79
  @constraints = []
78
80
  @stop_words = []
79
81
  @stop_on_unknown = false
82
+ @help_formatter = nil
80
83
 
81
84
  #instance_eval(&b) if b # can't take arguments
82
85
  cloaker(&b).bind(self).call(*a) if b
@@ -238,6 +241,14 @@ class Parser
238
241
  def banner s; @order << [:text, s] end
239
242
  alias :text :banner
240
243
 
244
+ def help_formatter(formatter)
245
+ @help_formatter = formatter
246
+ end
247
+
248
+ def current_help_formatter
249
+ @help_formatter
250
+ end
251
+
241
252
  ## Marks two (or more!) options as requiring each other. Only handles
242
253
  ## undirected (i.e., mutual) dependencies. Directed dependencies are
243
254
  ## better modeled with Trollop::die.
@@ -518,7 +529,7 @@ class Parser
518
529
  $stderr.puts "Error: #{arg}."
519
530
  end
520
531
  $stderr.puts "Try --help for help."
521
- exit(-1)
532
+ exit(1)
522
533
  end
523
534
 
524
535
  private
@@ -735,15 +746,15 @@ end
735
746
  ##
736
747
  ## Requires passing in the parser object.
737
748
 
738
- def with_standard_exception_handling parser
749
+ def with_standard_exception_handling(parser)
739
750
  begin
740
751
  yield
741
752
  rescue CommandlineError => e
742
753
  $stderr.puts "Error: #{e.message}."
743
754
  $stderr.puts "Try --help for help."
744
- exit(-1)
755
+ exit(1)
745
756
  rescue HelpNeeded
746
- parser.educate
757
+ parser.current_help_formatter ? parser.current_help_formatter.print(parser) : parser.educate
747
758
  exit
748
759
  rescue VersionNeeded
749
760
  puts parser.version
@@ -0,0 +1,21 @@
1
+ require 'shellwords'
2
+
3
+ module Escort
4
+ class Utils
5
+ class << self
6
+ def symbolize_keys(hash)
7
+ hash.inject({}) do |result, (key, value)|
8
+ new_key = (key.kind_of?(String) ? key.to_sym : key)
9
+ new_value = (value.kind_of?(Hash) ? symbolize_keys(value) : value)
10
+ result[new_key] = new_value
11
+ result
12
+ end
13
+ end
14
+
15
+ def tokenize_option_string(option_string)
16
+ option_string ||= ''
17
+ Shellwords.shellwords(option_string)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,42 @@
1
+ module Escort
2
+ class Validator
3
+ class << self
4
+ def for(parser)
5
+ self.new(parser)
6
+ end
7
+ end
8
+
9
+ attr_reader :parser
10
+
11
+ def initialize(parser)
12
+ @parser = parser
13
+ end
14
+
15
+ def validate(options, validations)
16
+ validations.each do |option, validations_array|
17
+ if option_exists?(option)
18
+ validate_option(option, options, validations_array)
19
+ else
20
+ raise Escort::ClientError.new("Unable to create validation for '#{option}' as no such option was defined, maybe you misspelled it")
21
+ end
22
+ end
23
+ options
24
+ end
25
+
26
+ private
27
+
28
+ def option_exists?(option)
29
+ parser.specs.keys.include?(option)
30
+ end
31
+
32
+ def validate_option(option, options, validations_array)
33
+ validations_array.each do |validation_data|
34
+ parser.die(option, validation_data[:desc]) if invalid?(option, options, validation_data)
35
+ end
36
+ end
37
+
38
+ def invalid?(option, options, validation_data)
39
+ options[option] && !validation_data[:block].call(options[option])
40
+ end
41
+ end
42
+ end
@@ -1,3 +1,3 @@
1
1
  module Escort
2
- VERSION = '0.0.1'
2
+ VERSION = '0.1.0'
3
3
  end
@@ -0,0 +1,21 @@
1
+ RSpec::Matchers.define :execute_action do |command_name, result|
2
+ match do |block|
3
+ begin
4
+ block.call
5
+ rescue SystemExit => e
6
+ end
7
+ result.first == command_name
8
+ end
9
+
10
+ failure_message_for_should do |block|
11
+ "'#{command_name}' action should have been executed as a result of block, but instead '#{result.first}' was executed"
12
+ end
13
+
14
+ failure_message_for_should_not do |block|
15
+ "'#{command_name}' action should not have been executed as a result of block"
16
+ end
17
+
18
+ description do
19
+ "'#{command_name}' action should have been executed as a result of block"
20
+ end
21
+ end
@@ -0,0 +1,21 @@
1
+ RSpec::Matchers.define :exit_with_code do |exp_code|
2
+ actual = nil
3
+ match do |block|
4
+ begin
5
+ block.call
6
+ rescue SystemExit => e
7
+ actual = e.status
8
+ end
9
+ actual and actual == exp_code
10
+ end
11
+ failure_message_for_should do |block|
12
+ "expected block to call exit(#{exp_code}) but exit" +
13
+ (actual.nil? ? " not called" : "(#{actual}) was called")
14
+ end
15
+ failure_message_for_should_not do |block|
16
+ "expected block not to call exit(#{exp_code})"
17
+ end
18
+ description do
19
+ "expect block to call exit(#{exp_code})"
20
+ end
21
+ end
@@ -0,0 +1,22 @@
1
+ RSpec::Matchers.define :give_option_to_action_with_value do |option_name, expected_value, result|
2
+ match do |block|
3
+ begin
4
+ block.call
5
+ rescue SystemExit => e
6
+ end
7
+ options = result[1] || {}
8
+ options.keys.include?(option_name) && options[option_name] == expected_value
9
+ end
10
+
11
+ failure_message_for_should do |block|
12
+ "'#{option_name}' should have been passed to action with value '#{expected_value}', but instead we got '#{(result[1] || {})[option_name]}'"
13
+ end
14
+
15
+ failure_message_for_should_not do |block|
16
+ "'#{option_name}' should not have been passed to action with value '#{expected_value}'"
17
+ end
18
+
19
+ description do
20
+ "'#{option_name}' should have been passed to action with value '#{expected_value}'"
21
+ end
22
+ end
@@ -0,0 +1,53 @@
1
+ describe "Escort basic app with options defined" do
2
+ subject { Escort::App.create(option_string, &app_configuration) }
3
+ let(:result) { [] }
4
+
5
+ before do
6
+ $stderr = StringIO.new
7
+ $stdout = StringIO.new
8
+ end
9
+
10
+ after do
11
+ $stderr = STDERR
12
+ $stdout = STDOUT
13
+ end
14
+
15
+ let(:app_configuration) do
16
+ ->(app) do
17
+ app.options do |opts|
18
+ opts.opt :option1, "Option1", :short => '-o', :long => '--option1', :type => :string, :default => "option 1"
19
+ end
20
+
21
+ app.action do |options, arguments|
22
+ result << :global
23
+ result << {:option1 => options[:global][:options][:option1]}
24
+ end
25
+ end
26
+ end
27
+
28
+ context "when called with empty option string" do
29
+ let(:option_string) { "" }
30
+ it("should exit with code 0") { expect{ subject }.to exit_with_code(0) }
31
+ it("should execute the global action") { expect{subject}.to execute_action(:global, result) }
32
+ it("option1 have its default value in action") { expect{subject}.to give_option_to_action_with_value(:option1, 'option 1', result) }
33
+ end
34
+
35
+ context "when called with option string with short code option" do
36
+ let(:option_string) { "-o blah" }
37
+ it("should exit with code 0") { expect{ subject }.to exit_with_code(0) }
38
+ it("should execute the global action") { expect{subject}.to execute_action(:global, result) }
39
+ it("option1 have the value 'blah' in action") { expect{subject}.to give_option_to_action_with_value(:option1, 'blah', result) }
40
+ end
41
+
42
+ context "when called with option string with long code option" do
43
+ let(:option_string) { "--option1=blah" }
44
+ it("should exit with code 0") { expect{ subject }.to exit_with_code(0) }
45
+ it("should execute the global action") { expect{subject}.to execute_action(:global, result) }
46
+ it("option1 have the value 'blah' in action") { expect{subject}.to give_option_to_action_with_value(:option1, 'blah', result) }
47
+ end
48
+
49
+ context "when called with option string with unknown option" do
50
+ let(:option_string) { "-k" }
51
+ it("should not exit with code 0") { expect{ subject }.not_to exit_with_code(0) }
52
+ end
53
+ end