toys 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|