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,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