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.
- checksums.yaml +15 -0
- data/.gitignore +1 -0
- data/.irbrc +1 -0
- data/.rspec +3 -0
- data/.rvmrc +22 -0
- data/README.md +31 -56
- data/TODO.md +152 -0
- data/escort.gemspec +6 -2
- data/examples/1_1_basic.rb +15 -0
- data/examples/1_2_basic_requires_arguments.rb +15 -0
- data/examples/2_2_command.rb +18 -0
- data/examples/2_2_command_requires_arguments.rb +20 -0
- data/examples/2_3_nested_commands.rb +26 -0
- data/examples/3_validations.rb +31 -0
- data/examples/4_1_config_file.rb +42 -0
- data/examples/argument_handling/basic.rb +12 -0
- data/examples/argument_handling/basic_command.rb +18 -0
- data/examples/argument_handling/no_arguments.rb +14 -0
- data/examples/argument_handling/no_arguments_command.rb +20 -0
- data/examples/basic/app.rb +16 -0
- data/examples/command_aliases/app.rb +31 -0
- data/examples/config_file/.apprc2 +16 -0
- data/examples/config_file/app.rb +78 -0
- data/examples/config_file/sub_commands.rb +35 -0
- data/examples/default_command/app.rb +20 -0
- data/examples/sub_commands/app.rb +18 -0
- data/examples/validation_basic/app.rb +31 -0
- data/lib/escort.rb +51 -4
- data/lib/escort/action_command/base.rb +79 -0
- data/lib/escort/action_command/escort_utility_command.rb +53 -0
- data/lib/escort/app.rb +89 -36
- data/lib/escort/arguments.rb +20 -0
- data/lib/escort/auto_options.rb +71 -0
- data/lib/escort/error/error.rb +50 -0
- data/lib/escort/formatter/borderless_table.rb +102 -0
- data/lib/escort/formatter/common.rb +58 -0
- data/lib/escort/formatter/default_help_formatter.rb +106 -0
- data/lib/escort/formatter/options.rb +13 -0
- data/lib/escort/formatter/string_splitter.rb +30 -0
- data/lib/escort/formatter/terminal.rb +22 -0
- data/lib/escort/formatter/terminal_formatter.rb +52 -0
- data/lib/escort/global_pre_parser.rb +43 -0
- data/lib/escort/logger.rb +75 -0
- data/lib/escort/option_parser.rb +145 -0
- data/lib/escort/setup/configuration/generator.rb +75 -0
- data/lib/escort/setup/configuration/instance.rb +33 -0
- data/lib/escort/setup/configuration/loader.rb +37 -0
- data/lib/escort/setup/configuration/locator/base.rb +19 -0
- data/lib/escort/setup/configuration/locator/descending_to_home.rb +23 -0
- data/lib/escort/setup/configuration/merge_tool.rb +38 -0
- data/lib/escort/setup/configuration/reader.rb +36 -0
- data/lib/escort/setup/configuration/writer.rb +44 -0
- data/lib/escort/setup/dsl/action.rb +17 -0
- data/lib/escort/setup/dsl/command.rb +74 -0
- data/lib/escort/setup/dsl/config_file.rb +13 -0
- data/lib/escort/setup/dsl/global.rb +84 -0
- data/lib/escort/setup/dsl/options.rb +25 -0
- data/lib/escort/setup/dsl/validations.rb +25 -0
- data/lib/escort/setup_accessor.rb +194 -0
- data/lib/escort/trollop.rb +15 -4
- data/lib/escort/utils.rb +21 -0
- data/lib/escort/validator.rb +42 -0
- data/lib/escort/version.rb +1 -1
- data/spec/helpers/execute_action_matcher.rb +21 -0
- data/spec/helpers/exit_with_code_matcher.rb +21 -0
- data/spec/helpers/give_option_to_action_with_value_matcher.rb +22 -0
- data/spec/integration/basic_options_spec.rb +53 -0
- data/spec/integration/basic_spec.rb +24 -0
- data/spec/lib/escort/formatter/string_splitter_spec.rb +38 -0
- data/spec/lib/escort/setup_accessor_spec.rb +42 -0
- data/spec/lib/escort/utils_spec.rb +30 -0
- data/spec/spec_helper.rb +22 -0
- metadata +128 -16
- data/lib/escort/command.rb +0 -23
- data/lib/escort/dsl.rb +0 -20
- 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
|
data/lib/escort/app.rb
CHANGED
@@ -1,58 +1,111 @@
|
|
1
1
|
module Escort
|
2
2
|
class App
|
3
|
-
|
4
|
-
include GlobalDsl
|
5
|
-
|
3
|
+
#TODO ensure that every command must have an action
|
6
4
|
class << self
|
7
|
-
def create(
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
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
|
-
|
44
|
+
private
|
23
45
|
|
24
|
-
def
|
25
|
-
|
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
|
-
|
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
|
33
|
-
|
34
|
-
|
59
|
+
def context_from_options(options)
|
60
|
+
commands_in_order(options)
|
61
|
+
end
|
35
62
|
|
36
|
-
|
37
|
-
|
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
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
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
|
51
|
-
|
101
|
+
def print_stacktrace(e)
|
102
|
+
error_logger.error{ e }
|
52
103
|
end
|
53
104
|
|
54
|
-
def
|
55
|
-
|
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
|