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,58 @@
1
+ module Escort
2
+ module Formatter
3
+ module Common
4
+ def option_output_strings(parser)
5
+ options = {}
6
+ parser.specs.each do |name, spec|
7
+ options[name] = "--#{spec[:long]}" +
8
+ (spec[:type] == :flag && spec[:default] ? ", --no-#{spec[:long]}" : "") +
9
+ (spec[:short] && spec[:short] != :none ? ", -#{spec[:short]}" : "") +
10
+ case spec[:type]
11
+ when :flag; ""
12
+ when :int; " <i>"
13
+ when :ints; " <i+>"
14
+ when :string; " <s>"
15
+ when :strings; " <s+>"
16
+ when :float; " <f>"
17
+ when :floats; " <f+>"
18
+ when :io; " <filename/uri>"
19
+ when :ios; " <filename/uri+>"
20
+ when :date; " <date>"
21
+ when :dates; " <date+>"
22
+ end
23
+ end
24
+
25
+ parser.order.each do |what, opt|
26
+ if what == :text
27
+ next
28
+ end
29
+ spec = parser.specs[opt]
30
+ desc = spec[:desc] + begin
31
+ default_s = case spec[:default]
32
+ when $stdout; "<stdout>"
33
+ when $stdin; "<stdin>"
34
+ when $stderr; "<stderr>"
35
+ when Array
36
+ spec[:default].join(", ")
37
+ else
38
+ spec[:default].to_s
39
+ end
40
+
41
+ if spec[:default]
42
+ if spec[:desc] =~ /\.$/
43
+ " (Default: #{default_s})"
44
+ else
45
+ " (default: #{default_s})"
46
+ end
47
+ else
48
+ ""
49
+ end
50
+ end
51
+ options[opt] = {:string => options[opt]}
52
+ options[opt][:desc] = desc
53
+ end
54
+ options
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,106 @@
1
+ module Escort
2
+ module Formatter
3
+ class DefaultHelpFormatter
4
+ include Escort::Formatter::Common
5
+
6
+ attr_reader :setup, :context
7
+
8
+ def initialize(setup, context)
9
+ @setup = setup
10
+ @context = context
11
+ end
12
+
13
+ def print(parser)
14
+ option_strings = option_output_strings(parser)
15
+ command_strings = command_output_strings
16
+
17
+ TerminalFormatter.display($stdout, Terminal.width) do |d|
18
+ d.puts "NAME"
19
+ d.indent(4) do
20
+ d.table(:columns => 3, :newlines => 1) do |t|
21
+ t.row script_name_with_command, '-', current_command_summary
22
+ end
23
+ d.put(setup.description_for(context), :newlines => 2) if setup.description_for(context)
24
+ end
25
+ d.puts "USAGE"
26
+ d.indent(4) do
27
+ context_usage_part = context.map { |command_name| "#{command_name} [#{command_name} options]" }.join(" ")
28
+ context_usage_part ||= ""
29
+ nested_command_part = "command [command options]" if !setup.canonical_command_names_for(context).nil? && setup.canonical_command_names_for(context).length > 0
30
+ nested_command_part ||= ""
31
+ usage_string = "#{script_name} [options] #{context_usage_part} #{nested_command_part} [arguments...]".gsub(/\s+/, ' ')
32
+ d.put usage_string, :newlines => 2
33
+ end
34
+ if setup.version
35
+ d.puts "VERSION"
36
+ d.indent(4) {
37
+ d.put setup.version, :newlines => 2
38
+ }
39
+ end
40
+ if option_strings.keys.size > 0
41
+ d.puts "OPTIONS"
42
+ d.indent(4) {
43
+ d.table(:columns => 3, :newlines => 1) do |t|
44
+ option_strings.each_pair do |key, value|
45
+ t.row value[:string], '-', value[:desc] || ''
46
+ end
47
+ end
48
+ }
49
+ end
50
+ if command_strings.keys.size > 0
51
+ d.puts "COMMANDS"
52
+ d.indent(4) {
53
+ d.table(:columns => 3, :newlines => 1) do |t|
54
+ command_strings.each_pair do |command_name, values_array|
55
+ t.row values_array[0], '-', command_outline(values_array)
56
+ end
57
+ end
58
+ }
59
+ end
60
+ end
61
+ end
62
+
63
+
64
+ private
65
+
66
+ def script_name_with_command
67
+ result = []
68
+ result << script_name
69
+ result << current_command unless context.empty?
70
+ result.join(" ")
71
+ end
72
+
73
+ def script_name
74
+ File.basename($0)
75
+ end
76
+
77
+ def current_command
78
+ context.last || :global
79
+ end
80
+
81
+ def command_outline(command_data)
82
+ result = command_data[1] || ''
83
+ result = command_data[2] || '' if result.nil? || result.empty?
84
+ result
85
+ end
86
+
87
+ def current_command_summary
88
+ setup.summary_for(context) || ''
89
+ end
90
+
91
+ def command_output_strings
92
+ commands = {}
93
+ setup.canonical_command_names_for(context).each do |command_name|
94
+ command_description = setup.command_description_for(command_name, context) || ""
95
+ command_summary = setup.command_summary_for(command_name, context) || ""
96
+ command_aliases = setup.command_aliases_for(command_name, context)
97
+ command_alias_string = command_aliases.join(", ") if command_aliases && command_aliases.size > 0
98
+ command_string = (command_aliases && command_aliases.size > 0 ? "#{command_name}, #{command_alias_string}" : "#{command_name}" )
99
+ command_name = command_name.to_s
100
+ commands[command_name] = [command_string, command_summary, command_description]
101
+ end
102
+ commands
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,13 @@
1
+ module Escort
2
+ module Formatter
3
+ class Options
4
+ attr_reader :setup, :context, :parser
5
+
6
+ def initialize(setup, context, parser)
7
+ @setup = setup
8
+ @context = context
9
+ @parser = parser
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,30 @@
1
+ module Escort
2
+ module Formatter
3
+ class StringSplitter
4
+ attr_reader :max_segment_width
5
+
6
+ def initialize(max_segment_width)
7
+ @max_segment_width = max_segment_width
8
+ end
9
+
10
+ def split(string)
11
+ string.split("\n").map { |s| split_string(s) }.flatten
12
+ end
13
+
14
+ private
15
+
16
+ def split_string(string)
17
+ result = []
18
+ if string.length > max_segment_width
19
+ first_part = string.slice(0, max_segment_width)
20
+ second_part = string.slice(max_segment_width..-1)
21
+ result << first_part
22
+ result << split_string(second_part)
23
+ else
24
+ result << string
25
+ end
26
+ result
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,22 @@
1
+ require 'curses'
2
+
3
+ module Escort
4
+ module Formatter
5
+ class Terminal
6
+ DEFAULT_WIDTH = 80
7
+
8
+ class << self
9
+ def width
10
+ screen_size = self::DEFAULT_WIDTH
11
+ Curses.init_screen
12
+ begin
13
+ screen_size = Curses.cols
14
+ ensure
15
+ Curses.close_screen
16
+ end
17
+ screen_size
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,52 @@
1
+ module Escort
2
+ module Formatter
3
+ class TerminalFormatter
4
+ class << self
5
+ def display(stream = $stdout, max_width = Terminal::DEFAULT_WIDTH, &block)
6
+ formatter = self.new(stream, max_width)
7
+ block.call(formatter)
8
+ end
9
+ end
10
+
11
+ attr_reader :stream, :indent_char, :indent_count, :terminal_columns
12
+
13
+ def initialize(stream = $stdout, max_width = Terminal::DEFAULT_WIDTH)
14
+ @stream = stream
15
+ @indent_char = " "
16
+ @indent_count = 0
17
+ @terminal_columns = (max_width < Terminal::DEFAULT_WIDTH/2 ? Terminal::DEFAULT_WIDTH/2 : max_width)
18
+ end
19
+
20
+ def put(data, options = {:newlines => 0})
21
+ segments = StringSplitter.new(terminal_columns - current_indent_string.size - 1).split(data.to_s)
22
+ segments.each do |segment|
23
+ stream.print "#{current_indent_string}#{segment}"
24
+ end
25
+ newline(options[:newlines])
26
+ end
27
+
28
+ def puts(data)
29
+ put(data, :newlines => 1)
30
+ end
31
+
32
+ def indent(count, &block)
33
+ @indent_count += count
34
+ block.call
35
+ @indent_count -= count
36
+ end
37
+
38
+ def table(options = {}, &block)
39
+ BorderlessTable.new(self, options).output(&block)
40
+ newline(options[:newlines] || 1)
41
+ end
42
+
43
+ def newline(newline_count = 1)
44
+ stream.print("\n" * newline_count)
45
+ end
46
+
47
+ def current_indent_string
48
+ indent_char * indent_count
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,43 @@
1
+ module Escort
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(Escort::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 Escort
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
+
@@ -0,0 +1,145 @@
1
+ module Escort
2
+ class OptionParser
3
+ attr_reader :setup, :configuration
4
+
5
+ def initialize(configuration, setup)
6
+ @configuration = configuration
7
+ @setup = setup
8
+ end
9
+
10
+ def parse(cli_options)
11
+ options = init_invoked_options_hash
12
+ parse_global_options(cli_options, options[:global][:options])
13
+ parse_command_options(cli_options, [], options[:global][:commands])
14
+
15
+ [options, arguments_from(cli_options)]
16
+ end
17
+
18
+ private
19
+
20
+ def init_invoked_options_hash
21
+ {
22
+ :global => {
23
+ :options => {},
24
+ :commands => {}
25
+ }
26
+ }
27
+ end
28
+
29
+ def parse_global_options(cli_options, options)
30
+ context = []
31
+ options.merge!(parse_options(cli_options, context))
32
+ end
33
+
34
+ def parse_command_options(cli_options, context, options)
35
+ unless cli_option_is_a_command?(cli_options, context)
36
+ options
37
+ else
38
+ command = command_name_from(cli_options)
39
+ context << command
40
+ current_options = parse_options(cli_options, context)
41
+ options[command] = {}
42
+ options[command][:options] = current_options
43
+ options[command][:commands] = {}
44
+ parse_command_options(cli_options, context, options[command][:commands])
45
+ end
46
+ end
47
+
48
+ def parse_options(cli_options, context = [])
49
+ stop_words = setup.command_names_for(context).map(&:to_s)
50
+ parser = init_parser(stop_words)
51
+ parser = add_setup_options_to(parser, context)
52
+ parser = add_option_conflicts_to(parser, context)
53
+ parser = default_option_values_from_config_for(parser, context)
54
+ parser.version(setup.version) #set the version if it was provided
55
+ parser.help_formatter(Escort::Formatter::DefaultHelpFormatter.new(setup, context))
56
+ parsed_options = parse_options_string(parser, cli_options)
57
+ Escort::Validator.for(parser).validate(parsed_options, setup.validations_for(context))
58
+ end
59
+
60
+ def add_setup_options_to(parser, context = [])
61
+ setup.options_for(context).each do |name, opts|
62
+ parser.opt name, opts[:desc] || "", opts.dup #have to make sure to dup here, otherwise opts might get stuff added to it and it
63
+ #may cause problems later, e.g. adds default value and when parsed again trollop barfs
64
+ end
65
+ parser
66
+ end
67
+
68
+ def add_option_conflicts_to(parser, context = [])
69
+ setup.conflicting_options_for(context).each do |conflict_list|
70
+ parser.conflicts *conflict_list
71
+ end
72
+ parser
73
+ end
74
+
75
+ def default_option_values_from_config_for(parser, context)
76
+ unless configuration.empty?
77
+ parser.specs.each do |sym, opts|
78
+ if config_has_value_for_context?(sym, context)
79
+ default = config_value_for_context(sym, context)
80
+ opts[:default] = default
81
+ if opts[:multi] && default.nil?
82
+ opts[:default] = [] # multi arguments default to [], not nil
83
+ elsif opts[:multi] && !default.kind_of?(Array)
84
+ opts[:default] = [default]
85
+ else
86
+ opts[:default] = default
87
+ end
88
+ end
89
+ end
90
+ end
91
+ parser
92
+ end
93
+
94
+ def config_has_value_for_context?(option, context)
95
+ relevant_config_hash = config_hash_for_context(context)
96
+ relevant_config_hash[:options].include?(option)
97
+ end
98
+
99
+ def config_value_for_context(option, context)
100
+ relevant_config_hash = config_hash_for_context(context)
101
+ relevant_config_hash[:options][option]
102
+ end
103
+
104
+ def config_hash_for_context(context)
105
+ relevant_config_hash = configuration.global
106
+ context.each do |command_name|
107
+ command_name = command_name.to_sym
108
+ relevant_config_hash = relevant_config_hash[:commands][command_name]
109
+ relevant_config_hash = ensure_config_hash_has_options_and_commands(relevant_config_hash)
110
+ end
111
+ relevant_config_hash
112
+ end
113
+
114
+ def ensure_config_hash_has_options_and_commands(relevant_config_hash)
115
+ relevant_config_hash ||= {}
116
+ relevant_config_hash[:commands] ||= {}
117
+ relevant_config_hash[:options] ||= {}
118
+ relevant_config_hash
119
+ end
120
+
121
+ def cli_option_is_a_command?(cli_options, context)
122
+ cli_options.size > 0 && setup.command_names_for(context).include?(cli_options[0].to_sym)
123
+ end
124
+
125
+ def init_parser(stop_words)
126
+ Trollop::Parser.new.tap do |parser|
127
+ parser.stop_on(stop_words) # make sure we halt parsing if we see a command
128
+ end
129
+ end
130
+
131
+ def parse_options_string(parser, cli_options)
132
+ Trollop::with_standard_exception_handling(parser) do
133
+ parser.parse(cli_options)
134
+ end
135
+ end
136
+
137
+ def command_name_from(cli_options)
138
+ cli_options.shift.to_sym
139
+ end
140
+
141
+ def arguments_from(cli_options)
142
+ cli_options
143
+ end
144
+ end
145
+ end