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,49 @@
1
+ require 'open3'
2
+
3
+ module Convoy
4
+ module Formatter
5
+ class ShellCommandExecutor
6
+ attr_reader :command
7
+
8
+ def initialize(command)
9
+ @command = command
10
+ end
11
+
12
+ def execute_in_current_shell(success_callback = nil, error_callback = nil)
13
+ begin
14
+ result = `#{command}`
15
+ process_status = $?
16
+ raise Convoy::InternalError.new("Shell command exited with a non-zero (#{process_status.exitstatus}) exit code") if process_status.exitstatus != 0
17
+ success_callback.call(command, result) if success_callback
18
+ rescue => e
19
+ error_callback.call(command, e) if error_callback
20
+ nil
21
+ end
22
+ end
23
+
24
+ #def execute_in_new_shell(success_callback = nil, error_callback = nil)
25
+ #stdin, stdout, stderr = nil, nil, nil
26
+ #begin
27
+ #stdin, stdout, stderr, thread = ensure_successful_exit do
28
+ #Open3.popen3(command)
29
+ #end
30
+ #success_callback.call(command, stdin, stdout, stderr) if success_callback
31
+ #rescue => e
32
+ #error_callback.call(command, e) if error_callback
33
+ #nil
34
+ #ensure
35
+ #[stdin, stdout, stderr].each {|io| io.close if io}
36
+ #end
37
+ #end
38
+
39
+ #private
40
+
41
+ #def ensure_successful_exit(&block)
42
+ #stdin, stdout, stderr, thread = block.call
43
+ #process_status = thread.value
44
+ #raise Convoy::InternalError.new("Shell command exited with a non-zero (#{process_status.exitstatus}) exit code") if process_status.exitstatus != 0
45
+ #[stdin, stdout, stderr, thread]
46
+ #end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,88 @@
1
+ module Convoy
2
+ module Formatter
3
+ class StreamOutputFormatter
4
+ DEFAULT_OUTPUT_WIDTH = 80
5
+ DEFAULT_INDENT_STRING = ' '
6
+ DEFAULT_INDENT = 0
7
+
8
+ attr_reader :stream, :indent_string, :current_indent, :max_output_width, :cursor_position
9
+
10
+ def initialize(stream = $stdout, options = {}, &block)
11
+ @stream = stream
12
+ @max_output_width = options[:max_output_width] || DEFAULT_OUTPUT_WIDTH
13
+ @indent_string = options[:indent_string] || DEFAULT_INDENT_STRING
14
+ @current_indent = options[:current_indent] || DEFAULT_INDENT
15
+ @cursor_position = CursorPosition.new(@max_output_width)
16
+ block.call(self) if block_given?
17
+ end
18
+
19
+ def print(string)
20
+ splitter_input = pad_string_to_account_for_cursor_position(string.to_s)
21
+ segments = StringSplitter.new(max_output_width).split(splitter_input)
22
+ segments = remove_padding_that_account_for_cursor_position(segments)
23
+ segments.each do |segment|
24
+ output_string = "#{current_indent_string}#{segment}"
25
+ output_string = segment unless cursor_position.newline?
26
+ stream.print output_string
27
+ cursor_position.update_for(segment)
28
+ newline if segments.last != segment
29
+ end
30
+ end
31
+
32
+ def puts(string, options = {:newlines => 1})
33
+ print(string)
34
+ newline(options[:newlines])
35
+ end
36
+
37
+ def newline(newline_count = 1)
38
+ stream.print("\n" * newline_count)
39
+ cursor_position.reset if newline_count > 0
40
+ end
41
+
42
+ def indent(count, &block)
43
+ newline unless cursor_position.newline?
44
+ new_indent = current_indent + count
45
+ self.class.new(stream, :max_output_width => max_output_width - count, :indent_string => indent_string, :current_indent => new_indent, &block)
46
+ end
47
+
48
+ def grid(options = {}, &block)
49
+ if block_given?
50
+ options[:width] ||= max_output_width
51
+ grid = StringGrid.new(options, &block)
52
+ puts grid.to_s
53
+ end
54
+ end
55
+
56
+ private
57
+
58
+ def pad_string_to_account_for_cursor_position(string)
59
+ "#{padding_string}#{string}"
60
+ end
61
+
62
+ def remove_padding_that_account_for_cursor_position(segments)
63
+ first_string = segments.first
64
+ if first_string
65
+ segments[0] = first_string.sub(/#{padding_string}/, '')
66
+ end
67
+ segments
68
+ end
69
+
70
+ def padding_string
71
+ "." * cursor_position.position
72
+ end
73
+
74
+ def current_indent_width
75
+ current_indent_string.length
76
+ end
77
+
78
+ def current_indent_string
79
+ indent_string * current_indent
80
+ end
81
+
82
+ #def table(options = {}, &block)
83
+ #BorderlessTable.new(self, options).output(&block)
84
+ #newline(options[:newlines] || 1)
85
+ #end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,108 @@
1
+ module Convoy
2
+ module Formatter
3
+ class StringGrid
4
+ DEFAULT_WIDTH = 80
5
+
6
+ attr_reader :column_count, :width
7
+ attr_accessor :rows
8
+
9
+ def initialize(options = {}, &block)
10
+ @width = options[:width] || DEFAULT_WIDTH
11
+ @column_count = options[:columns] || 3
12
+ @rows = []
13
+ block.call(self) if block_given?
14
+ end
15
+
16
+ def row(*column_values)
17
+ while column_values.size < @column_count
18
+ column_values << ''
19
+ end
20
+ rows << column_values.map(&:to_s)
21
+ end
22
+
23
+ def to_s
24
+ buffer = []
25
+ rows.each do |cells|
26
+ virtual_row = normalize_virtual_row(virtual_row_for(cells))
27
+ physical_row_count_for(virtual_row).times do |physical_count|
28
+ physical_row = format_physical_row_values(physical_row_for(virtual_row, physical_count))
29
+ buffer << physical_row.join("").chomp
30
+ end
31
+ end
32
+ buffer.join("\n")
33
+ end
34
+
35
+ private
36
+
37
+ def format_physical_row_values(physical_row)
38
+ physical_row.each_with_index.map do |value, index|
39
+ cell_value(value, index)
40
+ end
41
+ end
42
+
43
+ def physical_row_for(virtual_row, index)
44
+ virtual_row.map { |physical| physical[index] }
45
+ end
46
+
47
+ def virtual_row_for(column_values)
48
+ virtual_row = []
49
+ column_values.each_with_index do |cell, index|
50
+ virtual_row << Convoy::Formatter::StringSplitter.new(column_width(index) - 1).split(cell)
51
+ end
52
+ normalize_virtual_row(virtual_row)
53
+ end
54
+
55
+ def normalize_virtual_row(virtual_row)
56
+ virtual_row.map do |physical|
57
+ while physical.size < physical_row_count_for(virtual_row)
58
+ physical << ""
59
+ end
60
+ physical
61
+ end
62
+ end
63
+
64
+ def physical_row_count_for(virtual_row)
65
+ virtual_row.map { |physical| physical.size }.max
66
+ end
67
+
68
+ def column_width(column_index)
69
+ width = fair_column_width(column_index)
70
+ if column_index == column_count - 1
71
+ width = last_column_width
72
+ end
73
+ width
74
+ end
75
+
76
+ def fair_column_width(index)
77
+ width = values_in_column(index).map(&:length).max
78
+ width = width + 1
79
+ width > max_column_width ? max_column_width : width
80
+ end
81
+
82
+ def last_column_width
83
+ full_fair_column_width = max_column_width * column_count + max_column_width_remainder
84
+ all_but_last_fair_column_width = 0
85
+ (column_count - 1).times do |index|
86
+ all_but_last_fair_column_width += fair_column_width(index)
87
+ end
88
+ full_fair_column_width - all_but_last_fair_column_width
89
+ end
90
+
91
+ def values_in_column(column_index)
92
+ rows.map { |cells| cells[column_index] }
93
+ end
94
+
95
+ def max_column_width
96
+ width/column_count
97
+ end
98
+
99
+ def max_column_width_remainder
100
+ width%column_count
101
+ end
102
+
103
+ def cell_value(value, column_index)
104
+ sprintf("%-#{column_width(column_index)}s", value)
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,50 @@
1
+ module Convoy
2
+ module Formatter
3
+ class StringSplitter
4
+ attr_reader :max_segment_width #, :first_segment_max_length
5
+
6
+ def initialize(max_segment_width, options = {})
7
+ @max_segment_width = max_segment_width
8
+ #@first_segment_max_length = options[:first_segment_max_length] || max_segment_width
9
+ end
10
+
11
+ def split(input_string)
12
+ input_strings = input_string.split("\n")
13
+ [split_strings(input_strings)].flatten
14
+ #first_string = strings.shift
15
+ #other_strings = strings
16
+ #result = [split_first_string(first_string) + split_strings(other_strings)].flatten
17
+ #result
18
+ end
19
+
20
+ private
21
+
22
+ #def split_first_string(string)
23
+ #if first_segment_max_length >= string.length
24
+ #split_string(string)
25
+ #else
26
+ #first = string.slice(0, first_segment_max_length)
27
+ #last = string.slice(first_segment_max_length..-1)
28
+ #split_strings([first, last])
29
+ #end
30
+ #end
31
+
32
+ def split_strings(strings)
33
+ strings.map { |s| split_string(s) }
34
+ end
35
+
36
+ def split_string(string)
37
+ result = []
38
+ if string.length > max_segment_width
39
+ first_part = string.slice(0, max_segment_width)
40
+ second_part = string.slice(max_segment_width..-1)
41
+ result << first_part
42
+ result << split_string(second_part)
43
+ else
44
+ result << string
45
+ end
46
+ result
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,30 @@
1
+ module Convoy
2
+ module Formatter
3
+ class Terminal
4
+ DEFAULT_WIDTH = 80
5
+
6
+ class << self
7
+ def width
8
+ tput_width
9
+ end
10
+
11
+ private
12
+
13
+ def tput_width
14
+ ShellCommandExecutor.new('/usr/bin/env tput cols').execute_in_current_shell(tput_cols_command_success_callback, tput_cols_command_error_callback) || DEFAULT_WIDTH
15
+ end
16
+
17
+ def tput_cols_command_success_callback
18
+ lambda { |command, result| result.to_i }
19
+ end
20
+
21
+ def tput_cols_command_error_callback
22
+ lambda do |command, e|
23
+ error_logger.debug { e }
24
+ error_logger.info { "Unable to find terminal width via '#{command}', using default of #{DEFAULT_WIDTH}" }
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,43 @@
1
+ module Convoy
2
+ class GlobalPreParser
3
+ attr_reader :setup
4
+
5
+ def initialize(setup)
6
+ @setup = setup
7
+ end
8
+
9
+ def parse(cli_options)
10
+ AutoOptions.new(parse_global_options(cli_options))
11
+ end
12
+
13
+ private
14
+
15
+ def parse_global_options(cli_options, context = [])
16
+ stop_words = setup.command_names_for(context).map(&:to_s)
17
+ parser = init_parser(stop_words)
18
+ parser = add_setup_options_to(parser, context)
19
+ parser.version(setup.version) #set the version if it was provided
20
+ parser.help_formatter(Convoy::Formatter::DefaultHelpFormatter.new(setup, context))
21
+ parsed_options = parse_options_string(parser, cli_options)
22
+ end
23
+
24
+ def init_parser(stop_words)
25
+ Trollop::Parser.new.tap do |parser|
26
+ parser.stop_on(stop_words)
27
+ end
28
+ end
29
+
30
+ def add_setup_options_to(parser, context = [])
31
+ setup.options_for(context).each do |name, opts|
32
+ parser.opt name, opts[:desc] || "", opts
33
+ end
34
+ parser
35
+ end
36
+
37
+ def parse_options_string(parser, cli_options)
38
+ Trollop::with_standard_exception_handling(parser) do
39
+ parser.parse(cli_options)
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,75 @@
1
+ require 'logger'
2
+
3
+ module Convoy
4
+ class Logger
5
+ class << self
6
+ def close
7
+ error.close
8
+ output.close
9
+ end
10
+
11
+ def error
12
+ @error_logger ||= ::Logger.new($stderr).tap do |l|
13
+ #l.formatter = advanced_error_formatter
14
+ l.formatter = basic_error_formatter
15
+ l.sev_threshold = ::Logger::WARN
16
+ end
17
+ end
18
+
19
+ def output
20
+ @output_logger ||= ::Logger.new($stdout).tap do |l|
21
+ l.formatter = output_formatter
22
+ l.sev_threshold = ::Logger::DEBUG
23
+ l.instance_eval do
24
+ def puts(message = nil, &block)
25
+ if block_given?
26
+ fatal(&block)
27
+ else
28
+ fatal(message || "")
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+
35
+ def setup_error_logger(auto_options)
36
+ error.formatter = send(:"#{auto_options.error_formatter}_error_formatter")
37
+ error.sev_threshold = ::Logger.const_get(auto_options.verbosity)
38
+ end
39
+
40
+ private
41
+
42
+ def basic_error_formatter
43
+ proc do |severity, datetime, progname, msg|
44
+ "#{msg2str(msg)}\n"
45
+ end
46
+ end
47
+
48
+ #"#{severity} [#{datetime.strftime("%d/%b/%Y %H:%M:%S")}] \"#{msg}\"\n"
49
+ def advanced_error_formatter
50
+ proc do |severity, datetime, progname, msg|
51
+ sprintf("%-8s #{msg2str(msg, 10)}\n", severity)
52
+ end
53
+ end
54
+
55
+ def output_formatter
56
+ proc do |severity, datetime, progname, msg|
57
+ "#{msg}\n"
58
+ end
59
+ end
60
+
61
+ def msg2str(msg, backtrace_indent = 0)
62
+ case msg
63
+ when ::String
64
+ msg
65
+ when ::Exception
66
+ "#{msg.message} (#{ msg.class })\n" <<
67
+ (msg.backtrace || []).map { |line| sprintf("%#{backtrace_indent}s#{line}", "") }.join("\n")
68
+ else
69
+ msg.inspect
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
75
+