toys 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 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: []