cri 1.0.1 → 2.0a1
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.
- data/.gemtest +0 -0
- data/NEWS.md +18 -0
- data/README.md +15 -0
- data/Rakefile +15 -15
- data/cri.gemspec +23 -0
- data/lib/cri.rb +9 -5
- data/lib/cri/command.rb +251 -44
- data/lib/cri/command_dsl.rb +98 -0
- data/lib/cri/commands/basic_help.rb +22 -0
- data/lib/cri/commands/basic_root.rb +8 -0
- data/lib/cri/core_ext.rb +2 -0
- data/lib/cri/core_ext/string.rb +19 -1
- data/lib/cri/option_parser.rb +112 -30
- data/test/helper.rb +26 -0
- data/test/test_base.rb +8 -0
- data/test/test_command.rb +232 -0
- data/test/test_command_dsl.rb +66 -0
- data/test/test_core_ext.rb +58 -0
- data/test/test_option_parser.rb +281 -0
- metadata +30 -20
- data/NEWS +0 -9
- data/README +0 -4
- data/VERSION +0 -1
- data/lib/cri/base.rb +0 -153
@@ -0,0 +1,66 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
class Cri::CommandDSLTestCase < Cri::TestCase
|
4
|
+
|
5
|
+
def test_create_command
|
6
|
+
# Define
|
7
|
+
dsl = Cri::CommandDSL.new
|
8
|
+
dsl.instance_eval do
|
9
|
+
name 'moo'
|
10
|
+
usage 'dunno whatever'
|
11
|
+
summary 'does stuff'
|
12
|
+
description 'This command does a lot of stuff.'
|
13
|
+
|
14
|
+
option :a, :aaa, 'opt a', :argument => :optional
|
15
|
+
required :b, :bbb, 'opt b'
|
16
|
+
optional :c, :ccc, 'opt c'
|
17
|
+
flag :d, :ddd, 'opt d'
|
18
|
+
forbidden :e, :eee, 'opt e'
|
19
|
+
|
20
|
+
run do |opts, args|
|
21
|
+
$did_it_work = :probably
|
22
|
+
end
|
23
|
+
end
|
24
|
+
command = dsl.command
|
25
|
+
|
26
|
+
# Run
|
27
|
+
$did_it_work = :sadly_not
|
28
|
+
command.run(%w( -a x -b y -c -d -e ))
|
29
|
+
assert_equal :probably, $did_it_work
|
30
|
+
|
31
|
+
# Check
|
32
|
+
assert_equal 'moo', command.name
|
33
|
+
assert_equal 'dunno whatever', command.usage
|
34
|
+
assert_equal 'does stuff', command.short_desc
|
35
|
+
assert_equal 'This command does a lot of stuff.', command.long_desc
|
36
|
+
|
37
|
+
# Check options
|
38
|
+
expected_option_definitions = Set.new([
|
39
|
+
{ :short => 'a', :long => 'aaa', :desc => 'opt a', :argument => :optional, :block => nil },
|
40
|
+
{ :short => 'b', :long => 'bbb', :desc => 'opt b', :argument => :required, :block => nil },
|
41
|
+
{ :short => 'c', :long => 'ccc', :desc => 'opt c', :argument => :optional, :block => nil },
|
42
|
+
{ :short => 'd', :long => 'ddd', :desc => 'opt d', :argument => :forbidden, :block => nil },
|
43
|
+
{ :short => 'e', :long => 'eee', :desc => 'opt e', :argument => :forbidden, :block => nil }
|
44
|
+
])
|
45
|
+
actual_option_definitions = Set.new(command.option_definitions)
|
46
|
+
assert_equal expected_option_definitions, actual_option_definitions
|
47
|
+
end
|
48
|
+
|
49
|
+
def test_subcommand
|
50
|
+
# Define
|
51
|
+
dsl = Cri::CommandDSL.new
|
52
|
+
dsl.instance_eval do
|
53
|
+
name 'super'
|
54
|
+
subcommand do
|
55
|
+
name 'sub'
|
56
|
+
end
|
57
|
+
end
|
58
|
+
command = dsl.command
|
59
|
+
|
60
|
+
# Check
|
61
|
+
assert_equal 'super', command.name
|
62
|
+
assert_equal 1, command.subcommands.size
|
63
|
+
assert_equal 'sub', command.subcommands.to_a[0].name
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
class Cri::CoreExtTestCase < Cri::TestCase
|
4
|
+
|
5
|
+
def test_string_to_paragraphs
|
6
|
+
original = "Lorem ipsum dolor sit amet,\nconsectetur adipisicing.\n\n" +
|
7
|
+
"Sed do eiusmod\ntempor incididunt ut labore."
|
8
|
+
|
9
|
+
expected = [ "Lorem ipsum dolor sit amet, consectetur adipisicing.",
|
10
|
+
"Sed do eiusmod tempor incididunt ut labore." ]
|
11
|
+
|
12
|
+
actual = original.to_paragraphs
|
13
|
+
assert_equal expected, actual
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_string_wrap_and_indent_without_indent
|
17
|
+
original = "Lorem ipsum dolor sit amet, consectetur adipisicing elit, " +
|
18
|
+
"sed do eiusmod tempor incididunt ut labore et dolore " +
|
19
|
+
"magna aliqua."
|
20
|
+
|
21
|
+
expected = "Lorem ipsum dolor sit amet, consectetur\n" +
|
22
|
+
"adipisicing elit, sed do eiusmod tempor\n" +
|
23
|
+
"incididunt ut labore et dolore magna\n" +
|
24
|
+
"aliqua."
|
25
|
+
|
26
|
+
actual = original.wrap_and_indent(40, 0)
|
27
|
+
assert_equal expected, actual
|
28
|
+
end
|
29
|
+
|
30
|
+
def test_string_wrap_and_indent_with_indent
|
31
|
+
original = "Lorem ipsum dolor sit amet, consectetur adipisicing elit, " +
|
32
|
+
"sed do eiusmod tempor incididunt ut labore et dolore " +
|
33
|
+
"magna aliqua."
|
34
|
+
|
35
|
+
expected = " Lorem ipsum dolor sit amet,\n" +
|
36
|
+
" consectetur adipisicing elit, sed\n" +
|
37
|
+
" do eiusmod tempor incididunt ut\n" +
|
38
|
+
" labore et dolore magna aliqua."
|
39
|
+
|
40
|
+
actual = original.wrap_and_indent(36, 4)
|
41
|
+
assert_equal expected, actual
|
42
|
+
end
|
43
|
+
|
44
|
+
def test_string_wrap_and_indent_with_multiple_lines
|
45
|
+
original = "Lorem ipsum dolor sit\namet, consectetur adipisicing elit, " +
|
46
|
+
"sed do\neiusmod tempor incididunt ut\nlabore et dolore " +
|
47
|
+
"magna\naliqua."
|
48
|
+
|
49
|
+
expected = " Lorem ipsum dolor sit amet,\n" +
|
50
|
+
" consectetur adipisicing elit, sed\n" +
|
51
|
+
" do eiusmod tempor incididunt ut\n" +
|
52
|
+
" labore et dolore magna aliqua."
|
53
|
+
|
54
|
+
actual = original.wrap_and_indent(36, 4)
|
55
|
+
assert_equal expected, actual
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
@@ -0,0 +1,281 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
class Cri::OptionParserTestCase < Cri::TestCase
|
4
|
+
|
5
|
+
def test_parse_without_options
|
6
|
+
input = %w( foo bar baz )
|
7
|
+
definitions = []
|
8
|
+
|
9
|
+
result = Cri::OptionParser.parse(input, definitions)
|
10
|
+
|
11
|
+
assert_equal({}, result[:options])
|
12
|
+
assert_equal([ 'foo', 'bar', 'baz' ], result[:arguments])
|
13
|
+
end
|
14
|
+
|
15
|
+
def test_parse_with_invalid_option
|
16
|
+
input = %w( foo -x )
|
17
|
+
definitions = []
|
18
|
+
|
19
|
+
result = nil
|
20
|
+
|
21
|
+
assert_raises(Cri::OptionParser::IllegalOptionError) do
|
22
|
+
result = Cri::OptionParser.parse(input, definitions)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def test_parse_without_options
|
27
|
+
input = %w( foo )
|
28
|
+
definitions = [
|
29
|
+
{ :long => 'aaa', :short => 'a', :argument => :forbidden }
|
30
|
+
]
|
31
|
+
|
32
|
+
result = Cri::OptionParser.parse(input, definitions)
|
33
|
+
|
34
|
+
assert(!result[:options][:aaa])
|
35
|
+
end
|
36
|
+
|
37
|
+
def test_parse_with_long_valueless_option
|
38
|
+
input = %w( foo --aaa bar )
|
39
|
+
definitions = [
|
40
|
+
{ :long => 'aaa', :short => 'a', :argument => :forbidden }
|
41
|
+
]
|
42
|
+
|
43
|
+
result = Cri::OptionParser.parse(input, definitions)
|
44
|
+
|
45
|
+
assert(result[:options][:aaa])
|
46
|
+
assert_equal([ 'foo', 'bar' ], result[:arguments])
|
47
|
+
end
|
48
|
+
|
49
|
+
def test_parse_with_long_valueful_option
|
50
|
+
input = %w( foo --aaa xxx bar )
|
51
|
+
definitions = [
|
52
|
+
{ :long => 'aaa', :short => 'a', :argument => :required }
|
53
|
+
]
|
54
|
+
|
55
|
+
result = Cri::OptionParser.parse(input, definitions)
|
56
|
+
|
57
|
+
assert_equal({ :aaa => 'xxx' }, result[:options])
|
58
|
+
assert_equal([ 'foo', 'bar' ], result[:arguments])
|
59
|
+
end
|
60
|
+
|
61
|
+
def test_parse_with_long_valueful_equalsign_option
|
62
|
+
input = %w( foo --aaa=xxx bar )
|
63
|
+
definitions = [
|
64
|
+
{ :long => 'aaa', :short => 'a', :argument => :required }
|
65
|
+
]
|
66
|
+
|
67
|
+
result = Cri::OptionParser.parse(input, definitions)
|
68
|
+
|
69
|
+
assert_equal({ :aaa => 'xxx' }, result[:options])
|
70
|
+
assert_equal([ 'foo', 'bar' ], result[:arguments])
|
71
|
+
end
|
72
|
+
|
73
|
+
def test_parse_with_long_valueful_option_with_missing_value
|
74
|
+
input = %w( foo --aaa )
|
75
|
+
definitions = [
|
76
|
+
{ :long => 'aaa', :short => 'a', :argument => :required }
|
77
|
+
]
|
78
|
+
|
79
|
+
result = nil
|
80
|
+
|
81
|
+
assert_raises(Cri::OptionParser::OptionRequiresAnArgumentError) do
|
82
|
+
result = Cri::OptionParser.parse(input, definitions)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def test_parse_with_two_long_valueful_options
|
87
|
+
input = %w( foo --all --port 2 )
|
88
|
+
definitions = [
|
89
|
+
{ :long => 'all', :short => 'a', :argument => :required },
|
90
|
+
{ :long => 'port', :short => 'p', :argument => :required }
|
91
|
+
]
|
92
|
+
|
93
|
+
result = nil
|
94
|
+
|
95
|
+
assert_raises(Cri::OptionParser::OptionRequiresAnArgumentError) do
|
96
|
+
result = Cri::OptionParser.parse(input, definitions)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def test_parse_with_long_valueless_option_with_optional_value
|
101
|
+
input = %w( foo --aaa )
|
102
|
+
definitions = [
|
103
|
+
{ :long => 'aaa', :short => 'a', :argument => :optional }
|
104
|
+
]
|
105
|
+
|
106
|
+
result = Cri::OptionParser.parse(input, definitions)
|
107
|
+
|
108
|
+
assert(result[:options][:aaa])
|
109
|
+
assert_equal([ 'foo' ], result[:arguments])
|
110
|
+
end
|
111
|
+
|
112
|
+
def test_parse_with_long_valueful_option_with_optional_value
|
113
|
+
input = %w( foo --aaa xxx )
|
114
|
+
definitions = [
|
115
|
+
{ :long => 'aaa', :short => 'a', :argument => :optional }
|
116
|
+
]
|
117
|
+
|
118
|
+
result = Cri::OptionParser.parse(input, definitions)
|
119
|
+
|
120
|
+
assert_equal({ :aaa => 'xxx' }, result[:options])
|
121
|
+
assert_equal([ 'foo' ], result[:arguments])
|
122
|
+
end
|
123
|
+
|
124
|
+
def test_parse_with_long_valueless_option_with_optional_value_and_more_options
|
125
|
+
input = %w( foo --aaa -b -c )
|
126
|
+
definitions = [
|
127
|
+
{ :long => 'aaa', :short => 'a', :argument => :optional },
|
128
|
+
{ :long => 'bbb', :short => 'b', :argument => :forbidden },
|
129
|
+
{ :long => 'ccc', :short => 'c', :argument => :forbidden }
|
130
|
+
]
|
131
|
+
|
132
|
+
result = Cri::OptionParser.parse(input, definitions)
|
133
|
+
|
134
|
+
assert(result[:options][:aaa])
|
135
|
+
assert(result[:options][:bbb])
|
136
|
+
assert(result[:options][:ccc])
|
137
|
+
assert_equal([ 'foo' ], result[:arguments])
|
138
|
+
end
|
139
|
+
|
140
|
+
def test_parse_with_short_valueless_options
|
141
|
+
input = %w( foo -a bar )
|
142
|
+
definitions = [
|
143
|
+
{ :long => 'aaa', :short => 'a', :argument => :forbidden }
|
144
|
+
]
|
145
|
+
|
146
|
+
result = Cri::OptionParser.parse(input, definitions)
|
147
|
+
|
148
|
+
assert(result[:options][:aaa])
|
149
|
+
assert_equal([ 'foo', 'bar' ], result[:arguments])
|
150
|
+
end
|
151
|
+
|
152
|
+
def test_parse_with_short_valueful_option_with_missing_value
|
153
|
+
input = %w( foo -a )
|
154
|
+
definitions = [
|
155
|
+
{ :long => 'aaa', :short => 'a', :argument => :required }
|
156
|
+
]
|
157
|
+
|
158
|
+
result = nil
|
159
|
+
|
160
|
+
assert_raises(Cri::OptionParser::OptionRequiresAnArgumentError) do
|
161
|
+
result = Cri::OptionParser.parse(input, definitions)
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
def test_parse_with_short_combined_valueless_options
|
166
|
+
input = %w( foo -abc bar )
|
167
|
+
definitions = [
|
168
|
+
{ :long => 'aaa', :short => 'a', :argument => :forbidden },
|
169
|
+
{ :long => 'bbb', :short => 'b', :argument => :forbidden },
|
170
|
+
{ :long => 'ccc', :short => 'c', :argument => :forbidden }
|
171
|
+
]
|
172
|
+
|
173
|
+
result = Cri::OptionParser.parse(input, definitions)
|
174
|
+
|
175
|
+
assert(result[:options][:aaa])
|
176
|
+
assert(result[:options][:bbb])
|
177
|
+
assert(result[:options][:ccc])
|
178
|
+
assert_equal([ 'foo', 'bar' ], result[:arguments])
|
179
|
+
end
|
180
|
+
|
181
|
+
def test_parse_with_short_combined_valueful_options_with_missing_value
|
182
|
+
input = %w( foo -abc bar )
|
183
|
+
definitions = [
|
184
|
+
{ :long => 'aaa', :short => 'a', :argument => :required },
|
185
|
+
{ :long => 'bbb', :short => 'b', :argument => :forbidden },
|
186
|
+
{ :long => 'ccc', :short => 'c', :argument => :forbidden }
|
187
|
+
]
|
188
|
+
|
189
|
+
result = nil
|
190
|
+
|
191
|
+
assert_raises(Cri::OptionParser::OptionRequiresAnArgumentError) do
|
192
|
+
result = Cri::OptionParser.parse(input, definitions)
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
def test_parse_with_two_short_valueful_options
|
197
|
+
input = %w( foo -a -p 2 )
|
198
|
+
definitions = [
|
199
|
+
{ :long => 'all', :short => 'a', :argument => :required },
|
200
|
+
{ :long => 'port', :short => 'p', :argument => :required }
|
201
|
+
]
|
202
|
+
|
203
|
+
result = nil
|
204
|
+
|
205
|
+
assert_raises(Cri::OptionParser::OptionRequiresAnArgumentError) do
|
206
|
+
result = Cri::OptionParser.parse(input, definitions)
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
def test_parse_with_short_valueless_option_with_optional_value
|
211
|
+
input = %w( foo -a )
|
212
|
+
definitions = [
|
213
|
+
{ :long => 'aaa', :short => 'a', :argument => :optional }
|
214
|
+
]
|
215
|
+
|
216
|
+
result = Cri::OptionParser.parse(input, definitions)
|
217
|
+
|
218
|
+
assert(result[:options][:aaa])
|
219
|
+
assert_equal([ 'foo' ], result[:arguments])
|
220
|
+
end
|
221
|
+
|
222
|
+
def test_parse_with_short_valueful_option_with_optional_value
|
223
|
+
input = %w( foo -a xxx )
|
224
|
+
definitions = [
|
225
|
+
{ :long => 'aaa', :short => 'a', :argument => :optional }
|
226
|
+
]
|
227
|
+
|
228
|
+
result = Cri::OptionParser.parse(input, definitions)
|
229
|
+
|
230
|
+
assert_equal({ :aaa => 'xxx' }, result[:options])
|
231
|
+
assert_equal([ 'foo' ], result[:arguments])
|
232
|
+
end
|
233
|
+
|
234
|
+
def test_parse_with_short_valueless_option_with_optional_value_and_more_options
|
235
|
+
input = %w( foo -a -b -c )
|
236
|
+
definitions = [
|
237
|
+
{ :long => 'aaa', :short => 'a', :argument => :optional },
|
238
|
+
{ :long => 'bbb', :short => 'b', :argument => :forbidden },
|
239
|
+
{ :long => 'ccc', :short => 'c', :argument => :forbidden }
|
240
|
+
]
|
241
|
+
|
242
|
+
result = Cri::OptionParser.parse(input, definitions)
|
243
|
+
|
244
|
+
assert(result[:options][:aaa])
|
245
|
+
assert(result[:options][:bbb])
|
246
|
+
assert(result[:options][:ccc])
|
247
|
+
assert_equal([ 'foo' ], result[:arguments])
|
248
|
+
end
|
249
|
+
|
250
|
+
def test_parse_with_single_hyphen
|
251
|
+
input = %w( foo - bar )
|
252
|
+
definitions = []
|
253
|
+
|
254
|
+
result = Cri::OptionParser.parse(input, definitions)
|
255
|
+
|
256
|
+
assert_equal({}, result[:options])
|
257
|
+
assert_equal([ 'foo', '-', 'bar' ], result[:arguments])
|
258
|
+
end
|
259
|
+
|
260
|
+
def test_parse_with_end_marker
|
261
|
+
input = %w( foo bar -- -x --yyy -abc )
|
262
|
+
definitions = []
|
263
|
+
|
264
|
+
result = Cri::OptionParser.parse(input, definitions)
|
265
|
+
|
266
|
+
assert_equal({}, result[:options])
|
267
|
+
assert_equal([ 'foo', 'bar', '-x', '--yyy', '-abc' ], result[:arguments])
|
268
|
+
end
|
269
|
+
|
270
|
+
def test_parse_with_end_marker_between_option_key_and_value
|
271
|
+
input = %w( foo --aaa -- zzz )
|
272
|
+
definitions = [
|
273
|
+
{ :long => 'aaa', :short => 'a', :argument => :required }
|
274
|
+
]
|
275
|
+
|
276
|
+
assert_raises(Cri::OptionParser::OptionRequiresAnArgumentError) do
|
277
|
+
result = Cri::OptionParser.parse(input, definitions)
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
281
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,8 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: cri
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
|
4
|
+
prerelease: 3
|
5
|
+
version: 2.0a1
|
5
6
|
platform: ruby
|
6
7
|
authors:
|
7
8
|
- Denis Defreyne
|
@@ -9,11 +10,10 @@ autorequire:
|
|
9
10
|
bindir: bin
|
10
11
|
cert_chain: []
|
11
12
|
|
12
|
-
date:
|
13
|
-
default_executable:
|
13
|
+
date: 2011-06-19 00:00:00 Z
|
14
14
|
dependencies: []
|
15
15
|
|
16
|
-
description: Cri
|
16
|
+
description: Cri allows building easy-to-use commandline interfaces with support for subcommands.
|
17
17
|
email: denis.defreyne@stoneship.org
|
18
18
|
executables: []
|
19
19
|
|
@@ -22,47 +22,57 @@ extensions: []
|
|
22
22
|
extra_rdoc_files:
|
23
23
|
- ChangeLog
|
24
24
|
- LICENSE
|
25
|
-
- README
|
25
|
+
- README.md
|
26
|
+
- NEWS.md
|
26
27
|
files:
|
27
28
|
- ChangeLog
|
28
29
|
- LICENSE
|
29
|
-
- NEWS
|
30
|
-
- README
|
30
|
+
- NEWS.md
|
31
31
|
- Rakefile
|
32
|
-
-
|
33
|
-
- lib/cri.rb
|
34
|
-
- lib/cri/base.rb
|
32
|
+
- README.md
|
35
33
|
- lib/cri/command.rb
|
36
|
-
- lib/cri/
|
34
|
+
- lib/cri/command_dsl.rb
|
35
|
+
- lib/cri/commands/basic_help.rb
|
36
|
+
- lib/cri/commands/basic_root.rb
|
37
37
|
- lib/cri/core_ext/string.rb
|
38
|
+
- lib/cri/core_ext.rb
|
38
39
|
- lib/cri/option_parser.rb
|
39
|
-
|
40
|
-
|
40
|
+
- lib/cri.rb
|
41
|
+
- test/helper.rb
|
42
|
+
- test/test_base.rb
|
43
|
+
- test/test_command.rb
|
44
|
+
- test/test_command_dsl.rb
|
45
|
+
- test/test_core_ext.rb
|
46
|
+
- test/test_option_parser.rb
|
47
|
+
- cri.gemspec
|
48
|
+
- .gemtest
|
49
|
+
homepage: http://stoneship.org/software/cri/
|
41
50
|
licenses: []
|
42
51
|
|
43
52
|
post_install_message:
|
44
53
|
rdoc_options:
|
45
|
-
- --
|
54
|
+
- --main
|
55
|
+
- README.md
|
46
56
|
require_paths:
|
47
57
|
- lib
|
48
58
|
required_ruby_version: !ruby/object:Gem::Requirement
|
59
|
+
none: false
|
49
60
|
requirements:
|
50
61
|
- - ">="
|
51
62
|
- !ruby/object:Gem::Version
|
52
63
|
version: "0"
|
53
|
-
version:
|
54
64
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
55
66
|
requirements:
|
56
|
-
- - "
|
67
|
+
- - ">"
|
57
68
|
- !ruby/object:Gem::Version
|
58
|
-
version:
|
59
|
-
version:
|
69
|
+
version: 1.3.1
|
60
70
|
requirements: []
|
61
71
|
|
62
72
|
rubyforge_project:
|
63
|
-
rubygems_version: 1.
|
73
|
+
rubygems_version: 1.8.5
|
64
74
|
signing_key:
|
65
75
|
specification_version: 3
|
66
|
-
summary:
|
76
|
+
summary: a library for building easy-to-use commandline tools
|
67
77
|
test_files: []
|
68
78
|
|