escort 0.0.1 → 0.1.0

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