escort 0.0.1 → 0.1.0

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 (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