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 +7 -0
- data/bin/toys +6 -0
- data/lib/toys.rb +10 -0
- data/lib/toys/builtins/system.rb +9 -0
- data/lib/toys/cli.rb +90 -0
- data/lib/toys/context.rb +39 -0
- data/lib/toys/errors.rb +7 -0
- data/lib/toys/helpers/exec.rb +50 -0
- data/lib/toys/lookup.rb +187 -0
- data/lib/toys/parser.rb +128 -0
- data/lib/toys/tool.rb +450 -0
- data/lib/toys/version.rb +3 -0
- metadata +154 -0
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
data/lib/toys.rb
ADDED
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
|
data/lib/toys/context.rb
ADDED
@@ -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
|
data/lib/toys/errors.rb
ADDED
@@ -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
|
data/lib/toys/lookup.rb
ADDED
@@ -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
|
data/lib/toys/parser.rb
ADDED
@@ -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
|
data/lib/toys/version.rb
ADDED
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: []
|