toys 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: ddcf431eae87a4ef5781ee240d99dca2bf39b9ec3d4f2343158c4f1a3e593078
4
+ data.tar.gz: b7b0fb7173d9e58c42814def124141af2383024c982a30096773dc835718f57c
5
+ SHA512:
6
+ metadata.gz: 5dc3a473a1a2288cfc58f928629d491c4227d73a9765aae48b4384fee391999d4e096fd1fc46a1eb3290a982877a46028c5279c6ed3d863e95b11d6f6a916249
7
+ data.tar.gz: 378aa6465225148659697d6c4c4b75720c6f182c7539b7ef922da601fac94d5ad950a50265452d4439e9c178744b3d4906ec22d9319d444fd4c8163ac97396a0
data/bin/toys ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $:.unshift(File.absolute_path(File.join(File.dirname(__dir__), "lib")))
4
+ require "toys"
5
+
6
+ Toys::Cli.create_standard.run(ARGV)
data/lib/toys.rb ADDED
@@ -0,0 +1,10 @@
1
+ module Toys
2
+ end
3
+
4
+ require "toys/cli"
5
+ require "toys/context"
6
+ require "toys/errors"
7
+ require "toys/lookup"
8
+ require "toys/parser"
9
+ require "toys/tool"
10
+ require "toys/version"
@@ -0,0 +1,9 @@
1
+ short_desc "A collection of system commands for toys"
2
+ long_desc "A collection of system commands for toys"
3
+
4
+ name "version" do
5
+ short_desc "Print current toys version"
6
+ execute do
7
+ puts Toys::VERSION
8
+ end
9
+ end
data/lib/toys/cli.rb ADDED
@@ -0,0 +1,90 @@
1
+ module Toys
2
+ class Cli
3
+ BUILTINS_PATH = File.join(__dir__, "builtins")
4
+ DEFAULT_DIR_NAME = ".toys"
5
+ DEFAULT_FILE_NAME = ".toys.rb"
6
+ DEFAULT_BINARY_NAME = "toys"
7
+ ETC_PATH = "/etc"
8
+
9
+ def initialize(
10
+ binary_name: nil,
11
+ logger: nil,
12
+ config_dir_name: nil,
13
+ config_file_name: nil,
14
+ index_file_name: nil
15
+ )
16
+ @lookup = Toys::Lookup.new(
17
+ config_dir_name: config_dir_name,
18
+ config_file_name: config_file_name,
19
+ index_file_name: index_file_name)
20
+ @context = Context.new(
21
+ @lookup,
22
+ logger: logger || self.class.default_logger,
23
+ binary_name: binary_name)
24
+ end
25
+
26
+ def add_paths(paths)
27
+ @lookup.add_paths(paths)
28
+ self
29
+ end
30
+
31
+ def add_config_paths(paths)
32
+ @lookup.add_config_paths(paths)
33
+ self
34
+ end
35
+
36
+ def add_config_path_hierarchy(path=nil, base="/")
37
+ path ||= Dir.pwd
38
+ paths = []
39
+ loop do
40
+ paths << path
41
+ break if !base || path == base
42
+ next_path = File.dirname(path)
43
+ break if next_path == path
44
+ path = next_path
45
+ end
46
+ @lookup.add_config_paths(paths)
47
+ self
48
+ end
49
+
50
+ def run(*args)
51
+ @context.run(*args)
52
+ end
53
+
54
+ class << self
55
+ def create_standard
56
+ cli = new(
57
+ binary_name: DEFAULT_BINARY_NAME,
58
+ config_dir_name: DEFAULT_DIR_NAME,
59
+ config_file_name: DEFAULT_FILE_NAME,
60
+ index_file_name: DEFAULT_FILE_NAME
61
+ )
62
+ cli.add_config_path_hierarchy
63
+ if !File.directory?(ETC_PATH) || !File.readable?(ETC_PATH)
64
+ cli.add_config_paths(ETC_PATH)
65
+ end
66
+ cli.add_paths(BUILTINS_PATH)
67
+ cli
68
+ end
69
+
70
+ def default_logger
71
+ logger = Logger.new(STDERR)
72
+ logger.formatter = ->(severity, time, progname, msg) {
73
+ msg_str =
74
+ case msg
75
+ when String
76
+ msg
77
+ when Exception
78
+ "#{msg.message} (#{msg.class})\n" << (msg.backtrace || []).join("\n")
79
+ else
80
+ msg.inspect
81
+ end
82
+ timestr = time.strftime("%Y-%m-%d %H:%M:%S")
83
+ "[%s %5s] %s\n" % [timestr, severity, msg_str]
84
+ }
85
+ logger.level = Logger::WARN
86
+ logger
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,39 @@
1
+ module Toys
2
+ class Context
3
+ def initialize(lookup, logger: nil, binary_name: nil, tool_name: nil, args: nil, options: nil)
4
+ @_lookup = lookup
5
+ @logger = logger || Logger.new(STDERR)
6
+ @binary_name = binary_name
7
+ @tool_name = tool_name
8
+ @args = args
9
+ @options = options
10
+ end
11
+
12
+ attr_reader :logger
13
+ attr_reader :binary_name
14
+ attr_reader :tool_name
15
+ attr_reader :args
16
+ attr_reader :options
17
+
18
+ def [](key)
19
+ @options[key]
20
+ end
21
+
22
+ def run(*args)
23
+ args = args.flatten
24
+ tool = @_lookup.lookup(args)
25
+ tool.execute(self, args.slice(tool.full_name.length..-1))
26
+ end
27
+
28
+ def exit_with_code(code)
29
+ throw :result, code
30
+ end
31
+
32
+ attr_reader :_lookup
33
+
34
+ def _create_child(tool_name, args, options)
35
+ Context.new(@_lookup, logger: @logger, binary_name: @binary_name,
36
+ tool_name: tool_name, args: args, options: options)
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,7 @@
1
+ module Toys
2
+ class ToolDefinitionError < StandardError
3
+ end
4
+
5
+ class LookupError < StandardError
6
+ end
7
+ end
@@ -0,0 +1,50 @@
1
+ module Toys
2
+ module Helpers
3
+ module Exec
4
+ def config_exec(opts={})
5
+ @exec_config ||= {}
6
+ @exec_config.merge!(opts)
7
+ end
8
+
9
+ def sh(cmd, opts={})
10
+ utils = Utils.new(self, opts, @exec_config)
11
+ utils.log(cmd)
12
+ system(cmd)
13
+ utils.handle_status($?.exitstatus)
14
+ end
15
+
16
+ def capture(cmd, opts={})
17
+ utils = Utils.new(self, opts, @exec_config)
18
+ utils.log(cmd)
19
+ result = ""
20
+ begin
21
+ result = `#{cmd}`
22
+ utils.handle_status($?.exitstatus)
23
+ rescue StandardError
24
+ utils.handle_status(-1)
25
+ end
26
+ result
27
+ end
28
+
29
+ class Utils
30
+ def initialize(context, opts, config)
31
+ @context = context
32
+ @config = config ? config.merge(opts) : opts
33
+ end
34
+
35
+ def log(cmd)
36
+ unless @config[:log_level] == false
37
+ @context.logger.add(@config[:log_level] || Logger::INFO, cmd)
38
+ end
39
+ end
40
+
41
+ def handle_status(status)
42
+ if status != 0 && @config[:report_subprocess_errors]
43
+ @context.exit_with_code(status)
44
+ end
45
+ status
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,187 @@
1
+ module Toys
2
+ class Lookup
3
+ def initialize(config_dir_name: nil, config_file_name: nil, index_file_name: nil)
4
+ @config_dir_name = config_dir_name
5
+ if @config_dir_name && File.extname(@config_dir_name) == ".rb"
6
+ raise LookupError, "Illegal config dir name #{@config_dir_name.inspect}"
7
+ end
8
+ @config_file_name = config_file_name
9
+ if @config_file_name && File.extname(@config_file_name) != ".rb"
10
+ raise LookupError, "Illegal config file name #{@config_file_name.inspect}"
11
+ end
12
+ @index_file_name = index_file_name
13
+ if @index_file_name && File.extname(@index_file_name) != ".rb"
14
+ raise LookupError, "Illegal index file name #{@index_file_name.inspect}"
15
+ end
16
+ @load_worklist = []
17
+ @tools = {[] => [Tool.new(nil, nil), nil]}
18
+ @max_priority = @min_priority = 0
19
+ end
20
+
21
+ def add_paths(paths, high_priority: false)
22
+ paths = Array(paths)
23
+ paths = paths.reverse if high_priority
24
+ paths.each do |path|
25
+ path = check_path(path)
26
+ priority = high_priority ? (@max_priority += 1) : (@min_priority -= 1)
27
+ @load_worklist << [path, [], priority]
28
+ end
29
+ self
30
+ end
31
+
32
+ def add_config_paths(paths, high_priority: false)
33
+ paths = Array(paths)
34
+ paths = paths.reverse if high_priority
35
+ paths.each do |path|
36
+ if !File.directory?(path) || !File.readable?(path)
37
+ raise LookupError, "Cannot read config directory #{path}"
38
+ end
39
+ priority = high_priority ? (@max_priority += 1) : (@min_priority -= 1)
40
+ if @config_file_name
41
+ p = File.join(path, @config_file_name)
42
+ if !File.directory?(p) && File.readable?(p)
43
+ @load_worklist << [p, [], priority]
44
+ end
45
+ end
46
+ if @config_dir_name
47
+ p = File.join(path, @config_dir_name)
48
+ if File.directory?(p) && File.readable?(p)
49
+ @load_worklist << [p, [], priority]
50
+ end
51
+ end
52
+ end
53
+ self
54
+ end
55
+
56
+ def lookup(args)
57
+ orig_prefix = args.take_while{ |arg| !arg.start_with?("-") }
58
+ cur_prefix = orig_prefix.dup
59
+ loop do
60
+ load_for_prefix(cur_prefix)
61
+ p = orig_prefix.dup
62
+ while p.length >= cur_prefix.length
63
+ return @tools[p].first if @tools.key?(p)
64
+ p.pop
65
+ end
66
+ raise "Bug: No tools found" unless cur_prefix.pop
67
+ end
68
+ end
69
+
70
+ def get_tool(words, priority)
71
+ if @tools.key?(words)
72
+ tool, tool_priority = @tools[words]
73
+ return tool if tool_priority.nil? || tool_priority == priority
74
+ return nil if tool_priority > priority
75
+ end
76
+ parent = get_tool(words[0..-2], priority)
77
+ return nil if parent.nil?
78
+ tool = Tool.new(parent, words.last)
79
+ @tools[words] = [tool, priority]
80
+ tool
81
+ end
82
+
83
+ def tool_defined?(words)
84
+ @tools.key?(words)
85
+ end
86
+
87
+ def list_subtools(words, recursive)
88
+ found_tools = []
89
+ len = words.length
90
+ @tools.each do |n, tp|
91
+ if n.length > 0
92
+ if !recursive && n.slice(0..-2) == words ||
93
+ recursive && n.length > len && n.slice(0, len) == words
94
+ found_tools << tp.first
95
+ end
96
+ end
97
+ end
98
+ found_tools.sort do |a, b|
99
+ a = a.full_name
100
+ b = b.full_name
101
+ while !a.empty? && !b.empty? && a.first == b.first
102
+ a = a.slice(1..-1)
103
+ b = b.slice(1..-1)
104
+ end
105
+ a.first.to_s <=> b.first.to_s
106
+ end
107
+ end
108
+
109
+ def include_path(path, words, remaining_words, priority)
110
+ handle_path(check_path(path), words, remaining_words, priority)
111
+ end
112
+
113
+ private
114
+
115
+ def load_for_prefix(prefix)
116
+ cur_worklist = @load_worklist
117
+ @load_worklist = []
118
+ cur_worklist.each do |path, words, priority|
119
+ handle_path(path, words, calc_remaining_words(prefix, words), priority)
120
+ end
121
+ end
122
+
123
+ def handle_path(path, words, remaining_words, priority)
124
+ if remaining_words
125
+ load_path(path, words, remaining_words, priority)
126
+ else
127
+ @load_worklist << [path, words, priority]
128
+ end
129
+ end
130
+
131
+ def load_path(path, words, remaining_words, priority)
132
+ if File.extname(path) == ".rb"
133
+ tool = get_tool(words, priority)
134
+ if tool
135
+ Parser.parse(path, tool, remaining_words, priority, self, IO.read(path))
136
+ end
137
+ else
138
+ children = Dir.entries(path) - [".", ".."]
139
+ children.each do |child|
140
+ child_path = File.join(path, child)
141
+ if child == @index_file_name
142
+ load_path(check_path(child_path), words, remaining_words, priority)
143
+ else
144
+ next unless check_path(child_path, strict: false)
145
+ child_word = File.basename(child, ".rb")
146
+ next_words = words + [child_word]
147
+ next_remaining_words =
148
+ if remaining_words.empty?
149
+ remaining_words
150
+ elsif child_word == remaining_words.first
151
+ remaining_words.slice(1..-1)
152
+ else
153
+ nil
154
+ end
155
+ handle_path(child_path, next_words, next_remaining_words, priority)
156
+ end
157
+ end
158
+ end
159
+ end
160
+
161
+ def check_path(path, strict: true)
162
+ if File.extname(path) == ".rb"
163
+ if File.directory?(path) || !File.readable?(path)
164
+ raise LookupError, "Cannot read file #{path}"
165
+ end
166
+ else
167
+ if !File.directory?(path) || !File.readable?(path)
168
+ if strict
169
+ raise LookupError, "Cannot read directory #{path}"
170
+ else
171
+ return nil
172
+ end
173
+ end
174
+ end
175
+ path
176
+ end
177
+
178
+ def calc_remaining_words(words1, words2)
179
+ index = 0
180
+ loop do
181
+ return words1.slice(index..-1) if index == words1.length || index == words2.length
182
+ return nil if words1[index] != words2[index]
183
+ index += 1
184
+ end
185
+ end
186
+ end
187
+ end
@@ -0,0 +1,128 @@
1
+ module Toys
2
+ class Parser
3
+ def initialize(path, tool, remaining_words, priority, lookup)
4
+ @path = path
5
+ @tool = tool
6
+ @remaining_words = remaining_words
7
+ @priority = priority
8
+ @lookup = lookup
9
+ end
10
+
11
+ def name(word, alias_of: nil, &block)
12
+ word = word.to_s
13
+ subtool = @lookup.get_tool(@tool.full_name + [word], @priority)
14
+ return self if subtool.nil?
15
+ if alias_of
16
+ if block
17
+ raise Toys::ToysDefinitionError, "Cannot take a block with alias_of"
18
+ end
19
+ target = @tool.full_name + [alias_of.to_s]
20
+ target_tool = @lookup.lookup(target)
21
+ unless target_tool.full_name == target
22
+ raise Toys::ToysDefinitionError, "Alias target #{target.inspect} not found"
23
+ end
24
+ subtool.set_alias_target(target)
25
+ return self
26
+ end
27
+ next_remaining = @remaining_words
28
+ if next_remaining && !next_remaining.empty?
29
+ if next_remaining.first == word
30
+ next_remaining = next_remaining.slice(1..-1)
31
+ else
32
+ next_remaining = nil
33
+ end
34
+ end
35
+ Parser.parse(@path, subtool, next_remaining, @priority, @lookup, block)
36
+ self
37
+ end
38
+
39
+ def alias_as(word)
40
+ unless @tool.root?
41
+ alias_tool = @lookup.get_tool(@tool.full_name.slice(0..-2) + [word], @priority)
42
+ alias_tool.set_alias_target(@tool) if alias_tool
43
+ end
44
+ self
45
+ end
46
+
47
+ def alias_of(target)
48
+ target_tool = @lookup.lookup(target)
49
+ unless target_tool.full_name == target
50
+ raise Toys::ToysDefinitionError, "Alias target #{target.inspect} not found"
51
+ end
52
+ @tool.set_alias_target(target_tool)
53
+ self
54
+ end
55
+
56
+ def include(path)
57
+ @tool.yield_definition do
58
+ @lookup.include_path(path, @tool.full_name, @remaining_words, @priority)
59
+ end
60
+ self
61
+ end
62
+
63
+ def long_desc(desc)
64
+ @tool.long_desc = desc
65
+ self
66
+ end
67
+
68
+ def short_desc(desc)
69
+ @tool.short_desc = desc
70
+ self
71
+ end
72
+
73
+ def switch(key, *switches, accept: nil, default: nil, doc: nil)
74
+ @tool.add_switch(key, *switches, accept: accept, default: default, doc: doc)
75
+ self
76
+ end
77
+
78
+ def required_arg(key, accept: nil, doc: nil)
79
+ @tool.add_required_arg(key, accept: accept, doc: doc)
80
+ self
81
+ end
82
+
83
+ def optional_arg(key, accept: nil, default: nil, doc: nil)
84
+ @tool.add_optional_arg(key, accept: accept, default: default, doc: doc)
85
+ self
86
+ end
87
+
88
+ def remaining_args(key, accept: nil, default: [], doc: nil)
89
+ @tool.set_remaining_args(key, accept: accept, default: default, doc: doc)
90
+ self
91
+ end
92
+
93
+ def execute(&block)
94
+ @tool.executor = block
95
+ self
96
+ end
97
+
98
+ def helper(name, &block)
99
+ @tool.add_helper(name, &block)
100
+ self
101
+ end
102
+
103
+ def helper_module(mod, &block)
104
+ if block
105
+ @tool.define_helper_module(mod, &block)
106
+ else
107
+ @tool.use_helper_module(mod)
108
+ end
109
+ self
110
+ end
111
+
112
+ def _binding
113
+ binding
114
+ end
115
+
116
+ def self.parse(path, tool, remaining_words, priority, lookup, source)
117
+ parser = new(path, tool, remaining_words, priority, lookup)
118
+ tool.defining_from(path) do
119
+ if String === source
120
+ eval(source, parser._binding, path, 1)
121
+ elsif Proc === source
122
+ parser.instance_eval(&source)
123
+ end
124
+ end
125
+ tool
126
+ end
127
+ end
128
+ end
data/lib/toys/tool.rb ADDED
@@ -0,0 +1,450 @@
1
+ require "logger"
2
+ require "optparse"
3
+
4
+ module Toys
5
+ class Tool
6
+
7
+ def initialize(parent, name)
8
+ @parent = parent
9
+ @simple_name = name
10
+ @full_name = name ? [name] : []
11
+ @full_name = parent.full_name + @full_name if parent
12
+
13
+ @definition_path = nil
14
+ @cur_path = nil
15
+
16
+ @alias_target = nil
17
+
18
+ @long_desc = nil
19
+ @short_desc = nil
20
+
21
+ @default_data = {}
22
+ @switches = []
23
+ @required_args = []
24
+ @optional_args = []
25
+ @remaining_args = nil
26
+ @helpers = {}
27
+ @modules = []
28
+ @executor = nil
29
+
30
+ @defined_modules = {}
31
+ end
32
+
33
+ attr_reader :simple_name
34
+ attr_reader :full_name
35
+
36
+ def root?
37
+ @parent.nil?
38
+ end
39
+
40
+ def display_name
41
+ full_name.join(" ")
42
+ end
43
+
44
+ def effective_short_desc
45
+ @short_desc || default_desc
46
+ end
47
+
48
+ def effective_long_desc
49
+ @long_desc || @short_desc || default_desc
50
+ end
51
+
52
+ def has_description?
53
+ !@long_desc.nil? || !@short_desc.nil?
54
+ end
55
+
56
+ def has_definition?
57
+ !@default_data.empty? || !@switches.empty? ||
58
+ !@required_args.empty? || !@optional_args.empty? ||
59
+ !@remaining_args.nil? || !!@executor ||
60
+ !@helpers.empty? || !@modules.empty?
61
+ end
62
+
63
+ def only_collection?
64
+ @executor == false
65
+ end
66
+
67
+ def defining_from(path)
68
+ raise ToolDefinitionError, "Already being defined" if @cur_path
69
+ @cur_path = path
70
+ begin
71
+ yield
72
+ ensure
73
+ @definition_path = @cur_path if has_description? || has_definition?
74
+ @cur_path = nil
75
+ end
76
+ end
77
+
78
+ def yield_definition
79
+ saved_path = @cur_path
80
+ @cur_path = nil
81
+ begin
82
+ yield
83
+ ensure
84
+ @cur_path = saved_path
85
+ end
86
+ end
87
+
88
+ def set_alias_target(target_tool)
89
+ unless target_tool.is_a?(Toys::Tool)
90
+ raise ArgumentError, "Illegal target type"
91
+ end
92
+ if only_collection?
93
+ raise ToolDefinitionError, "Tool #{display_name.inspect} is already" \
94
+ " a collection and cannot be made an alias"
95
+ end
96
+ if has_description? || has_definition?
97
+ raise ToolDefinitionError, "Tool #{display_name.inspect} already has" \
98
+ " a definition and cannot be made an alias"
99
+ end
100
+ if @executor == false
101
+ raise ToolDefinitionError, "Cannot make tool #{display_name.inspect}" \
102
+ " an alias because a descendant is already executable"
103
+ end
104
+ @parent.ensure_collection_only(full_name) if @parent
105
+ @alias_target = target_tool
106
+ end
107
+
108
+ def define_helper_module(name, &block)
109
+ if @alias_target
110
+ raise ToolDefinitionError, "Tool #{display_name.inspect} is an alias"
111
+ end
112
+ unless name.is_a?(String)
113
+ raise ToolDefinitionError,
114
+ "Helper module name #{name.inspect} is not a string"
115
+ end
116
+ if @defined_modules.key?(name)
117
+ raise ToolDefinitionError,
118
+ "Helper module #{name.inspect} is already defined"
119
+ end
120
+ mod = Module.new(&block)
121
+ mod.instance_methods.each do |meth|
122
+ name_str = meth.to_s
123
+ unless name_str =~ /^[a-z]\w+$/
124
+ raise ToolDefinitionError,
125
+ "Illegal helper method name: #{name_str.inspect}"
126
+ end
127
+ end
128
+ @defined_modules[name] = mod
129
+ end
130
+
131
+ def short_desc=(str)
132
+ check_definition_state
133
+ @short_desc = str
134
+ end
135
+
136
+ def long_desc=(str)
137
+ check_definition_state
138
+ @long_desc = str
139
+ end
140
+
141
+ def add_helper(name, &block)
142
+ check_definition_state(true)
143
+ name_str = name.to_s
144
+ unless name_str =~ /^[a-z]\w+$/
145
+ raise ToolDefinitionError, "Illegal helper name: #{name_str.inspect}"
146
+ end
147
+ @helpers[name.to_sym] = block
148
+ end
149
+
150
+ def use_helper_module(mod)
151
+ check_definition_state(true)
152
+ case mod
153
+ when Module
154
+ @modules << mod
155
+ when Symbol
156
+ mod = mod.to_s
157
+ file_name = mod.gsub(/([a-zA-Z])([A-Z])/){ |m| "#{$1}_#{$2.downcase}" }.downcase
158
+ require "toys/helpers/#{file_name}"
159
+ const_name = mod.gsub(/_([a-zA-Z0-9])/){ |m| $1.upcase }.capitalize
160
+ @modules << Toys::Helpers.const_get(const_name)
161
+ when String
162
+ @modules << mod
163
+ else
164
+ raise ToolDefinitionError, "Illegal helper module name: #{mod.inspect}"
165
+ end
166
+ end
167
+
168
+ def add_switch(key, *switches, accept: nil, default: nil, doc: nil)
169
+ check_definition_state(true)
170
+ @default_data[key] = default
171
+ switches << "--#{canonical_switch(key)}=VALUE" if switches.empty?
172
+ switches << accept unless accept.nil?
173
+ switches += Array(doc)
174
+ @switches << [key, switches]
175
+ end
176
+
177
+ def add_required_arg(key, accept: nil, doc: nil)
178
+ check_definition_state(true)
179
+ @default_data[key] = nil
180
+ @required_args << [key, accept, Array(doc)]
181
+ end
182
+
183
+ def add_optional_arg(key, accept: nil, default: nil, doc: nil)
184
+ check_definition_state(true)
185
+ @default_data[key] = default
186
+ @optional_args << [key, accept, Array(doc)]
187
+ end
188
+
189
+ def set_remaining_args(key, accept: nil, default: [], doc: nil)
190
+ check_definition_state(true)
191
+ @default_data[key] = default
192
+ @remaining_args = [key, accept, Array(doc)]
193
+ end
194
+
195
+ def executor=(executor)
196
+ check_definition_state(true)
197
+ @executor = executor
198
+ end
199
+
200
+ def execute(context, args)
201
+ return @alias_target.execute(context, args) if @alias_target
202
+ execution_data = parse_args(args, context.binary_name)
203
+ context = create_child_context(context, args, execution_data)
204
+ if execution_data[:usage_error]
205
+ puts(execution_data[:usage_error])
206
+ puts("")
207
+ show_usage(context, execution_data[:optparse])
208
+ -1
209
+ elsif execution_data[:show_help]
210
+ show_usage(context, execution_data[:optparse],
211
+ recursive: execution_data[:recursive])
212
+ 0
213
+ else
214
+ catch(:result) do
215
+ context.instance_eval(&@executor)
216
+ 0
217
+ end
218
+ end
219
+ end
220
+
221
+ protected
222
+
223
+ def find_module_named(name)
224
+ return @defined_modules[name] if @defined_modules.key?(name)
225
+ return @parent.find_module_named(name) if @parent
226
+ nil
227
+ end
228
+
229
+ def ensure_collection_only(source_name)
230
+ if has_definition?
231
+ raise ToolDefinitionError, "Cannot create tool #{source_name.inspect}" \
232
+ " because #{display_name.inspect} is already a tool."
233
+ end
234
+ if @executor != false
235
+ @executor = false
236
+ @parent.ensure_collection_only(source_name) if @parent
237
+ end
238
+ end
239
+
240
+ private
241
+
242
+ SPECIAL_FLAGS = ["-q", "--quiet", "-v", "--verbose", "-?", "-h", "--help"]
243
+
244
+ def default_desc
245
+ if @alias_target
246
+ "(Alias of #{@alias_target.display_name.inspect})"
247
+ elsif @executor
248
+ "(No description available)"
249
+ else
250
+ "(A collection of commands)"
251
+ end
252
+ end
253
+
254
+ def check_definition_state(execution_field=false)
255
+ if @alias_target
256
+ raise ToolDefinitionError, "Tool #{display_name.inspect} is an alias"
257
+ end
258
+ if @definition_path
259
+ in_clause = @cur_path ? "in #{@cur_path} " : ""
260
+ raise ToolDefinitionError,
261
+ "Cannot redefine tool #{display_name.inspect} #{in_clause}" \
262
+ "(already defined in #{@definition_path})"
263
+ end
264
+ if execution_field
265
+ if @executor == false
266
+ raise ToolDefinitionError,
267
+ "Cannot make tool #{display_name.inspect} executable because a" \
268
+ " descendant is already executable"
269
+ end
270
+ @parent.ensure_collection_only(full_name) if @parent
271
+ end
272
+ end
273
+
274
+ def leaf_option_parser(execution_data, binary_name)
275
+ optparse = OptionParser.new
276
+ banner = ["Usage:", binary_name] + full_name
277
+ banner << "[<options...>]" unless @switches.empty?
278
+ @required_args.each do |key, opts|
279
+ banner << "<#{canonical_switch(key)}>"
280
+ end
281
+ @optional_args.each do |key, opts|
282
+ banner << "[<#{canonical_switch(key)}>]"
283
+ end
284
+ if @remaining_args
285
+ banner << "[<#{canonical_switch(@remaining_args.first)}...>]"
286
+ end
287
+ optparse.banner = banner.join(" ")
288
+ desc = @long_desc || @short_desc || default_desc
289
+ unless desc.empty?
290
+ optparse.separator("")
291
+ optparse.separator(desc)
292
+ end
293
+ optparse.separator("")
294
+ optparse.separator("Options:")
295
+ found_special_flags = []
296
+ @switches.each do |key, opts|
297
+ found_special_flags |= (opts & SPECIAL_FLAGS)
298
+ optparse.on(*opts) do |val|
299
+ execution_data[:options][key] = val
300
+ end
301
+ end
302
+ flags = ["-v", "--verbose"] - found_special_flags
303
+ unless flags.empty?
304
+ optparse.on(*(flags + ["Increase verbosity"])) do
305
+ execution_data[:delta_severity] -= 1
306
+ end
307
+ end
308
+ flags = ["-q", "--quiet"] - found_special_flags
309
+ unless flags.empty?
310
+ optparse.on(*(flags + ["Decrease verbosity"])) do
311
+ execution_data[:delta_severity] += 1
312
+ end
313
+ end
314
+ flags = ["-?", "-h", "--help"] - found_special_flags
315
+ unless flags.empty?
316
+ optparse.on(*(flags + ["Show help message"])) do
317
+ execution_data[:show_help] = true
318
+ end
319
+ end
320
+ optparse
321
+ end
322
+
323
+ def collection_option_parser(execution_data, binary_name)
324
+ optparse = OptionParser.new
325
+ optparse.banner = (["Usage:", binary_name] + full_name + ["<command>", "[<options...>]"]).join(" ")
326
+ desc = @long_desc || @short_desc || default_desc
327
+ unless desc.empty?
328
+ optparse.separator("")
329
+ optparse.separator(desc)
330
+ end
331
+ optparse.separator("")
332
+ optparse.separator("Options:")
333
+ optparse.on("-?", "--help", "Show help message")
334
+ optparse.on("-r", "--[no-]recursive", "Show all subcommands recursively") do |val|
335
+ execution_data[:recursive] = val
336
+ end
337
+ execution_data[:show_help] = true
338
+ optparse
339
+ end
340
+
341
+ def parse_args(args, binary_name)
342
+ optdata = @default_data.dup
343
+ execution_data = {
344
+ show_help: false,
345
+ usage_error: nil,
346
+ delta_severity: 0,
347
+ recursive: false,
348
+ options: optdata
349
+ }
350
+ begin
351
+ binary_name ||= File.basename($0)
352
+ option_parser = @executor ?
353
+ leaf_option_parser(execution_data, binary_name) :
354
+ collection_option_parser(execution_data, binary_name)
355
+ execution_data[:optparse] = option_parser
356
+ remaining = option_parser.parse(args)
357
+ @required_args.each do |key, accept, doc|
358
+ if !execution_data[:show_help] && remaining.empty?
359
+ error = OptionParser::ParseError.new(*args)
360
+ error.reason = "No value given for required argument named <#{canonical_switch(key)}>"
361
+ raise error
362
+ end
363
+ optdata[key] = process_value(key, remaining.shift, accept)
364
+ end
365
+ @optional_args.each do |key, accept, doc|
366
+ break if remaining.empty?
367
+ optdata[key] = process_value(key, remaining.shift, accept)
368
+ end
369
+ unless remaining.empty?
370
+ if !@remaining_args
371
+ if @executor
372
+ error = OptionParser::ParseError.new(*remaining)
373
+ error.reason = "Extra arguments provided"
374
+ raise error
375
+ else
376
+ error = OptionParser::ParseError.new(*(full_name + args))
377
+ error.reason = "Tool not found"
378
+ raise error
379
+ end
380
+ end
381
+ key = @remaining_args[0]
382
+ accept = @remaining_args[1]
383
+ optdata[key] = remaining.map{ |arg| process_value(key, arg, accept) }
384
+ end
385
+ rescue OptionParser::ParseError => e
386
+ execution_data[:usage_error] = e
387
+ end
388
+ execution_data
389
+ end
390
+
391
+ def create_child_context(parent_context, args, execution_data)
392
+ context = parent_context._create_child(
393
+ full_name, args, execution_data[:options])
394
+ context.logger.level += execution_data[:delta_severity]
395
+ @modules.each do |mod|
396
+ unless Module === mod
397
+ found = find_module_named(mod)
398
+ raise StandardError, "Unable to find module #{mod}" unless found
399
+ mod = found
400
+ end
401
+ context.extend(mod)
402
+ end
403
+ @helpers.each do |name, block|
404
+ context.define_singleton_method(name, &block)
405
+ end
406
+ context
407
+ end
408
+
409
+ def show_usage(context, optparse, recursive: false)
410
+ puts(optparse.to_s)
411
+ if @executor
412
+ if !@required_args.empty? || !@optional_args.empty? || @remaining_args
413
+ puts("")
414
+ puts("Positional arguments:")
415
+ args_to_display = @required_args + @optional_args
416
+ args_to_display << @remaining_args if @remaining_args
417
+ args_to_display.each do |key, accept, doc|
418
+ puts(" #{canonical_switch(key).ljust(31)} #{doc.first}")
419
+ doc[1..-1].each do |d|
420
+ puts(" #{d}")
421
+ end unless doc.empty?
422
+ end
423
+ end
424
+ else
425
+ puts("")
426
+ puts("Commands:")
427
+ name_len = full_name.length
428
+ context._lookup.list_subtools(full_name, recursive).each do |tool|
429
+ desc = tool.effective_short_desc
430
+ tool_name = tool.full_name.slice(name_len..-1).join(' ').ljust(31)
431
+ puts(" #{tool_name} #{desc}")
432
+ end
433
+ end
434
+ end
435
+
436
+ def canonical_switch(name)
437
+ name.to_s.downcase.gsub("_", "-").gsub(/[^a-z0-9-]/, "")
438
+ end
439
+
440
+ def process_value(key, val, accept)
441
+ return val unless accept
442
+ n = canonical_switch(key)
443
+ result = val
444
+ optparse = OptionParser.new
445
+ optparse.on("--#{n}=VALUE", accept){ |v| result = v }
446
+ optparse.parse(["--#{n}", val])
447
+ result
448
+ end
449
+ end
450
+ end
@@ -0,0 +1,3 @@
1
+ module Toys
2
+ VERSION = "0.1.0".freeze
3
+ end
metadata ADDED
@@ -0,0 +1,154 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: toys
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Daniel Azuma
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-02-18 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.16'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.16'
27
+ - !ruby/object:Gem::Dependency
28
+ name: minitest
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '5.10'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '5.10'
41
+ - !ruby/object:Gem::Dependency
42
+ name: minitest-focus
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.1'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.1'
55
+ - !ruby/object:Gem::Dependency
56
+ name: minitest-rg
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '5.2'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '5.2'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rake
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '12.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '12.0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rdoc
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '4.2'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '4.2'
97
+ - !ruby/object:Gem::Dependency
98
+ name: yard
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '0.9'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '0.9'
111
+ description: Command line tool framework
112
+ email:
113
+ - dazuma@gmail.com
114
+ executables:
115
+ - toys
116
+ extensions: []
117
+ extra_rdoc_files: []
118
+ files:
119
+ - bin/toys
120
+ - lib/toys.rb
121
+ - lib/toys/builtins/system.rb
122
+ - lib/toys/cli.rb
123
+ - lib/toys/context.rb
124
+ - lib/toys/errors.rb
125
+ - lib/toys/helpers/exec.rb
126
+ - lib/toys/lookup.rb
127
+ - lib/toys/parser.rb
128
+ - lib/toys/tool.rb
129
+ - lib/toys/version.rb
130
+ homepage: https://github.com/dazuma/toys
131
+ licenses:
132
+ - BSD-3-Clause
133
+ metadata: {}
134
+ post_install_message:
135
+ rdoc_options: []
136
+ require_paths:
137
+ - lib
138
+ required_ruby_version: !ruby/object:Gem::Requirement
139
+ requirements:
140
+ - - ">="
141
+ - !ruby/object:Gem::Version
142
+ version: 2.0.0
143
+ required_rubygems_version: !ruby/object:Gem::Requirement
144
+ requirements:
145
+ - - ">="
146
+ - !ruby/object:Gem::Version
147
+ version: '0'
148
+ requirements: []
149
+ rubyforge_project:
150
+ rubygems_version: 2.7.3
151
+ signing_key:
152
+ specification_version: 4
153
+ summary: Command line tool framework
154
+ test_files: []