convoy 1.0.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 (109) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +20 -0
  3. data/.irbrc +3 -0
  4. data/.rspec +3 -0
  5. data/.ruby-version +1 -0
  6. data/.travis.yml +8 -0
  7. data/Gemfile +4 -0
  8. data/LICENSE +22 -0
  9. data/README.md +705 -0
  10. data/Rakefile +1 -0
  11. data/convoy.gemspec +24 -0
  12. data/examples/.my_apprc +24 -0
  13. data/examples/basic +10 -0
  14. data/examples/basic_config_file +16 -0
  15. data/examples/basic_conflicts +17 -0
  16. data/examples/basic_depends_on +25 -0
  17. data/examples/basic_flags +15 -0
  18. data/examples/basic_options +14 -0
  19. data/examples/basic_options_multi +15 -0
  20. data/examples/basic_require_arguments +17 -0
  21. data/examples/basic_texts +21 -0
  22. data/examples/basic_validations +21 -0
  23. data/examples/basic_with_everything +30 -0
  24. data/examples/commands/example_command.rb +13 -0
  25. data/examples/suite_complex +65 -0
  26. data/examples/suite_simple +19 -0
  27. data/examples/suite_with_sub_commands +94 -0
  28. data/lib/convoy.rb +83 -0
  29. data/lib/convoy/action_command/base.rb +85 -0
  30. data/lib/convoy/action_command/escort_utility_command.rb +53 -0
  31. data/lib/convoy/app.rb +127 -0
  32. data/lib/convoy/arguments.rb +20 -0
  33. data/lib/convoy/auto_options.rb +71 -0
  34. data/lib/convoy/error/error.rb +33 -0
  35. data/lib/convoy/formatter/command.rb +87 -0
  36. data/lib/convoy/formatter/commands.rb +37 -0
  37. data/lib/convoy/formatter/cursor_position.rb +29 -0
  38. data/lib/convoy/formatter/default_help_formatter.rb +117 -0
  39. data/lib/convoy/formatter/global_command.rb +17 -0
  40. data/lib/convoy/formatter/option.rb +152 -0
  41. data/lib/convoy/formatter/options.rb +28 -0
  42. data/lib/convoy/formatter/shell_command_executor.rb +49 -0
  43. data/lib/convoy/formatter/stream_output_formatter.rb +88 -0
  44. data/lib/convoy/formatter/string_grid.rb +108 -0
  45. data/lib/convoy/formatter/string_splitter.rb +50 -0
  46. data/lib/convoy/formatter/terminal.rb +30 -0
  47. data/lib/convoy/global_pre_parser.rb +43 -0
  48. data/lib/convoy/logger.rb +75 -0
  49. data/lib/convoy/option_dependency_validator.rb +82 -0
  50. data/lib/convoy/option_parser.rb +155 -0
  51. data/lib/convoy/setup/configuration/generator.rb +75 -0
  52. data/lib/convoy/setup/configuration/instance.rb +34 -0
  53. data/lib/convoy/setup/configuration/loader.rb +43 -0
  54. data/lib/convoy/setup/configuration/locator/base.rb +19 -0
  55. data/lib/convoy/setup/configuration/locator/chaining.rb +29 -0
  56. data/lib/convoy/setup/configuration/locator/descending_to_home.rb +23 -0
  57. data/lib/convoy/setup/configuration/locator/executing_script_directory.rb +15 -0
  58. data/lib/convoy/setup/configuration/locator/specified_directory.rb +21 -0
  59. data/lib/convoy/setup/configuration/merge_tool.rb +38 -0
  60. data/lib/convoy/setup/configuration/reader.rb +36 -0
  61. data/lib/convoy/setup/configuration/writer.rb +46 -0
  62. data/lib/convoy/setup/dsl/action.rb +17 -0
  63. data/lib/convoy/setup/dsl/command.rb +67 -0
  64. data/lib/convoy/setup/dsl/config_file.rb +13 -0
  65. data/lib/convoy/setup/dsl/global.rb +29 -0
  66. data/lib/convoy/setup/dsl/options.rb +81 -0
  67. data/lib/convoy/setup_accessor.rb +206 -0
  68. data/lib/convoy/trollop.rb +861 -0
  69. data/lib/convoy/utils.rb +21 -0
  70. data/lib/convoy/validator.rb +45 -0
  71. data/spec/integration/basic_config_file_spec.rb +126 -0
  72. data/spec/integration/basic_conflicts_spec.rb +47 -0
  73. data/spec/integration/basic_depends_on_spec.rb +275 -0
  74. data/spec/integration/basic_options_spec.rb +41 -0
  75. data/spec/integration/basic_options_with_multi_spec.rb +30 -0
  76. data/spec/integration/basic_spec.rb +38 -0
  77. data/spec/integration/basic_validations_spec.rb +77 -0
  78. data/spec/integration/basic_with_arguments_spec.rb +35 -0
  79. data/spec/integration/basic_with_text_fields_spec.rb +21 -0
  80. data/spec/integration/suite_simple_spec.rb +45 -0
  81. data/spec/integration/suite_sub_command_spec.rb +51 -0
  82. data/spec/lib/convoy/action_command/base_spec.rb +200 -0
  83. data/spec/lib/convoy/formatter/command_spec.rb +238 -0
  84. data/spec/lib/convoy/formatter/global_command_spec.rb +50 -0
  85. data/spec/lib/convoy/formatter/option_spec.rb +300 -0
  86. data/spec/lib/convoy/formatter/shell_command_executor_spec.rb +59 -0
  87. data/spec/lib/convoy/formatter/stream_output_formatter_spec.rb +214 -0
  88. data/spec/lib/convoy/formatter/string_grid_spec.rb +59 -0
  89. data/spec/lib/convoy/formatter/string_splitter_spec.rb +50 -0
  90. data/spec/lib/convoy/formatter/terminal_spec.rb +19 -0
  91. data/spec/lib/convoy/setup/configuration/generator_spec.rb +101 -0
  92. data/spec/lib/convoy/setup/configuration/loader_spec.rb +79 -0
  93. data/spec/lib/convoy/setup/configuration/locator/chaining_spec.rb +81 -0
  94. data/spec/lib/convoy/setup/configuration/locator/descending_to_home_spec.rb +57 -0
  95. data/spec/lib/convoy/setup/configuration/locator/executing_script_directory_spec.rb +29 -0
  96. data/spec/lib/convoy/setup/configuration/locator/specified_directory_spec.rb +33 -0
  97. data/spec/lib/convoy/setup/configuration/merge_tool_spec.rb +41 -0
  98. data/spec/lib/convoy/setup/configuration/reader_spec.rb +41 -0
  99. data/spec/lib/convoy/setup/configuration/writer_spec.rb +75 -0
  100. data/spec/lib/convoy/setup_accessor_spec.rb +226 -0
  101. data/spec/lib/convoy/utils_spec.rb +30 -0
  102. data/spec/spec_helper.rb +29 -0
  103. data/spec/support/integration_helpers.rb +2 -0
  104. data/spec/support/matchers/execute_action_for_command_matcher.rb +21 -0
  105. data/spec/support/matchers/execute_action_with_arguments_matcher.rb +25 -0
  106. data/spec/support/matchers/execute_action_with_options_matcher.rb +29 -0
  107. data/spec/support/matchers/exit_with_code_matcher.rb +29 -0
  108. data/spec/support/shared_contexts/integration_setup.rb +34 -0
  109. metadata +292 -0
