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,79 @@
1
+ module Escort
2
+ module ActionCommand
3
+ class Base
4
+ attr_reader :options, :arguments, :config
5
+
6
+ def initialize(options, arguments, config={})
7
+ @options = options
8
+ @arguments = arguments
9
+ @config = config
10
+ @command_context = nil
11
+ @command_options = nil
12
+ @parent_options = nil
13
+ @grandparent_options = nil
14
+ @global_options = nil
15
+ end
16
+
17
+ protected
18
+
19
+ def command_context
20
+ return @command_context if @command_context
21
+ @command_context = []
22
+ current_command_hash = options[:global][:commands]
23
+ until current_command_hash.keys.empty?
24
+ key = current_command_hash.keys.first
25
+ @command_context << key
26
+ current_command_hash = current_command_hash[key][:commands]
27
+ end
28
+ @command_context
29
+ end
30
+
31
+ def command_options
32
+ @command_options ||= context_hash(command_context)[:options] || {}
33
+ end
34
+
35
+ def global_options
36
+ @global_options ||= options[:global][:options] || {}
37
+ end
38
+
39
+ def parent_options
40
+ @parent_options ||= ensure_parent{ |parent_context| context_hash(parent_context)[:options] || {} }
41
+ end
42
+
43
+ def grandparent_options
44
+ @grandparent_options ||= ensure_grandparent{ |grandparent_context| context_hash(grandparent_context)[:options] || {} }
45
+ end
46
+
47
+ #generation_number 1 is parent, 2 is grandparent and so on
48
+ #default is 3 for great grandparent
49
+ def ancestor_options(generation_number = 3)
50
+ ensure_ancestor(generation_number){ |ancestor_context| context_hash(ancestor_context)[:options] || {} }
51
+ end
52
+
53
+ private
54
+
55
+ def context_hash(context)
56
+ context_hash = options[:global]
57
+ context.each do |command_name|
58
+ context_hash = context_hash[:commands][command_name]
59
+ end
60
+ context_hash
61
+ end
62
+
63
+ def ensure_parent(&block)
64
+ ensure_ancestor(1, &block)
65
+ end
66
+
67
+ def ensure_grandparent(&block)
68
+ ensure_ancestor(2, &block)
69
+ end
70
+
71
+ def ensure_ancestor(generation_number, &block)
72
+ return {} if generation_number < 1
73
+ return {} unless command_context.size > generation_number
74
+ ancestor_context = command_context.dup.slice(0, command_context.size - generation_number)
75
+ block.call(ancestor_context)
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,53 @@
1
+ module Escort
2
+ module ActionCommand
3
+ class EscortUtilityCommand < Base
4
+ attr_reader :setup
5
+
6
+ def initialize(setup, options, arguments, config = {})
7
+ super(options, arguments, config)
8
+ @setup = setup
9
+ end
10
+
11
+ def execute
12
+ current_command_options = command_options
13
+ if current_command_options[:create_config_given]
14
+ create_config(current_command_options[:create_config])
15
+ elsif current_command_options[:create_default_config_given]
16
+ create_default_config
17
+ elsif current_command_options[:update_config_given]
18
+ update_config(current_command_options[:update_config])
19
+ elsif current_command_options[:update_default_config_given]
20
+ update_default_config
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ def create_config(path)
27
+ config_path = absolute_path(path)
28
+ Escort::Setup::Configuration::Writer.new(config_path, Escort::Setup::Configuration::Generator.new(setup).default_data).write
29
+ end
30
+
31
+ def create_default_config
32
+ Escort::Setup::Configuration::Writer.new(default_config_path, Escort::Setup::Configuration::Generator.new(setup).default_data).write
33
+ end
34
+
35
+ def update_config(path)
36
+ config_path = absolute_path(path)
37
+ Escort::Setup::Configuration::Writer.new(config_path, Escort::Setup::Configuration::Generator.new(setup).default_data).update
38
+ end
39
+
40
+ def update_default_config
41
+ Escort::Setup::Configuration::Writer.new(default_config_path, Escort::Setup::Configuration::Generator.new(setup).default_data).update
42
+ end
43
+
44
+ def absolute_path(path)
45
+ File.expand_path(path)
46
+ end
47
+
48
+ def default_config_path
49
+ Escort::Setup::Configuration::Loader.new(setup, nil).default_config_path
50
+ end
51
+ end
52
+ end
53
+ end
@@ -1,58 +1,111 @@
1
1
  module Escort
2
2
  class App
3
- include Dsl
4
- include GlobalDsl
5
-
3
+ #TODO ensure that every command must have an action
6
4
  class << self
