convoy 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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
+