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,75 @@
|
|
1
|
+
module Escort
|
2
|
+
module Setup
|
3
|
+
module Configuration
|
4
|
+
class Generator
|
5
|
+
attr_reader :setup
|
6
|
+
|
7
|
+
def initialize(setup)
|
8
|
+
@setup = setup
|
9
|
+
end
|
10
|
+
|
11
|
+
def default_data
|
12
|
+
config_hash = init_config_hash
|
13
|
+
options([], config_hash[:global][:options]) #global options
|
14
|
+
options_for_commands(setup.canonical_command_names_for([]), [], config_hash[:global][:commands])
|
15
|
+
config_hash
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def options_for_commands(commands, context, options = {})
|
21
|
+
commands.each do |command_name|
|
22
|
+
command_name = command_name.to_sym
|
23
|
+
#next if command_name == :escort
|
24
|
+
options[command_name] = {}
|
25
|
+
options[command_name][:options] = {}
|
26
|
+
options[command_name][:commands] = {}
|
27
|
+
current_context = context.dup
|
28
|
+
current_context << command_name
|
29
|
+
options(current_context, options[command_name][:options]) #command_options
|
30
|
+
options_for_commands(setup.canonical_command_names_for(current_context), current_context, options[command_name][:commands])
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def init_config_hash
|
35
|
+
{
|
36
|
+
:global => {
|
37
|
+
:options => {},
|
38
|
+
:commands => {}
|
39
|
+
},
|
40
|
+
:user => {}
|
41
|
+
}
|
42
|
+
end
|
43
|
+
|
44
|
+
def options(context = [], options = {})
|
45
|
+
command_names = setup.command_names_for(context)
|
46
|
+
parser = init_parser(command_names)
|
47
|
+
parser = add_setup_options_to(parser, context)
|
48
|
+
options.merge!(default_option_values(parser))
|
49
|
+
end
|
50
|
+
|
51
|
+
def init_parser(stop_words)
|
52
|
+
Trollop::Parser.new.tap do |parser|
|
53
|
+
parser.stop_on(stop_words) # make sure we halt parsing if we see a command
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def add_setup_options_to(parser, context = [])
|
58
|
+
setup.options_for(context).each do |name, opts|
|
59
|
+
parser.opt name, opts[:desc] || "", opts.dup #have to make sure to dup here, otherwise opts might get stuff added to it and it
|
60
|
+
#may cause problems later, e.g. adds default value and when parsed again trollop barfs
|
61
|
+
end
|
62
|
+
parser
|
63
|
+
end
|
64
|
+
|
65
|
+
def default_option_values(parser)
|
66
|
+
hash = {}
|
67
|
+
parser.specs.each_pair do |key, data|
|
68
|
+
hash[key] = data[:default] || nil
|
69
|
+
end
|
70
|
+
hash
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Escort
|
2
|
+
module Setup
|
3
|
+
module Configuration
|
4
|
+
class Instance
|
5
|
+
class << self
|
6
|
+
def blank
|
7
|
+
self.new(nil, {})
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
attr_reader :data, :path
|
12
|
+
|
13
|
+
def initialize(path, hash)
|
14
|
+
@data = hash
|
15
|
+
@path = path
|
16
|
+
end
|
17
|
+
|
18
|
+
def blank?
|
19
|
+
data.empty?
|
20
|
+
end
|
21
|
+
alias empty? blank?
|
22
|
+
|
23
|
+
def global
|
24
|
+
data[:global] || {}
|
25
|
+
end
|
26
|
+
|
27
|
+
def user
|
28
|
+
data[:user] || {}
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Escort
|
2
|
+
module Setup
|
3
|
+
module Configuration
|
4
|
+
class Loader
|
5
|
+
attr_reader :setup, :auto_options
|
6
|
+
|
7
|
+
def initialize(setup, auto_options)
|
8
|
+
@setup = setup
|
9
|
+
@auto_options = auto_options
|
10
|
+
end
|
11
|
+
|
12
|
+
def configuration
|
13
|
+
if setup.has_config_file?
|
14
|
+
Writer.new(config_path, Generator.new(setup).default_data).write if setup.config_file_autocreatable?
|
15
|
+
Reader.new(config_path).read
|
16
|
+
else
|
17
|
+
Instance.blank
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def default_config_path
|
22
|
+
@default_config_path ||= File.join(File.expand_path(ENV["HOME"]), config_filename)
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def config_filename
|
28
|
+
@config_filename ||= setup.config_file
|
29
|
+
end
|
30
|
+
|
31
|
+
def config_path
|
32
|
+
@config_path ||= (auto_options.non_default_config_path || Locator::DescendingToHome.new(config_filename).locate || default_config_path)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Escort
|
2
|
+
module Setup
|
3
|
+
module Configuration
|
4
|
+
module Locator
|
5
|
+
class Base
|
6
|
+
attr_reader :filename
|
7
|
+
|
8
|
+
def initialize(filename)
|
9
|
+
@filename = filename
|
10
|
+
end
|
11
|
+
|
12
|
+
def locate
|
13
|
+
raise "Must be defined in child class"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
|
3
|
+
module Escort
|
4
|
+
module Setup
|
5
|
+
module Configuration
|
6
|
+
module Locator
|
7
|
+
class DescendingToHome < Base
|
8
|
+
def locate
|
9
|
+
return nil unless filename
|
10
|
+
possible_configs = []
|
11
|
+
Pathname.new(Dir.pwd).descend do |path|
|
12
|
+
filepath = File.join(path, filename)
|
13
|
+
if File.exists?(filepath)
|
14
|
+
possible_configs << filepath
|
15
|
+
end
|
16
|
+
end
|
17
|
+
possible_configs.empty? ? nil : possible_configs.last
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Escort
|
2
|
+
module Setup
|
3
|
+
module Configuration
|
4
|
+
class MergeTool
|
5
|
+
def initialize(new_config_hash, old_config_hash)
|
6
|
+
@new_config_hash = new_config_hash
|
7
|
+
@old_config_hash = old_config_hash
|
8
|
+
end
|
9
|
+
|
10
|
+
def config_hash
|
11
|
+
merge_config(@new_config_hash, @old_config_hash)
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def merge_config(new_hash, old_hash)
|
17
|
+
new_hash.keys.each do |key|
|
18
|
+
new_config_value = new_hash[key]
|
19
|
+
old_config_value = old_hash[key]
|
20
|
+
|
21
|
+
if new_config_value.kind_of?(Hash) && old_config_value.kind_of?(Hash)
|
22
|
+
new_hash[key] = merge_config(new_config_value, old_config_value)
|
23
|
+
elsif old_config_value.nil?
|
24
|
+
new_hash[key] = new_config_value
|
25
|
+
else
|
26
|
+
new_hash[key] = old_config_value
|
27
|
+
end
|
28
|
+
|
29
|
+
if key == :user
|
30
|
+
new_hash[key] = old_config_value
|
31
|
+
end
|
32
|
+
end
|
33
|
+
new_hash
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module Escort
|
4
|
+
module Setup
|
5
|
+
module Configuration
|
6
|
+
class Reader
|
7
|
+
attr_reader :path
|
8
|
+
|
9
|
+
def initialize(path)
|
10
|
+
@path = path
|
11
|
+
end
|
12
|
+
|
13
|
+
def read
|
14
|
+
data = {}
|
15
|
+
data = load_config_at_path if path
|
16
|
+
Instance.new(path, data)
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def load_config_at_path
|
22
|
+
data = {}
|
23
|
+
begin
|
24
|
+
json = File.read(path)
|
25
|
+
hash = JSON.parse(json)
|
26
|
+
data = Escort::Utils.symbolize_keys(hash)
|
27
|
+
rescue => e
|
28
|
+
error_logger.warn { "Found config at #{path}, but failed to load it, perhaps your JSON syntax is invalid. Attempting to continue without..." }
|
29
|
+
error_logger.error(e)
|
30
|
+
end
|
31
|
+
data
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module Escort
|
4
|
+
module Setup
|
5
|
+
module Configuration
|
6
|
+
class Writer
|
7
|
+
attr_reader :path, :data
|
8
|
+
|
9
|
+
def initialize(path, data)
|
10
|
+
@path = path
|
11
|
+
@data = data
|
12
|
+
end
|
13
|
+
|
14
|
+
def write
|
15
|
+
if path && !File.exists?(path)
|
16
|
+
save_to_file
|
17
|
+
Instance.new(path, data)
|
18
|
+
else
|
19
|
+
Instance.blank
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def update
|
24
|
+
current_data = {}
|
25
|
+
if File.exists? path
|
26
|
+
current_data = Reader.new(path).read.data
|
27
|
+
end
|
28
|
+
@data = Escort::Setup::Configuration::MergeTool.new(data, current_data).config_hash
|
29
|
+
save_to_file
|
30
|
+
Instance.new(path, data)
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def save_to_file
|
36
|
+
current_path = File.expand_path(path)
|
37
|
+
File.open(current_path,"w") do |f|
|
38
|
+
f.puts JSON.pretty_generate(data)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Escort
|
2
|
+
module Setup
|
3
|
+
module Dsl
|
4
|
+
class Action
|
5
|
+
class << self
|
6
|
+
def action(command_name, instance, &block)
|
7
|
+
instance.instance_variable_set(:"@block", block)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def initialize(command_name = :global)
|
12
|
+
@command_name = command_name
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module Escort
|
2
|
+
module Setup
|
3
|
+
module Dsl
|
4
|
+
class Command
|
5
|
+
def initialize(name, options = {}, &block)
|
6
|
+
reset(name)
|
7
|
+
@name = name
|
8
|
+
@description = options[:description] || options[:desc] || ""
|
9
|
+
@aliases = [options[:aliases] || []].flatten
|
10
|
+
@requires_arguments ||= options[:requires_arguments]
|
11
|
+
block.call(self) if block_given?
|
12
|
+
rescue => e
|
13
|
+
raise Escort::ClientError.new("Problem with syntax of command '#{@name}' configuration", e)
|
14
|
+
end
|
15
|
+
|
16
|
+
def options(&block)
|
17
|
+
Options.options(@name, @options, &block)
|
18
|
+
end
|
19
|
+
|
20
|
+
def action(&block)
|
21
|
+
Action.action(@name, @action, &block)
|
22
|
+
end
|
23
|
+
|
24
|
+
def validations(&block)
|
25
|
+
Validations.validations(@name, @validations, &block)
|
26
|
+
end
|
27
|
+
|
28
|
+
def command(name, options = {}, &block)
|
29
|
+
options[:requires_arguments] = @requires_arguments
|
30
|
+
command = Command.new(name.to_sym, options, &block)
|
31
|
+
aliases = [options[:aliases] || []].flatten + [name]
|
32
|
+
aliases.each do |name|
|
33
|
+
@commands[name.to_sym] = command
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def requires_arguments(boolean = true)
|
38
|
+
#TODO raise a client error if the value is anything besides true or false
|
39
|
+
@requires_arguments = boolean
|
40
|
+
@commands.each do |command|
|
41
|
+
command.requires_arguments(boolean)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def conflicting_options(*command_names)
|
46
|
+
@conflicts << command_names
|
47
|
+
end
|
48
|
+
|
49
|
+
def summary(summary)
|
50
|
+
@summary = summary
|
51
|
+
end
|
52
|
+
|
53
|
+
def description(description)
|
54
|
+
@description = description
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def reset(name)
|
60
|
+
@requires_arguments = false
|
61
|
+
@commands = {}
|
62
|
+
@options = Options.new(name)
|
63
|
+
@action = Action.new(name)
|
64
|
+
@validations = Validations.new(name)
|
65
|
+
@name = nil
|
66
|
+
@description = nil
|
67
|
+
@aliases = []
|
68
|
+
@conflicts = []
|
69
|
+
@summary = nil
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
module Escort
|
2
|
+
module Setup
|
3
|
+
module Dsl
|
4
|
+
class Global
|
5
|
+
def initialize(&block)
|
6
|
+
reset
|
7
|
+
block.call(self)
|
8
|
+
rescue => e
|
9
|
+
raise Escort::ClientError.new("Problem with syntax of global configuration", e)
|
10
|
+
end
|
11
|
+
|
12
|
+
def options(&block)
|
13
|
+
Options.options(:global, @options, &block)
|
14
|
+
end
|
15
|
+
|
16
|
+
def action(&block)
|
17
|
+
Action.action(:global, @action, &block)
|
18
|
+
end
|
19
|
+
|
20
|
+
def validations(&block)
|
21
|
+
Validations.validations(:global, @validations, &block)
|
22
|
+
end
|
23
|
+
|
24
|
+
def command(name, options = {}, &block)
|
25
|
+
options[:requires_arguments] = @requires_arguments
|
26
|
+
command = Command.new(name.to_sym, options, &block)
|
27
|
+
aliases = [options[:aliases] || []].flatten + [name]
|
28
|
+
aliases.each do |name|
|
29
|
+
@commands[name.to_sym] = command
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def requires_arguments(boolean = true)
|
34
|
+
#TODO raise a client error if the value is anything besides true or false
|
35
|
+
@requires_arguments = boolean
|
36
|
+
@commands.each do |command|
|
37
|
+
command.requires_arguments(boolean)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def config_file(name, options = {})
|
42
|
+
@config_file = ConfigFile.new(name, options)
|
43
|
+
end
|
44
|
+
|
45
|
+
def version(version)
|
46
|
+
@version = version
|
47
|
+
end
|
48
|
+
|
49
|
+
def summary(summary)
|
50
|
+
@summary = summary
|
51
|
+
end
|
52
|
+
|
53
|
+
def description(description)
|
54
|
+
@description = description
|
55
|
+
end
|
56
|
+
|
57
|
+
|
58
|
+
def conflicting_options(*command_names)
|
59
|
+
@conflicts << command_names
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def reset
|
65
|
+
@version = nil
|
66
|
+
@summary = nil
|
67
|
+
@description = nil
|
68
|
+
@commands = {}
|
69
|
+
@requires_arguments = false
|
70
|
+
@options = Options.new
|
71
|
+
@action = Action.new
|
72
|
+
@validations = Validations.new
|
73
|
+
@config_file = nil
|
74
|
+
@conflicts = []
|
75
|
+
end
|
76
|
+
|
77
|
+
def set_instance_variable_on(instance, instance_variable, value)
|
78
|
+
instance_variable_symbol = :"@#{instance_variable.to_s}"
|
79
|
+
instance.instance_variable_set(instance_variable_symbol, value)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|