7
- def create(options_string = nil, &block)
8
- self.new(options_string).tap do |app|
9
- block.call(app) #run the block to get the various sub blocks
10
- begin
11
- app.parse_options # parse the global options
12
- app.current_command.parse_options # parse the current command options
13
- app.execute_before_block(app.current_command.name, app.current_options, app.current_command.current_options, app.arguments)
14
- app.current_command.perform_action(app.current_options, app.arguments)
15
- rescue => e
16
- app.execute_error_block(e)
17
- end
18
- end
5
+ def create(option_string = '', &block)
6
+ cli_app_configuration = Escort::Setup::Dsl::Global.new(&block)
7
+ setup = Escort::SetupAccessor.new(cli_app_configuration)
8
+ app = self.new(option_string, setup)
9
+ app.execute
10
+ exit(0)
11
+ end
12
+ end
13
+
14
+ attr_reader :setup, :option_string
15
+
16
+ def initialize(option_string, setup)
17
+ @setup = setup
18
+ @option_string = option_string
19
+ end
20
+
21
+ def execute
22
+ begin
23
+ AutoOptions.augment(setup)
24
+
25
+ cli_options = current_cli_options(option_string)
26
+
27
+ auto_options = Escort::GlobalPreParser.new(setup).parse(cli_options.dup)
28
+ Escort::Logger.setup_error_logger(auto_options)
29
+
30
+ #now we can start doing error logging everything above here has to be rock solid
31
+
32
+ configuration = Escort::Setup::Configuration::Loader.new(setup, auto_options).configuration
33
+
34
+ invoked_options, arguments = Escort::OptionParser.new(configuration, setup).parse(cli_options)
35
+ context = context_from_options(invoked_options[:global])
36
+ action = setup.action_for(context)
37
+ actual_arguments = Escort::Arguments.read(arguments, setup.arguments_required_for(context))
38
+ rescue => e
39
+ handle_escort_error(e)
19
40
  end
41
+ execute_action(action, invoked_options, actual_arguments, configuration.user)
20
42
  end
21
43
 
22
- attr_reader :current_options
44
+ private
23
45
 
24
- def initialize(options_string = nil)
25
- @options_string = options_string || ARGV.dup
46
+ def current_cli_options(option_string)
47
+ passed_in_options = Escort::Utils.tokenize_option_string(option_string)
48
+ passed_in_options.empty? ? ARGV.dup : passed_in_options
26
49
  end
27
50
 
28
- def arguments
29
- @options_string
51
+ def execute_action(action, options, arguments, user_config)
52
+ begin
53
+ action.call(options, arguments, user_config)
54
+ rescue => e
55
+ handle_action_error(e)
56
+ end
30
57
  end
31
58
 
32
- def parse_options
33
- parser = Trollop::Parser.new(&@options_block)
34
- parser.stop_on(@command_names)
59
+ def context_from_options(options)
60
+ commands_in_order(options)
61
+ end
35
62
 
36
- @current_options = Trollop::with_standard_exception_handling(parser) do
37
- parser.parse @options_string
63
+ def commands_in_order(options, commands = [])
64
+ if options[:commands].keys.empty?
65
+ commands
66
+ else
67
+ command = options[:commands].keys.first
68
+ commands << command
69
+ commands_in_order(options[:commands][command], commands)
38
70
  end
39
71
  end
40
72
 
41
- def current_command
42
- return @current_command if @current_command
43
- command_name = @options_string.shift.to_s
44
- command_block = @command_blocks[command_name]
45
- @current_command = Command.new(command_name, @options_string)
46
- command_block.call(@current_command)
47
- @current_command
73
+ def handle_escort_error(e)
74
+ if e.kind_of?(Escort::UserError)
75
+ print_escort_error_message(e)
76
+ error_logger.debug{ "Escort app failed to execute successfully, due to user error" }
77
+ exit(Escort::USER_ERROR_EXIT_CODE)
78
+ elsif e.kind_of?(Escort::ClientError)
79
+ print_escort_error_message(e)
80
+ error_logger.debug{ "Escort app failed to execute successfully, due to client setup error" }
81
+ exit(Escort::CLIENT_ERROR_EXIT_CODE)
82
+ else
83
+ print_escort_error_message(e)
84
+ error_logger.debug{ "Escort app failed to execute successfully, due to internal error" }
85
+ exit(Escort::INTERNAL_ERROR_EXIT_CODE)
86
+ end
87
+ end
88
+
89
+ def handle_action_error(e)
90
+ if e.kind_of?(Escort::Error)
91
+ print_escort_error_message(e)
92
+ error_logger.debug{ "Escort app failed to execute successfully, due to internal error" }
93
+ exit(Escort::INTERNAL_ERROR_EXIT_CODE)
94
+ else
95
+ print_stacktrace(e)
96
+ error_logger.debug{ "Escort app failed to execute successfully, due to unknown error" }
97
+ exit(Escort::EXTERNAL_ERROR_EXIT_CODE)
98
+ end
48
99
  end