@@ -0,0 +1,82 @@
1
+ module Convoy
2
+ class OptionDependencyValidator
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, dependencies)
16
+ dependencies.each_pair do |option_name, dependency_rules|
17
+ ensure_dependencies_satisfied_for(option_name, dependency_rules, options)
18
+ end
19
+ options
20
+ end
21
+
22
+ private
23
+
24
+ def option_exists?(option)
25
+ parser.specs.keys.include?(option)
26
+ end
27
+
28
+ def ensure_dependencies_satisfied_for(option_name, dependency_rules, options)
29
+ ensure_dependency_for_valid_option(option_name)
30
+ dependency_rules.each do |rule|
31
+ case rule
32
+ when Hash
33
+ handle_all_option_value_dependency_rules(option_name, rule, options)
34
+ else
35
+ ensure_option_depends_on_valid_option(option_name, rule)
36
+ handle_possible_presence_dependency_issue(option_name, rule, options)
37
+ end
38
+ end
39
+ end
40
+
41
+ def handle_all_option_value_dependency_rules(option_name, rule, options)
42
+ if option_was_specified?(option_name, options)
43
+ rule.each_pair do |rule_option, rule_option_value|
44
+ ensure_option_depends_on_valid_option(option_name, rule_option)
45
+ handle_possible_option_value_dependency_issue(option_name, rule_option, rule_option_value, options)
46
+ end
47
+ end
48
+ end
49
+
50
+ def handle_possible_option_value_dependency_issue(option_name, rule_option, rule_option_value, options)
51
+ unless options[rule_option] == rule_option_value
52
+ raise Convoy::UserError.new("Option dependency unsatisfied, '#{option_name}' depends on '#{rule_option}' having value '#{rule_option_value}', '#{option_name}' specified with value '#{options[option_name]}', but '#{rule_option}' is '#{options[rule_option]}'")
53
+ end
54
+ end
55
+
56
+ def handle_possible_presence_dependency_issue(option_name, rule, options)
57
+ if option_was_specified?(option_name, options) && option_was_unspecified?(rule, options)
58
+ raise Convoy::UserError.new("Option dependency unsatisfied, '#{option_name}' depends on '#{rule}', '#{option_name}' specified with value '#{options[option_name]}', but '#{rule}' is unspecified")
59
+ end
60
+ end
61
+
62
+ def option_was_unspecified?(option_name, options)
63
+ !option_was_specified?(option_name, options)
64
+ end
65
+
66
+ def option_was_specified?(option_name, options)
67
+ !options[option_name].nil? && !(options[option_name] == []) && !(options[option_name] == false)
68
+ end
69
+
70
+ def ensure_dependency_for_valid_option(option_name)
71
+ unless option_exists?(option_name)
72
+ raise Convoy::ClientError.new("Dependency specified for option '#{option_name}', but no such option was defined, perhaps you misspelled it")
73
+ end
74
+ end
75
+
76
+ def ensure_option_depends_on_valid_option(option_name, rule)
77
+ unless option_exists?(rule)
78
+ raise Convoy::ClientError.new("'#{option_name}' is set up to depend on '#{rule}', but '#{rule}' does not appear to be a valid option, perhaps it is a spelling error")
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,155 @@
1
+ module Convoy
2
+ class OptionParser
3
+ attr_reader :setup, :configuration
4
+
5
+ def initialize(configuration, setup)
6
+ @configuration = configuration
7
+ @setup = setup
8
+ end
9
+
10
+ def parse(cli_options)
11
+ options = init_invoked_options_hash
12
+ parse_global_options(cli_options, options[:global][:options])
13
+ parse_command_options(cli_options, [], options[:global][:commands])
14
+
15
+ [options, arguments_from(cli_options)]
16
+ end
17
+
18
+ private
19
+
20
+ def init_invoked_options_hash
21
+ {
22
+ :global => {
23
+ :options => {},
24
+ :commands => {}
25
+ }
26
+ }
27
+ end
28
+
29
+ def parse_global_options(cli_options, options)
30
+ context = []
31
+ options.merge!(parse_options(cli_options, context))
32
+ end
33
+
34
+ def parse_command_options(cli_options, context, options)
35
+ unless cli_option_is_a_command?(cli_options, context)
36
+ options
37
+ else
38
+ command = command_name_from(cli_options)
39
+ context << command
40
+ current_options = parse_options(cli_options, context)
41
+ options[command] = {}
42
+ options[command][:options] = current_options
43
+ options[command][:commands] = {}
44
+ parse_command_options(cli_options, context, options[command][:commands])
45
+ end
46
+ end
47
+
48
+ def parse_options(cli_options, context = [])
49
+ stop_words = setup.command_names_for(context).map(&:to_s)
50
+ parser = init_parser(stop_words)
51
+ parser = add_setup_options_to(parser, context)
52
+ parser = add_option_conflicts_to(parser, context)
53
+ parser = default_option_values_from_config_for(parser, context)
54
+ parser.version(setup.version) #set the version if it was provided
55
+ parser.help_formatter(Convoy::Formatter::DefaultHelpFormatter.new(setup, context))
56
+ parsed_options = parse_options_string(parser, cli_options)
57
+ Convoy::OptionDependencyValidator.for(parser).validate(parsed_options, setup.dependencies_for(context))
58
+ Convoy::Validator.for(parser).validate(parsed_options, setup.validations_for(context))
59
+ end
60
+
61
+ def add_setup_options_to(parser, context = [])
62
+ setup.options_for(context).each do |name, opts|
63
+ parser.opt name, opts[:desc] || "", opts.dup #have to make sure to dup here, otherwise opts might get stuff added to it and it
64
+ #may cause problems later, e.g. adds default value and when parsed again trollop barfs
65
+ end
66
+ parser
67
+ end
68
+
69
+ def add_option_conflicts_to(parser, context = [])
70
+ conflicting_options_for_context = setup.conflicting_options_for(context)
71
+ conflicting_options_for_context.keys.each do |option_name|
72
+ conflict_list = [option_name] + conflicting_options_for_context[option_name]
73
+ conflict_list.each do |option|
74
+ raise Convoy::ClientError.new("Conflict defined with option '#{option}', but this option does not exist, perhaps it is a spelling error.") unless option_exists?(parser, option)
75
+ end
76
+ parser.conflicts *conflict_list
77
+ end
78
+ parser
79
+ end
80
+
81
+ def option_exists?(parser, option)
82
+ parser.specs.keys.include?(option)
83
+ end
84
+
85
+ def default_option_values_from_config_for(parser, context)
86
+ unless configuration.empty?
87
+ parser.specs.each do |sym, opts|
88
+ if config_has_value_for_context?(sym, context)
89
+ default = config_value_for_context(sym, context)
90
+ opts[:default] = default
91
+ if opts[:multi] && default.nil?
92
+ opts[:default] = [] # multi arguments default to [], not nil
93
+ elsif opts[:multi] && !default.kind_of?(Array)
94
+ opts[:default] = [default]
95
+ else
96
+ opts[:default] = default
97
+ end
98
+ end
99
+ end
100
+ end
101
+ parser
102
+ end
103
+
104
+ def config_has_value_for_context?(option, context)
105
+ relevant_config_hash = config_hash_for_context(context)
106
+ relevant_config_hash[:options].include?(option)
107
+ end
108
+
109
+ def config_value_for_context(option, context)
110
+ relevant_config_hash = config_hash_for_context(context)
111
+ relevant_config_hash[:options][option]
112
+ end
113
+
114
+ def config_hash_for_context(context)
115
+ relevant_config_hash = configuration.global
116
+ context.each do |command_name|
117
+ command_name = command_name.to_sym
118
+ relevant_config_hash = relevant_config_hash[:commands][command_name]
119
+ relevant_config_hash = ensure_config_hash_has_options_and_commands(relevant_config_hash)
120
+ end
121
+ relevant_config_hash
122
+ end
123
+
124
+ def ensure_config_hash_has_options_and_commands(relevant_config_hash)
125
+ relevant_config_hash ||= {}
126
+ relevant_config_hash[:commands] ||= {}
127
+ relevant_config_hash[:options] ||= {}
128
+ relevant_config_hash
129
+ end
130
+
131
+ def cli_option_is_a_command?(cli_options, context)
132
+ cli_options.size > 0 && setup.command_names_for(context).include?(cli_options[0].to_sym)
133
+ end
134
+
135
+ def init_parser(stop_words)
136
+ Trollop::Parser.new.tap do |parser|
137
+ parser.stop_on(stop_words) # make sure we halt parsing if we see a command
138
+ end
139
+ end
140
+
141
+ def parse_options_string(parser, cli_options)
142
+ Trollop::with_standard_exception_handling(parser) do
143
+ parser.parse(cli_options)
144
+ end
145
+ end
146
+
147
+ def command_name_from(cli_options)
148
+ cli_options.shift.to_sym
149
+ end
150
+
151
+ def arguments_from(cli_options)
152
+ cli_options
153
+ end
154
+ end
155
+ end
@@ -0,0 +1,75 @@
1
+ module Convoy
2
+ module Setup
3
+ module Configuration
4
+ class Generator
5
+ attr_reader :setup
6
+
7
+ def initialize(setup)
8
+ @setup = setup
9
+ end
10
+
11
+ def default_data
12
+ config_hash = init_config_hash
13
+ options([], config_hash[:global][:options]) #global options
14
+ options_for_commands(setup.canonical_command_names_for([]), [], config_hash[:global][:commands])
15
+ config_hash
16
+ end
17
+
18
+ private
19
+
20
+ def options_for_commands(commands, context, options = {})
21
+ commands.each do |command_name|
22
+ command_name = command_name.to_sym
23
+ #next if command_name == :convoy
24
+ options[command_name] = {}
25
+ options[command_name][:options] = {}
26
+ options[command_name][:commands] = {}
27
+ current_context = context.dup
28
+ current_context << command_name
29
+ options(current_context, options[command_name][:options]) #command_options
30
+ options_for_commands(setup.canonical_command_names_for(current_context), current_context, options[command_name][:commands])
31
+ end
32
+ end
33
+
34
+ def init_config_hash
35
+ {
36
+ :global => {
37
+ :options => {},
38
+ :commands => {}
39
+ },
40
+ :user => {}
41
+ }
42
+ end
43
+
44
+ def options(context = [], options = {})
45
+ command_names = setup.command_names_for(context)
46
+ parser = init_parser(command_names)
47
+ parser = add_setup_options_to(parser, context)
48
+ options.merge!(default_option_values(parser))
49
+ end
50
+
51
+ def init_parser(stop_words)
52
+ Trollop::Parser.new.tap do |parser|
53
+ parser.stop_on(stop_words) # make sure we halt parsing if we see a command
54
+ end
55
+ end
56
+
57
+ def add_setup_options_to(parser, context = [])
58
+ setup.options_for(context).each do |name, opts|
59
+ parser.opt name, opts[:desc] || "", opts.dup #have to make sure to dup here, otherwise opts might get stuff added to it and it
60
+ #may cause problems later, e.g. adds default value and when parsed again trollop barfs
61
+ end
62
+ parser
63
+ end
64
+
65
+ def default_option_values(parser)
66
+ hash = {}
67
+ parser.specs.each_pair do |key, data|
68
+ hash[key] = data[:default] || nil
69
+ end
70
+ hash
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,34 @@
1
+ module Convoy
2
+ module Setup
3
+ module Configuration
4
+ class Instance
5
+ class << self
6
+ def blank
7
+ self.new(nil, {})
8
+ end
9
+ end
10
+
11
+ attr_reader :data, :path
12
+
13
+ def initialize(path, hash)
14
+ @data = hash
15
+ @path = path
16
+ end
17
+
18
+ def blank?
19
+ data.empty?
20
+ end
21
+
22
+ alias empty? blank?
23
+
24
+ def global
25
+ data[:global] || {}
26
+ end
27
+
28
+ def user
29
+ data[:user] || {}
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,43 @@
1
+ module Convoy
2
+ module Setup
3
+ module Configuration
4
+ class Loader
5
+ attr_reader :setup, :auto_options
6
+
7
+ def initialize(setup, auto_options)
8
+ @setup = setup
9
+ @auto_options = auto_options
10
+ end
11
+
12
+ def configuration
13
+ if setup.has_config_file?
14
+ Writer.new(config_path, Generator.new(setup).default_data).write if setup.config_file_autocreatable?
15
+ Reader.new(config_path).read
16
+ else
17
+ Instance.blank
18
+ end
19
+ end
20
+
21
+ def default_config_path
22
+ @default_config_path ||= (config_filename ? File.join(File.expand_path(ENV["HOME"]), config_filename) : nil)
23
+ end
24
+
25
+ private
26
+
27
+ def config_filename
28
+ @config_filename ||= setup.config_file
29
+ end
30
+
31
+ def config_path
32
+ @config_path ||= (auto_options.non_default_config_path || locator.locate || default_config_path)
33
+ end
34
+
35
+ def locator
36
+ Locator::Chaining.new(config_filename).
37
+ add_locator(Locator::ExecutingScriptDirectory.new(config_filename)).
38
+ add_locator(Locator::DescendingToHome.new(config_filename))
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,19 @@
1
+ module Convoy
2
+ module Setup
3
+ module Configuration
4
+ module Locator
5
+ class Base
6
+ attr_reader :filename
7
+
8
+ def initialize(filename)
9
+ @filename = filename
10
+ end
11
+
12
+ def locate
13
+ raise "Must be defined in child class"
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,29 @@
1
+ module Convoy
2
+ module Setup
3
+ module Configuration
4
+ module Locator
5
+ class Chaining < Base
6
+ attr_reader :locators
7
+
8
+ def initialize(filename, locators = [])
9
+ super(filename)
10
+ @locators = locators || []
11
+ end
12
+
13
+ def locate
14
+ locators.each do |locator|
15
+ filepath = locator.locate
16
+ return filepath if filepath
17
+ end
18
+ nil
19
+ end
20
+
21
+ def add_locator(locator)
22
+ @locators << locator
23
+ self
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,23 @@
1
+ require 'pathname'
2
+
3
+ module Convoy
4
+ module Setup
5
+ module Configuration
6
+ module Locator
7
+ class DescendingToHome < Base
8
+ def locate
9
+ return nil unless filename
10
+ possible_configs = []
11
+ Pathname.new(Dir.pwd).descend do |path|
12
+ filepath = File.join(path, filename)
13
+ if File.exists?(filepath)
14
+ possible_configs << filepath
15
+ end
16
+ end
17
+ possible_configs.empty? ? nil : possible_configs.last
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end