49
100
 
50
- def execute_before_block(command_name, global_options, command_options, arguments)
51
- @before_block.call(command_name, global_options, command_options, arguments) if @before_block
101
+ def print_stacktrace(e)
102
+ error_logger.error{ e }
52
103
  end
53
104
 
54
- def execute_error_block(error)
55
- @error_block.call(error)
105
+ def print_escort_error_message(e)
106
+ print_stacktrace(e)
107
+ error_logger.warn{ "\n\n" }
108
+ error_logger.warn{ "An internal Escort error has occurred, you should probably report it by creating an issue on github!" }
56
109
  end
57
110
  end
58
111
  end
@@ -0,0 +1,20 @@
1
+ require 'readline'
2
+
3
+ module Escort
4
+ class Arguments
5
+ class << self
6
+ def read(arguments, requires_arguments=false)
7
+ if arguments.empty? && requires_arguments
8
+ while command = Readline.readline("> ", true)
9
+ arguments << command
10
+ end
11
+ arguments = arguments.compact.keep_if{|value| value.length > 0}
12
+ if arguments.empty?
13
+ raise Escort::UserError.new("You must provide some arguments to this script")
14
+ end
15
+ end
16
+ arguments
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,71 @@
1
+ module Escort
2
+ class AutoOptions
3
+ class << self
4
+ def augment(setup)
5
+ if setup.has_config_file?
6
+ setup.add_global_option :config, "Configuration file to use for this execution", :short => :none, :long => '--config', :type => :string
7
+
8
+ setup.add_global_command :escort, :description => "Auto created utility command", :aliases => [] do |command|
9
+ command.requires_arguments false
10
+
11
+ command.options do |opts|
12
+ opts.opt :create_config, "Create configuration file at specified location", :short => :none, :long => '--create-config', :type => :string
13
+ opts.opt :create_default_config, "Create a default configuration file", :short => :none, :long => '--create-default-config', :type => :boolean, :default => false
14
+ opts.opt :update_config, "Update configuration file at specified location", :short => :none, :long => '--update-config', :type => :string
15
+ opts.opt :update_default_config, "Update the default configuration file", :short => :none, :long => '--update-default-config', :type => :boolean, :default => false
16
+ end
17
+
18
+ command.conflicting_options :create_config, :create_default_config, :update_config, :update_default_config
19
+
20
+ command.action do |options, arguments|
21
+ ActionCommand::EscortUtilityCommand.new(setup, options, arguments).execute
22
+ end
23
+ end
24
+ end
25
+ setup.add_global_option :verbosity, "Verbosity level of output for current execution (e.g. INFO, DEBUG)", :short => :none, :long => '--verbosity', :type => :string, :default => "WARN"
26
+
27
+ setup.add_global_option :error_output_format, "The format to use when outputting errors (e.g. basic, advanced)", :short => :none, :long => '--error-output-format', :type => :string, :default => "basic"
28
+ #TODO validations for the output format and the verbosity
29
+ end
30
+ end
31
+
32
+ attr_reader :options
33
+
34
+ def initialize(options)
35
+ @options = options
36
+ end
37
+
38
+ def non_default_config_path
39
+ if options[:config_given] && File.exists?(config_path)
40
+ config_path
41
+ elsif !options[:config_given]
42
+ nil
43
+ else
44
+ error_logger.warn "The given config file '#{options[:config]}' does not exist, falling back to default"
45
+ nil
46
+ end
47
+ end
48
+
49
+ def verbosity
50
+ error_verbosity.upcase
51
+ end
52
+
53
+ def error_formatter
54
+ error_output_format.to_sym
55
+ end
56
+
57
+ private
58
+
59
+ def config_path
60
+ File.expand_path(options[:config])
61
+ end
62
+
63
+ def error_output_format
64
+ options[:error_output_format]
65
+ end
66
+
67
+ def error_verbosity
68
+ options[:verbosity]
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,50 @@
1
+ module Escort
2
+ INTERNAL_ERROR_EXIT_CODE = 1
3
+ CLIENT_ERROR_EXIT_CODE = 2
4
+ USER_ERROR_EXIT_CODE = 3
5
+ EXTERNAL_ERROR_EXIT_CODE = 10
6
+
7
+ #module to tag all exceptions coming out of Escort with
8
+ module Error
9
+ end
10
+
11
+ #all our exceptions will supported nesting other exceptions
12
+ #also all our exception will be a kind_of? Escort::Error
13
+ class BaseError < StandardError
14
+ include Error
15
+ attr_reader :original
16
+
17
+ def initialize(msg, original=$!)
18
+ super(msg)
19
+ @original = original
20
+ end
21
+
22
+ def set_backtrace(bt)
23
+ if original
24
+ original.backtrace.reverse.each do |line|
25
+ bt.last == line ? bt.pop : break
26
+ end
27
+ original_first = original.backtrace.shift
28
+ bt.concat ["#{original_first}: #{original.message}"]
29
+ bt.concat original.backtrace
30
+ end
31
+ super(bt)
32
+ end
33
+ end
34
+
35
+ #user did something invalid
36
+ class UserError < BaseError
37
+ end
38
+
39
+ #for errors with escort itself
40
+ class InternalError < BaseError
41
+ end
42
+
43
+ #for errors with how escort is being used
44
+ class ClientError < BaseError
45
+ end
46
+
47
+ #a dependency is temporarily unavailable
48
+ class TransientError < BaseError
49
+ end
50
+ end
@@ -0,0 +1,102 @@
1
+ module Escort
2
+ module Formatter
3
+ class BorderlessTable
4
+ attr_reader :column_count, :formatter
5
+ attr_accessor :rows
6
+
7
+ def initialize(formatter, options = {})
8
+ @formatter = formatter
9
+ @column_count = options[:columns] || 3
10
+ @rows = []
11
+ end
12
+
13
+ def row(*column_values)
14
+ rows << column_values.map(&:to_s)
15
+ #TODO raise error if column values size doesn't match columns
16
+ end
17
+
18
+ def output(&block)
19
+ block.call(self)
20
+
21
+ rows.each do |cells|
22
+ virtual_row = normalize_virtual_row(virtual_row_for(cells))
23
+ physical_row_count_for(virtual_row).times do |physical_count|
24
+ physical_row = format_physical_row_values(physical_row_for(virtual_row, physical_count))
25
+ formatter.put physical_row.join("").chomp, :newlines => 1
26
+ end
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ def format_physical_row_values(physical_row)
33
+ physical_row.each_with_index.map do |value, index|
34
+ cell_value(value, index)
35
+ end
36
+ end
37
+
38
+ def physical_row_for(virtual_row, index)
39
+ virtual_row.map {|physical| physical[index]}
40
+ end
41
+
42
+ def virtual_row_for(column_values)
43
+ virtual_row = []
44
+ column_values.each_with_index do |cell, index|
45
+ virtual_row << Escort::Formatter::StringSplitter.new(column_width(index) - 1).split(cell)
46
+ end
47
+ normalize_virtual_row(virtual_row)
48
+ end
49
+
50
+ def normalize_virtual_row(virtual_row)
51
+ virtual_row.map do |physical|
52
+ while physical.size < physical_row_count_for(virtual_row)
53
+ physical << ""
54
+ end
55
+ physical
56
+ end
57
+ end
58
+
59
+ def physical_row_count_for(virtual_row)
60
+ virtual_row.map {|physical| physical.size}.max
61
+ end
62
+
63
+ def column_width(column_index)
64
+ #TODO raise error if index out of bounds
65
+ width = fair_column_width(column_index)
66
+ if column_index == column_count - 1
67
+ width = last_column_width
68
+ end
69
+ width
70
+ end
71
+
72
+ def fair_column_width(index)
73
+ #TODO raise error if index out of bounds
74
+ width = values_in_column(index).map(&:length).max
75
+ width = width + 1
76
+ width > max_column_width ? max_column_width : width
77
+ end
78
+
79
+ def last_column_width
80
+ full_fair_column_width = max_column_width * column_count
81
+ all_but_last_fair_column_width = 0
82
+ (column_count - 1).times do |index|
83
+ all_but_last_fair_column_width += fair_column_width(index)
84
+ end
85
+ full_fair_column_width - all_but_last_fair_column_width
86
+ end
87
+
88
+ def values_in_column(column_index)
89
+ #TODO raise error if index out of bounds
90
+ rows.map{|cells| cells[column_index]}
91
+ end
92
+
93
+ def max_column_width
94
+ (formatter.terminal_columns - 1 - formatter.current_indent_string.length)/column_count
95
+ end
96
+
97
+ def cell_value(value, column_index)
98
+ sprintf("%-#{column_width(column_index)}s", value.strip)
99
+ end
100
+ end
101
+ end
102
+ end