cri 2.0b1 → 2.0rc1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +115 -1
- data/lib/cri.rb +6 -4
- data/lib/cri/command.rb +79 -28
- data/lib/cri/command_dsl.rb +99 -21
- data/lib/cri/core_ext/string.rb +9 -4
- data/lib/cri/option_parser.rb +67 -72
- data/test/test_command_dsl.rb +14 -2
- data/test/test_option_parser.rb +57 -57
- metadata +2 -2
data/README.md
CHANGED
@@ -1,7 +1,121 @@
|
|
1
1
|
Cri
|
2
2
|
===
|
3
3
|
|
4
|
-
Cri is a library for building easy-to-use commandline tools
|
4
|
+
Cri is a library for building easy-to-use commandline tools with support for
|
5
|
+
nested commands.
|
6
|
+
|
7
|
+
Usage
|
8
|
+
-----
|
9
|
+
|
10
|
+
The central concept in Cri is the _command_, which has option definitions as
|
11
|
+
well as code for actually executing itself. In Cri, the commandline tool
|
12
|
+
itself is a command as well.
|
13
|
+
|
14
|
+
Here’s a sample command definition:
|
15
|
+
|
16
|
+
command = Cri::Command.define do
|
17
|
+
name 'dostuff'
|
18
|
+
usage 'dostuff [options]'
|
19
|
+
aliases :ds, :stuff
|
20
|
+
summary 'does stuff'
|
21
|
+
description 'This command does a lot of stuff. I really mean a lot.'
|
22
|
+
|
23
|
+
flag :h, :help, 'show help for this command' do |value, cmd|
|
24
|
+
puts cmd.help
|
25
|
+
exit 0
|
26
|
+
end
|
27
|
+
flag :m, :more, 'do even more stuff'
|
28
|
+
option :s, :stuff, 'specify stuff to do', :argument => :required
|
29
|
+
|
30
|
+
run do |opts, args, cmd|
|
31
|
+
stuff = opts[:stuff] || 'generic stuff'
|
32
|
+
puts "Doing #{stuff}!"
|
33
|
+
|
34
|
+
if opts[:more]
|
35
|
+
puts 'Doing it even more!'
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
To run this command, invoke the `#run` method with the raw arguments. For
|
41
|
+
example, for a root command (the commandline tool itself), the command could
|
42
|
+
be called like this:
|
43
|
+
|
44
|
+
command.run(ARGS)
|
45
|
+
|
46
|
+
Each command has automatically generated help. This help can be printed using
|
47
|
+
{Cri::Command#help}; something like this will be shown:
|
48
|
+
|
49
|
+
usage: dostuff [options]
|
50
|
+
|
51
|
+
does stuff
|
52
|
+
|
53
|
+
This command does a lot of stuff. I really mean a lot.
|
54
|
+
|
55
|
+
options:
|
56
|
+
|
57
|
+
-h --help show help for this command
|
58
|
+
-m --more do even more stuff
|
59
|
+
-s --stuff specify stuff to do
|
60
|
+
|
61
|
+
Let’s disect the command definition and start with the first five lines:
|
62
|
+
|
63
|
+
name 'dostuff'
|
64
|
+
usage 'dostuff [options]'
|
65
|
+
aliases :ds, :stuff
|
66
|
+
summary 'does stuff'
|
67
|
+
description 'This command does a lot of stuff. I really mean a lot.'
|
68
|
+
|
69
|
+
These lines of the command definition specify the name of the command (or the
|
70
|
+
commandline tool, if the command is the root command), the usage, a list of
|
71
|
+
aliases that can be used to call this command, a one-line summary and a (long)
|
72
|
+
description. The usage should not include a “usage:” prefix nor the name of
|
73
|
+
the supercommand, because the latter will be automatically prepended.
|
74
|
+
|
75
|
+
Aliases don’t make sense for root commands, but for subcommands they do.
|
76
|
+
|
77
|
+
The next few lines contain the command’s option definitions:
|
78
|
+
|
79
|
+
flag :h, :help, 'show help for this command' do |value, cmd|
|
80
|
+
puts cmd.help
|
81
|
+
exit 0
|
82
|
+
end
|
83
|
+
flag :m, :more, 'do even more stuff'
|
84
|
+
option :s, :stuff, 'specify stuff to do', :argument => :required
|
85
|
+
|
86
|
+
Options can be defined using the following methods:
|
87
|
+
|
88
|
+
* {Cri::CommandDSL#option} or {Cri::CommandDSL#opt}
|
89
|
+
* {Cri::CommandDSL#flag} (implies forbidden argument)
|
90
|
+
* {Cri::CommandDSL#required} (implies required argument)
|
91
|
+
* {Cri::CommandDSL#optional} (implies optional argument)
|
92
|
+
|
93
|
+
Each of the above methods also take a block, which will be executed when the
|
94
|
+
option is found. The argument to the block are the option value (`true` in
|
95
|
+
case the option does not have an argument) and the command.
|
96
|
+
|
97
|
+
The last part of the command defines the execution itself:
|
98
|
+
|
99
|
+
run do |opts, args, cmd|
|
100
|
+
stuff = opts[:stuff] || 'generic stuff'
|
101
|
+
puts "Doing #{stuff}!"
|
102
|
+
|
103
|
+
if opts[:more]
|
104
|
+
puts 'Doing it even more!'
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
The {Cri::CommandDSL#run} method takes a block with the actual code to
|
109
|
+
execute. This block takes three arguments: the options, any arguments passed
|
110
|
+
to the command, and the command itself.
|
111
|
+
|
112
|
+
Commands can have subcommands. For example, the `git` commandline tool would be represented by a command that has subcommands named `commit`, `add`, and so on. Commands with subcommands do not use a run block; execution will always be dispatched to a subcommand (or none, if no subcommand is found).
|
113
|
+
|
114
|
+
To add a command as a subcommand to another command, use the {Cri::Command#add_command} method, like this:
|
115
|
+
|
116
|
+
root_cmd.add_command cmd_add
|
117
|
+
root_cmd.add_command cmd_commit
|
118
|
+
root.cmd.add_command cmd_init
|
5
119
|
|
6
120
|
Contributors
|
7
121
|
------------
|
data/lib/cri.rb
CHANGED
@@ -2,20 +2,22 @@
|
|
2
2
|
|
3
3
|
module Cri
|
4
4
|
|
5
|
-
#
|
5
|
+
# A generic error class for all Cri-specific errors.
|
6
6
|
class Error < ::StandardError
|
7
7
|
end
|
8
8
|
|
9
|
-
#
|
9
|
+
# Error that will be raised when an implementation for a method or command
|
10
|
+
# is missing. For commands, this may mean that a run block is missing.
|
10
11
|
class NotImplementedError < Error
|
11
12
|
end
|
12
13
|
|
13
|
-
#
|
14
|
+
# Error that will be raised when no help is available because the help
|
15
|
+
# command has no supercommand for which to show help.
|
14
16
|
class NoHelpAvailableError < Error
|
15
17
|
end
|
16
18
|
|
17
19
|
# The current Cri version.
|
18
|
-
VERSION = '2.
|
20
|
+
VERSION = '2.0rc1'
|
19
21
|
|
20
22
|
autoload 'Command', 'cri/command'
|
21
23
|
autoload 'CommandDSL', 'cri/command_dsl'
|
data/lib/cri/command.rb
CHANGED
@@ -45,35 +45,49 @@ module Cri
|
|
45
45
|
|
46
46
|
end
|
47
47
|
|
48
|
-
# @
|
48
|
+
# @return [Cri::Command, nil] This command’s supercommand, or nil if the
|
49
|
+
# command has no supercommand
|
49
50
|
attr_accessor :supercommand
|
50
51
|
|
51
|
-
# @
|
52
|
+
# @return [Set<Cri::Command>] This command’s subcommands
|
52
53
|
attr_accessor :commands
|
53
54
|
alias_method :subcommands, :commands
|
54
55
|
|
55
|
-
# @
|
56
|
+
# @return [String] The name
|
56
57
|
attr_accessor :name
|
57
58
|
|
58
|
-
# @
|
59
|
+
# @return [Array<String>] A list of aliases for this command that can be
|
60
|
+
# used to invoke this command
|
59
61
|
attr_accessor :aliases
|
60
62
|
|
61
|
-
# @
|
62
|
-
attr_accessor :
|
63
|
+
# @return [String] The short description (“summary”)
|
64
|
+
attr_accessor :summary
|
63
65
|
|
64
|
-
# @
|
65
|
-
attr_accessor :
|
66
|
+
# @return [String] The long description (“description”)
|
67
|
+
attr_accessor :description
|
66
68
|
|
67
|
-
# @
|
69
|
+
# @return [String] The usage, without the “usage:” prefix and without the
|
70
|
+
# supercommands’ names.
|
68
71
|
attr_accessor :usage
|
69
72
|
|
70
|
-
# @
|
73
|
+
# @return [Array<Hash>] The list of option definitions
|
71
74
|
attr_accessor :option_definitions
|
72
75
|
|
73
|
-
# @
|
76
|
+
# @return [Proc] The block that should be executed when invoking this
|
77
|
+
# command (ignored for commands with subcommands)
|
74
78
|
attr_accessor :block
|
75
79
|
|
76
|
-
#
|
80
|
+
# Creates a new command using the DSL. If a string is given, the command
|
81
|
+
# will be defined using the string; if a block is given, the block will be
|
82
|
+
# used instead.
|
83
|
+
#
|
84
|
+
# If the block has one parameter, the block will be executed in the same
|
85
|
+
# context with the command DSL as its parameter. If the block has no
|
86
|
+
# parameters, the block will be executed in the context of the DSL.
|
87
|
+
#
|
88
|
+
# @param [String, nil] The string containing the command’s definition
|
89
|
+
#
|
90
|
+
# @return [Cri::Command] The newly defined command
|
77
91
|
def self.define(string=nil, &block)
|
78
92
|
dsl = Cri::CommandDSL.new
|
79
93
|
if string
|
@@ -86,13 +100,19 @@ module Cri
|
|
86
100
|
dsl.command
|
87
101
|
end
|
88
102
|
|
89
|
-
#
|
103
|
+
# Returns a new command that has support for the `-h`/`--help` option and
|
104
|
+
# also has a `help` subcommand. It is intended to be modified (adding
|
105
|
+
# name, summary, description, other subcommands, …)
|
106
|
+
#
|
107
|
+
# @return [Cri::Command] A basic root command
|
90
108
|
def self.new_basic_root
|
91
109
|
filename = File.dirname(__FILE__) + '/commands/basic_root.rb'
|
92
110
|
self.define(File.read(filename))
|
93
111
|
end
|
94
112
|
|
95
|
-
#
|
113
|
+
# Returns a new command that implements showing help.
|
114
|
+
#
|
115
|
+
# @return [Cri::Command] A basic help command
|
96
116
|
def self.new_basic_help
|
97
117
|
filename = File.dirname(__FILE__) + '/commands/basic_help.rb'
|
98
118
|
self.define(File.read(filename))
|
@@ -100,11 +120,17 @@ module Cri
|
|
100
120
|
|
101
121
|
def initialize
|
102
122
|
@aliases = Set.new
|
103
|
-
@commands = Set.new
|
123
|
+
@commands = Set.new
|
104
124
|
@option_definitions = Set.new
|
105
125
|
end
|
106
126
|
|
107
|
-
#
|
127
|
+
# Modifies the command using the DSL.
|
128
|
+
#
|
129
|
+
# If the block has one parameter, the block will be executed in the same
|
130
|
+
# context with the command DSL as its parameter. If the block has no
|
131
|
+
# parameters, the block will be executed in the context of the DSL.
|
132
|
+
#
|
133
|
+
# @return [Cri::Command] The command itself
|
108
134
|
def modify(&block)
|
109
135
|
dsl = Cri::CommandDSL.new(self)
|
110
136
|
if block.arity == 0
|
@@ -115,7 +141,8 @@ module Cri
|
|
115
141
|
self
|
116
142
|
end
|
117
143
|
|
118
|
-
# @
|
144
|
+
# @return [Hash] The option definitions for the command itself and all its
|
145
|
+
# ancestors
|
119
146
|
def global_option_definitions
|
120
147
|
res = Set.new
|
121
148
|
res.merge(option_definitions)
|
@@ -123,13 +150,22 @@ module Cri
|
|
123
150
|
res
|
124
151
|
end
|
125
152
|
|
126
|
-
#
|
153
|
+
# Adds the given command as a subcommand to the current command.
|
154
|
+
#
|
155
|
+
# @param [Cri::Command] command The command to add as a subcommand
|
156
|
+
#
|
157
|
+
# @return [void]
|
127
158
|
def add_command(command)
|
128
159
|
@commands << command
|
129
160
|
command.supercommand = self
|
130
161
|
end
|
131
162
|
|
132
|
-
#
|
163
|
+
# Defines a new subcommand for the current command using the DSL.
|
164
|
+
#
|
165
|
+
# @param [String, nil] name The name of the subcommand, or nil if no name
|
166
|
+
# should be set (yet)
|
167
|
+
#
|
168
|
+
# @return [Cri::Command] The subcommand
|
133
169
|
def define_command(name=nil, &block)
|
134
170
|
# Execute DSL
|
135
171
|
dsl = Cri::CommandDSL.new
|
@@ -149,7 +185,9 @@ module Cri
|
|
149
185
|
# Returns the commands that could be referred to with the given name. If
|
150
186
|
# the result contains more than one command, the name is ambiguous.
|
151
187
|
#
|
152
|
-
# @
|
188
|
+
# @param [String] name The full, partial or aliases name of the command
|
189
|
+
#
|
190
|
+
# @return [Array<Cri::Command>] A list of commands matching the given name
|
153
191
|
def commands_named(name)
|
154
192
|
# Find by exact name or alias
|
155
193
|
@commands.each do |cmd|
|
@@ -163,9 +201,15 @@ module Cri
|
|
163
201
|
end
|
164
202
|
end
|
165
203
|
|
166
|
-
# Returns the command with the given name.
|
204
|
+
# Returns the command with the given name. This method will display error
|
205
|
+
# messages and exit in case of an error (unknown or ambiguous command).
|
206
|
+
#
|
207
|
+
# The name can be a full command name, a partial command name (e.g. “com”
|
208
|
+
# for “commit”) or an aliased command name (e.g. “ci” for “commit”).
|
167
209
|
#
|
168
|
-
# @
|
210
|
+
# @param [String] name The full, partial or aliases name of the command
|
211
|
+
#
|
212
|
+
# @return [Cri::Command] The command with the given name
|
169
213
|
def command_named(name)
|
170
214
|
commands = commands_named(name)
|
171
215
|
|
@@ -181,7 +225,14 @@ module Cri
|
|
181
225
|
end
|
182
226
|
end
|
183
227
|
|
184
|
-
#
|
228
|
+
# Runs the command with the given commandline arguments.
|
229
|
+
#
|
230
|
+
# @param [Array<String>] opts_and_args A list of unparsed arguments
|
231
|
+
#
|
232
|
+
# @param [Hash] parent_opts A hash of options already handled by the
|
233
|
+
# supercommand
|
234
|
+
#
|
235
|
+
# @return [void]
|
185
236
|
def run(opts_and_args, parent_opts={})
|
186
237
|
if subcommands.empty?
|
187
238
|
# Parse
|
@@ -240,15 +291,15 @@ module Cri
|
|
240
291
|
end
|
241
292
|
|
242
293
|
# Append short description
|
243
|
-
if
|
294
|
+
if summary
|
244
295
|
text << "\n"
|
245
|
-
text <<
|
296
|
+
text << summary + "\n"
|
246
297
|
end
|
247
298
|
|
248
299
|
# Append long description
|
249
|
-
if
|
300
|
+
if description
|
250
301
|
text << "\n"
|
251
|
-
text <<
|
302
|
+
text << description.wrap_and_indent(78, 4) + "\n"
|
252
303
|
end
|
253
304
|
|
254
305
|
# Append subcommands
|
@@ -260,7 +311,7 @@ module Cri
|
|
260
311
|
self.commands.each do |cmd|
|
261
312
|
text << sprintf(" %-#{length+4}s %s\n",
|
262
313
|
cmd.name,
|
263
|
-
cmd.
|
314
|
+
cmd.summary)
|
264
315
|
end
|
265
316
|
end
|
266
317
|
|
data/lib/cri/command_dsl.rb
CHANGED
@@ -2,76 +2,155 @@
|
|
2
2
|
|
3
3
|
module Cri
|
4
4
|
|
5
|
-
#
|
5
|
+
# The command DSL is a class that is used for building and modifying
|
6
|
+
# commands.
|
6
7
|
class CommandDSL
|
7
8
|
|
9
|
+
# @param [Cri::Command, nil] command The command to modify, or nil if a
|
10
|
+
# new command should be created
|
8
11
|
def initialize(command=nil)
|
9
12
|
@command = command || Cri::Command.new
|
10
13
|
end
|
11
14
|
|
12
|
-
# @
|
15
|
+
# @return [Cri::Command] The built command
|
13
16
|
def command
|
14
17
|
@command
|
15
18
|
end
|
16
19
|
|
17
|
-
#
|
18
|
-
|
19
|
-
|
20
|
-
|
20
|
+
# Adds a subcommand to the current command. The command can either be
|
21
|
+
# given explicitly, or a block can be given that defines the command.
|
22
|
+
#
|
23
|
+
# @param [Cri::Command, nil] command The command to add as a subcommand,
|
24
|
+
# or nil if the block should be used to define the command that will be
|
25
|
+
# added as a subcommand
|
26
|
+
#
|
27
|
+
# @return [void]
|
28
|
+
def subcommand(command=nil, &block)
|
29
|
+
if command.nil?
|
30
|
+
command = Cri::Command.define(&block)
|
21
31
|
end
|
22
32
|
|
23
|
-
@command.add_command(
|
33
|
+
@command.add_command(command)
|
24
34
|
end
|
25
35
|
|
26
|
-
#
|
36
|
+
# Sets the command name.
|
37
|
+
#
|
38
|
+
# @param [String] arg The new command name
|
39
|
+
#
|
40
|
+
# @return [void]
|
27
41
|
def name(arg)
|
28
42
|
@command.name = arg
|
29
43
|
end
|
30
44
|
|
31
|
-
#
|
45
|
+
# Sets the command aliases.
|
46
|
+
#
|
47
|
+
# @param [String, Symbol, Array] args The new command aliases
|
48
|
+
#
|
49
|
+
# @return [void]
|
32
50
|
def aliases(*args)
|
33
|
-
@command.aliases = args.flatten
|
51
|
+
@command.aliases = args.flatten.map { |a| a.to_s }
|
34
52
|
end
|
35
53
|
|
36
|
-
#
|
54
|
+
# Sets the command summary.
|
55
|
+
#
|
56
|
+
# @param [String] arg The new command summary
|
57
|
+
#
|
58
|
+
# @return [void]
|
37
59
|
def summary(arg)
|
38
|
-
@command.
|
60
|
+
@command.summary = arg
|
39
61
|
end
|
40
62
|
|
41
|
-
#
|
63
|
+
# Sets the command description.
|
64
|
+
#
|
65
|
+
# @param [String] arg The new command description
|
66
|
+
#
|
67
|
+
# @return [void]
|
42
68
|
def description(arg)
|
43
|
-
@command.
|
69
|
+
@command.description = arg
|
44
70
|
end
|
45
71
|
|
46
|
-
#
|
72
|
+
# Sets the command usage. The usage should not include the “usage:”
|
73
|
+
# prefix, nor should it include the command names of the supercommand.
|
74
|
+
#
|
75
|
+
# @param [String] arg The new command usage
|
76
|
+
#
|
77
|
+
# @return [void]
|
47
78
|
def usage(arg)
|
48
79
|
@command.usage = arg
|
49
80
|
end
|
50
81
|
|
51
|
-
#
|
82
|
+
# Adds a new option to the command. If a block is given, it will be
|
83
|
+
# executed when the option is successfully parsed.
|
84
|
+
#
|
85
|
+
# @param [String, Symbol] short The short option name
|
86
|
+
#
|
87
|
+
# @param [String, Symbol] long The long option name
|
88
|
+
#
|
89
|
+
# @param [String] desc The option description
|
90
|
+
#
|
91
|
+
# @option params [:forbidden, :required, :optional] :argument Whether the
|
92
|
+
# argument is forbidden, required or optional
|
93
|
+
#
|
94
|
+
# @return [void]
|
52
95
|
def option(short, long, desc, params={}, &block)
|
53
96
|
requiredness = params[:argument] || :forbidden
|
54
97
|
self.add_option(short, long, desc, requiredness, block)
|
55
98
|
end
|
56
99
|
alias_method :opt, :option
|
57
100
|
|
58
|
-
#
|
101
|
+
# Adds a new option with a required argument to the command. If a block is
|
102
|
+
# given, it will be executed when the option is successfully parsed.
|
103
|
+
#
|
104
|
+
# @param [String, Symbol] short The short option name
|
105
|
+
#
|
106
|
+
# @param [String, Symbol] long The long option name
|
107
|
+
#
|
108
|
+
# @param [String] desc The option description
|
109
|
+
#
|
110
|
+
# @return [void]
|
111
|
+
#
|
112
|
+
# @see {#option}
|
59
113
|
def required(short, long, desc, &block)
|
60
114
|
self.add_option(short, long, desc, :required, block)
|
61
115
|
end
|
62
116
|
|
63
|
-
#
|
117
|
+
# Adds a new option with a forbidden argument to the command. If a block
|
118
|
+
# is given, it will be executed when the option is successfully parsed.
|
119
|
+
#
|
120
|
+
# @param [String, Symbol] short The short option name
|
121
|
+
#
|
122
|
+
# @param [String, Symbol] long The long option name
|
123
|
+
#
|
124
|
+
# @param [String] desc The option description
|
125
|
+
#
|
126
|
+
# @return [void]
|
127
|
+
#
|
128
|
+
# @see {#option}
|
64
129
|
def flag(short, long, desc, &block)
|
65
130
|
self.add_option(short, long, desc, :forbidden, block)
|
66
131
|
end
|
67
132
|
alias_method :forbidden, :flag
|
68
133
|
|
69
|
-
#
|
134
|
+
# Adds a new option with an optional argument to the command. If a block
|
135
|
+
# is given, it will be executed when the option is successfully parsed.
|
136
|
+
#
|
137
|
+
# @param [String, Symbol] short The short option name
|
138
|
+
#
|
139
|
+
# @param [String, Symbol] long The long option name
|
140
|
+
#
|
141
|
+
# @param [String] desc The option description
|
142
|
+
#
|
143
|
+
# @return [void]
|
144
|
+
#
|
145
|
+
# @see {#option}
|
70
146
|
def optional(short, long, desc, &block)
|
71
147
|
self.add_option(short, long, desc, :optional, block)
|
72
148
|
end
|
73
149
|
|
74
|
-
#
|
150
|
+
# Sets the run block to the given block. The given block should have two
|
151
|
+
# or three arguments (options, arguments, and optionally the command).
|
152
|
+
#
|
153
|
+
# @return [void]
|
75
154
|
def run(&block)
|
76
155
|
unless block.arity != 2 || block.arity != 3
|
77
156
|
raise ArgumentError,
|
@@ -83,7 +162,6 @@ module Cri
|
|
83
162
|
|
84
163
|
protected
|
85
164
|
|
86
|
-
# @todo Document
|
87
165
|
def add_option(short, long, desc, argument, block)
|
88
166
|
@command.option_definitions << {
|
89
167
|
:short => short.to_s,
|
data/lib/cri/core_ext/string.rb
CHANGED
@@ -4,7 +4,9 @@ module Cri::CoreExtensions
|
|
4
4
|
|
5
5
|
module String
|
6
6
|
|
7
|
-
#
|
7
|
+
# Extracts individual paragraphs (separated by two newlines).
|
8
|
+
#
|
9
|
+
# @return [Array<String>] A list of paragraphs in the string
|
8
10
|
def to_paragraphs
|
9
11
|
lines = self.scan(/([^\n]+\n|[^\n]*$)/).map { |s| s[0].strip }
|
10
12
|
|
@@ -22,10 +24,13 @@ module Cri::CoreExtensions
|
|
22
24
|
|
23
25
|
# Word-wraps and indents the string.
|
24
26
|
#
|
25
|
-
#
|
26
|
-
#
|
27
|
+
# @param [Number] width The maximal width of each line. This also includes
|
28
|
+
# indentation, i.e. the actual maximal width of the text is
|
29
|
+
# `width`-`indentation`.
|
30
|
+
#
|
31
|
+
# @param [Number] indentation The number of spaces to indent each line.
|
27
32
|
#
|
28
|
-
#
|
33
|
+
# @return [String] The word-wrapped and indented string
|
29
34
|
def wrap_and_indent(width, indentation)
|
30
35
|
# Split into paragraphs
|
31
36
|
paragraphs = self.to_paragraphs
|
data/lib/cri/option_parser.rb
CHANGED
@@ -3,6 +3,58 @@
|
|
3
3
|
module Cri
|
4
4
|
|
5
5
|
# Cri::OptionParser is used for parsing commandline options.
|
6
|
+
#
|
7
|
+
# Option definitions are hashes with the keys `:short`, `:long` and
|
8
|
+
# `:argument` (optionally `:description` but this is not used by the
|
9
|
+
# option parser, only by the help generator). `:short` is the short,
|
10
|
+
# one-character option, without the `-` prefix. `:long` is the long,
|
11
|
+
# multi-character option, without the `--` prefix. `:argument` can be
|
12
|
+
# :required (if an argument should be provided to the option), :optional
|
13
|
+
# (if an argument may be provided) or :forbidden (if an argument should
|
14
|
+
# not be provided).
|
15
|
+
#
|
16
|
+
# A sample array of definition hashes could look like this:
|
17
|
+
#
|
18
|
+
# [
|
19
|
+
# { :short => 'a', :long => 'all', :argument => :forbidden },
|
20
|
+
# { :short => 'p', :long => 'port', :argument => :required },
|
21
|
+
# ]
|
22
|
+
#
|
23
|
+
# For example, the following commandline options (which should not be
|
24
|
+
# passed as a string, but as an array of strings):
|
25
|
+
#
|
26
|
+
# foo -xyz -a hiss -s -m please --level 50 --father=ani -n luke squeak
|
27
|
+
#
|
28
|
+
# with the following option definitions:
|
29
|
+
#
|
30
|
+
# [
|
31
|
+
# { :short => 'x', :long => 'xxx', :argument => :forbidden },
|
32
|
+
# { :short => 'y', :long => 'yyy', :argument => :forbidden },
|
33
|
+
# { :short => 'z', :long => 'zzz', :argument => :forbidden },
|
34
|
+
# { :short => 'a', :long => 'all', :argument => :forbidden },
|
35
|
+
# { :short => 's', :long => 'stuff', :argument => :optional },
|
36
|
+
# { :short => 'm', :long => 'more', :argument => :optional },
|
37
|
+
# { :short => 'l', :long => 'level', :argument => :required },
|
38
|
+
# { :short => 'f', :long => 'father', :argument => :required },
|
39
|
+
# { :short => 'n', :long => 'name', :argument => :required }
|
40
|
+
# ]
|
41
|
+
#
|
42
|
+
# will be translated into:
|
43
|
+
#
|
44
|
+
# {
|
45
|
+
# :arguments => [ 'foo', 'hiss', 'squeak' ],
|
46
|
+
# :options => {
|
47
|
+
# :xxx => true,
|
48
|
+
# :yyy => true,
|
49
|
+
# :zzz => true,
|
50
|
+
# :all => true,
|
51
|
+
# :stuff => true,
|
52
|
+
# :more => 'please',
|
53
|
+
# :level => '50',
|
54
|
+
# :father => 'ani',
|
55
|
+
# :name => 'luke'
|
56
|
+
# }
|
57
|
+
# }
|
6
58
|
class OptionParser
|
7
59
|
|
8
60
|
# Error that will be raised when an unknown option is encountered.
|
@@ -49,6 +101,13 @@ module Cri
|
|
49
101
|
|
50
102
|
# Parses the commandline arguments. See the instance `parse` method for
|
51
103
|
# details.
|
104
|
+
#
|
105
|
+
# @param [Array<String>] arguments_and_options An array containing the
|
106
|
+
# commandline arguments (will probably be `ARGS` for a root command)
|
107
|
+
#
|
108
|
+
# @param [Array<Hash>] definitions An array of option definitions
|
109
|
+
#
|
110
|
+
# @return [Cri::OptionParser] The option parser self
|
52
111
|
def self.parse(arguments_and_options, definitions)
|
53
112
|
self.new(arguments_and_options, definitions).run
|
54
113
|
end
|
@@ -56,7 +115,7 @@ module Cri
|
|
56
115
|
# Creates a new parser with the given options/arguments and definitions.
|
57
116
|
#
|
58
117
|
# @param [Array<String>] arguments_and_options An array containing the
|
59
|
-
# commandline arguments
|
118
|
+
# commandline arguments (will probably be `ARGS` for a root command)
|
60
119
|
#
|
61
120
|
# @param [Array<Hash>] definitions An array of option definitions
|
62
121
|
def initialize(arguments_and_options, definitions)
|
@@ -83,80 +142,17 @@ module Cri
|
|
83
142
|
@running = false
|
84
143
|
end
|
85
144
|
|
86
|
-
# Parses the commandline arguments into options and arguments
|
87
|
-
#
|
88
|
-
# +arguments_and_options+ is an array of commandline arguments and
|
89
|
-
# options. This will usually be +ARGV+.
|
90
|
-
#
|
91
|
-
# +definitions+ contains a list of hashes defining which options are
|
92
|
-
# allowed and how they will be handled. Such a hash has three keys:
|
93
|
-
#
|
94
|
-
# :short:: The short name of the option, e.g. +a+. Do not include the '-'
|
95
|
-
# prefix.
|
96
|
-
#
|
97
|
-
# :long:: The long name of the option, e.g. +all+. Do not include the '--'
|
98
|
-
# prefix.
|
99
|
-
#
|
100
|
-
# :argument:: Whether this option's argument is required (:required),
|
101
|
-
# optional (:optional) or forbidden (:forbidden).
|
102
|
-
#
|
103
|
-
# A sample array of definition hashes could look like this:
|
104
|
-
#
|
105
|
-
# [
|
106
|
-
# { :short => 'a', :long => 'all', :argument => :forbidden },
|
107
|
-
# { :short => 'p', :long => 'port', :argument => :required },
|
108
|
-
# ]
|
145
|
+
# Parses the commandline arguments into options and arguments.
|
109
146
|
#
|
110
147
|
# During parsing, two errors can be raised:
|
111
148
|
#
|
112
|
-
# IllegalOptionError
|
113
|
-
#
|
114
|
-
# definitions.
|
115
|
-
#
|
116
|
-
# OptionRequiresAnArgumentError:: An option was found that did not have a
|
117
|
-
# value, even though this value was
|
118
|
-
# required.
|
119
|
-
#
|
120
|
-
# What will be returned, is a hash with two keys, :arguments and :options.
|
121
|
-
# The :arguments value contains a list of arguments, and the :options
|
122
|
-
# value contains a hash with key-value pairs for each option. Options
|
123
|
-
# without values will have a +nil+ value instead.
|
124
|
-
#
|
125
|
-
# For example, the following commandline options (which should not be
|
126
|
-
# passed as a string, but as an array of strings):
|
149
|
+
# @raise IllegalOptionError if an unrecognised option was encountered,
|
150
|
+
# i.e. an option that is not present in the list of option definitions
|
127
151
|
#
|
128
|
-
#
|
152
|
+
# @raise OptionRequiresAnArgumentError if an option was found that did not
|
153
|
+
# have a value, even though this value was required.
|
129
154
|
#
|
130
|
-
#
|
131
|
-
#
|
132
|
-
# [
|
133
|
-
# { :short => 'x', :long => 'xxx', :argument => :forbidden },
|
134
|
-
# { :short => 'y', :long => 'yyy', :argument => :forbidden },
|
135
|
-
# { :short => 'z', :long => 'zzz', :argument => :forbidden },
|
136
|
-
# { :short => 'a', :long => 'all', :argument => :forbidden },
|
137
|
-
# { :short => 's', :long => 'stuff', :argument => :optional },
|
138
|
-
# { :short => 'm', :long => 'more', :argument => :optional },
|
139
|
-
# { :short => 'l', :long => 'level', :argument => :required },
|
140
|
-
# { :short => 'f', :long => 'father', :argument => :required },
|
141
|
-
# { :short => 'n', :long => 'name', :argument => :required }
|
142
|
-
# ]
|
143
|
-
#
|
144
|
-
# will be translated into:
|
145
|
-
#
|
146
|
-
# {
|
147
|
-
# :arguments => [ 'foo', 'hiss', 'squeak' ],
|
148
|
-
# :options => {
|
149
|
-
# :xxx => true,
|
150
|
-
# :yyy => true,
|
151
|
-
# :zzz => true,
|
152
|
-
# :all => true,
|
153
|
-
# :stuff => true,
|
154
|
-
# :more => 'please',
|
155
|
-
# :level => '50',
|
156
|
-
# :father => 'ani',
|
157
|
-
# :name => 'luke'
|
158
|
-
# }
|
159
|
-
# }
|
155
|
+
# @return [Cri::OptionParser] The option parser self
|
160
156
|
def run
|
161
157
|
@running = true
|
162
158
|
|
@@ -241,8 +237,7 @@ module Cri
|
|
241
237
|
add_argument(e)
|
242
238
|
end
|
243
239
|
end
|
244
|
-
|
245
|
-
{ :options => options, :arguments => arguments }
|
240
|
+
self
|
246
241
|
ensure
|
247
242
|
@running = false
|
248
243
|
end
|
data/test/test_command_dsl.rb
CHANGED
@@ -31,8 +31,8 @@ class Cri::CommandDSLTestCase < Cri::TestCase
|
|
31
31
|
# Check
|
32
32
|
assert_equal 'moo', command.name
|
33
33
|
assert_equal 'dunno whatever', command.usage
|
34
|
-
assert_equal 'does stuff', command.
|
35
|
-
assert_equal 'This command does a lot of stuff.', command.
|
34
|
+
assert_equal 'does stuff', command.summary
|
35
|
+
assert_equal 'This command does a lot of stuff.', command.description
|
36
36
|
|
37
37
|
# Check options
|
38
38
|
expected_option_definitions = Set.new([
|
@@ -63,4 +63,16 @@ class Cri::CommandDSLTestCase < Cri::TestCase
|
|
63
63
|
assert_equal 'sub', command.subcommands.to_a[0].name
|
64
64
|
end
|
65
65
|
|
66
|
+
def test_aliases
|
67
|
+
# Define
|
68
|
+
dsl = Cri::CommandDSL.new
|
69
|
+
dsl.instance_eval do
|
70
|
+
aliases :moo, :aah
|
71
|
+
end
|
72
|
+
command = dsl.command
|
73
|
+
|
74
|
+
# Check
|
75
|
+
assert_equal %w( aah moo ), command.aliases.sort
|
76
|
+
end
|
77
|
+
|
66
78
|
end
|
data/test/test_option_parser.rb
CHANGED
@@ -6,10 +6,10 @@ class Cri::OptionParserTestCase < Cri::TestCase
|
|
6
6
|
input = %w( foo bar baz )
|
7
7
|
definitions = []
|
8
8
|
|
9
|
-
|
9
|
+
parser = Cri::OptionParser.parse(input, definitions)
|
10
10
|
|
11
|
-
assert_equal({},
|
12
|
-
assert_equal([ 'foo', 'bar', 'baz' ],
|
11
|
+
assert_equal({}, parser.options)
|
12
|
+
assert_equal([ 'foo', 'bar', 'baz' ], parser.arguments)
|
13
13
|
end
|
14
14
|
|
15
15
|
def test_parse_with_invalid_option
|
@@ -19,7 +19,7 @@ class Cri::OptionParserTestCase < Cri::TestCase
|
|
19
19
|
result = nil
|
20
20
|
|
21
21
|
assert_raises(Cri::OptionParser::IllegalOptionError) do
|
22
|
-
|
22
|
+
parser = Cri::OptionParser.parse(input, definitions)
|
23
23
|
end
|
24
24
|
end
|
25
25
|
|
@@ -29,9 +29,9 @@ class Cri::OptionParserTestCase < Cri::TestCase
|
|
29
29
|
{ :long => 'aaa', :short => 'a', :argument => :forbidden }
|
30
30
|
]
|
31
31
|
|
32
|
-
|
32
|
+
parser = Cri::OptionParser.parse(input, definitions)
|
33
33
|
|
34
|
-
assert(!
|
34
|
+
assert(!parser.options[:aaa])
|
35
35
|
end
|
36
36
|
|
37
37
|
def test_parse_with_long_valueless_option
|
@@ -40,10 +40,10 @@ class Cri::OptionParserTestCase < Cri::TestCase
|
|
40
40
|
{ :long => 'aaa', :short => 'a', :argument => :forbidden }
|
41
41
|
]
|
42
42
|
|
43
|
-
|
43
|
+
parser = Cri::OptionParser.parse(input, definitions)
|
44
44
|
|
45
|
-
assert(
|
46
|
-
assert_equal([ 'foo', 'bar' ],
|
45
|
+
assert(parser.options[:aaa])
|
46
|
+
assert_equal([ 'foo', 'bar' ], parser.arguments)
|
47
47
|
end
|
48
48
|
|
49
49
|
def test_parse_with_long_valueful_option
|
@@ -52,10 +52,10 @@ class Cri::OptionParserTestCase < Cri::TestCase
|
|
52
52
|
{ :long => 'aaa', :short => 'a', :argument => :required }
|
53
53
|
]
|
54
54
|
|
55
|
-
|
55
|
+
parser = Cri::OptionParser.parse(input, definitions)
|
56
56
|
|
57
|
-
assert_equal({ :aaa => 'xxx' },
|
58
|
-
assert_equal([ 'foo', 'bar' ],
|
57
|
+
assert_equal({ :aaa => 'xxx' }, parser.options)
|
58
|
+
assert_equal([ 'foo', 'bar' ], parser.arguments)
|
59
59
|
end
|
60
60
|
|
61
61
|
def test_parse_with_long_valueful_equalsign_option
|
@@ -64,10 +64,10 @@ class Cri::OptionParserTestCase < Cri::TestCase
|
|
64
64
|
{ :long => 'aaa', :short => 'a', :argument => :required }
|
65
65
|
]
|
66
66
|
|
67
|
-
|
67
|
+
parser = Cri::OptionParser.parse(input, definitions)
|
68
68
|
|
69
|
-
assert_equal({ :aaa => 'xxx' },
|
70
|
-
assert_equal([ 'foo', 'bar' ],
|
69
|
+
assert_equal({ :aaa => 'xxx' }, parser.options)
|
70
|
+
assert_equal([ 'foo', 'bar' ], parser.arguments)
|
71
71
|
end
|
72
72
|
|
73
73
|
def test_parse_with_long_valueful_option_with_missing_value
|
@@ -79,7 +79,7 @@ class Cri::OptionParserTestCase < Cri::TestCase
|
|
79
79
|
result = nil
|
80
80
|
|
81
81
|
assert_raises(Cri::OptionParser::OptionRequiresAnArgumentError) do
|
82
|
-
|
82
|
+
parser = Cri::OptionParser.parse(input, definitions)
|
83
83
|
end
|
84
84
|
end
|
85
85
|
|
@@ -93,7 +93,7 @@ class Cri::OptionParserTestCase < Cri::TestCase
|
|
93
93
|
result = nil
|
94
94
|
|
95
95
|
assert_raises(Cri::OptionParser::OptionRequiresAnArgumentError) do
|
96
|
-
|
96
|
+
parser = Cri::OptionParser.parse(input, definitions)
|
97
97
|
end
|
98
98
|
end
|
99
99
|
|
@@ -103,10 +103,10 @@ class Cri::OptionParserTestCase < Cri::TestCase
|
|
103
103
|
{ :long => 'aaa', :short => 'a', :argument => :optional }
|
104
104
|
]
|
105
105
|
|
106
|
-
|
106
|
+
parser = Cri::OptionParser.parse(input, definitions)
|
107
107
|
|
108
|
-
assert(
|
109
|
-
assert_equal([ 'foo' ],
|
108
|
+
assert(parser.options[:aaa])
|
109
|
+
assert_equal([ 'foo' ], parser.arguments)
|
110
110
|
end
|
111
111
|
|
112
112
|
def test_parse_with_long_valueful_option_with_optional_value
|
@@ -115,10 +115,10 @@ class Cri::OptionParserTestCase < Cri::TestCase
|
|
115
115
|
{ :long => 'aaa', :short => 'a', :argument => :optional }
|
116
116
|
]
|
117
117
|
|
118
|
-
|
118
|
+
parser = Cri::OptionParser.parse(input, definitions)
|
119
119
|
|
120
|
-
assert_equal({ :aaa => 'xxx' },
|
121
|
-
assert_equal([ 'foo' ],
|
120
|
+
assert_equal({ :aaa => 'xxx' }, parser.options)
|
121
|
+
assert_equal([ 'foo' ], parser.arguments)
|
122
122
|
end
|
123
123
|
|
124
124
|
def test_parse_with_long_valueless_option_with_optional_value_and_more_options
|
@@ -129,12 +129,12 @@ class Cri::OptionParserTestCase < Cri::TestCase
|
|
129
129
|
{ :long => 'ccc', :short => 'c', :argument => :forbidden }
|
130
130
|
]
|
131
131
|
|
132
|
-
|
132
|
+
parser = Cri::OptionParser.parse(input, definitions)
|
133
133
|
|
134
|
-
assert(
|
135
|
-
assert(
|
136
|
-
assert(
|
137
|
-
assert_equal([ 'foo' ],
|
134
|
+
assert(parser.options[:aaa])
|
135
|
+
assert(parser.options[:bbb])
|
136
|
+
assert(parser.options[:ccc])
|
137
|
+
assert_equal([ 'foo' ], parser.arguments)
|
138
138
|
end
|
139
139
|
|
140
140
|
def test_parse_with_short_valueless_options
|
@@ -143,10 +143,10 @@ class Cri::OptionParserTestCase < Cri::TestCase
|
|
143
143
|
{ :long => 'aaa', :short => 'a', :argument => :forbidden }
|
144
144
|
]
|
145
145
|
|
146
|
-
|
146
|
+
parser = Cri::OptionParser.parse(input, definitions)
|
147
147
|
|
148
|
-
assert(
|
149
|
-
assert_equal([ 'foo', 'bar' ],
|
148
|
+
assert(parser.options[:aaa])
|
149
|
+
assert_equal([ 'foo', 'bar' ], parser.arguments)
|
150
150
|
end
|
151
151
|
|
152
152
|
def test_parse_with_short_valueful_option_with_missing_value
|
@@ -158,7 +158,7 @@ class Cri::OptionParserTestCase < Cri::TestCase
|
|
158
158
|
result = nil
|
159
159
|
|
160
160
|
assert_raises(Cri::OptionParser::OptionRequiresAnArgumentError) do
|
161
|
-
|
161
|
+
parser = Cri::OptionParser.parse(input, definitions)
|
162
162
|
end
|
163
163
|
end
|
164
164
|
|
@@ -170,12 +170,12 @@ class Cri::OptionParserTestCase < Cri::TestCase
|
|
170
170
|
{ :long => 'ccc', :short => 'c', :argument => :forbidden }
|
171
171
|
]
|
172
172
|
|
173
|
-
|
173
|
+
parser = Cri::OptionParser.parse(input, definitions)
|
174
174
|
|
175
|
-
assert(
|
176
|
-
assert(
|
177
|
-
assert(
|
178
|
-
assert_equal([ 'foo', 'bar' ],
|
175
|
+
assert(parser.options[:aaa])
|
176
|
+
assert(parser.options[:bbb])
|
177
|
+
assert(parser.options[:ccc])
|
178
|
+
assert_equal([ 'foo', 'bar' ], parser.arguments)
|
179
179
|
end
|
180
180
|
|
181
181
|
def test_parse_with_short_combined_valueful_options_with_missing_value
|
@@ -189,7 +189,7 @@ class Cri::OptionParserTestCase < Cri::TestCase
|
|
189
189
|
result = nil
|
190
190
|
|
191
191
|
assert_raises(Cri::OptionParser::OptionRequiresAnArgumentError) do
|
192
|
-
|
192
|
+
parser = Cri::OptionParser.parse(input, definitions)
|
193
193
|
end
|
194
194
|
end
|
195
195
|
|
@@ -203,7 +203,7 @@ class Cri::OptionParserTestCase < Cri::TestCase
|
|
203
203
|
result = nil
|
204
204
|
|
205
205
|
assert_raises(Cri::OptionParser::OptionRequiresAnArgumentError) do
|
206
|
-
|
206
|
+
parser = Cri::OptionParser.parse(input, definitions)
|
207
207
|
end
|
208
208
|
end
|
209
209
|
|
@@ -213,10 +213,10 @@ class Cri::OptionParserTestCase < Cri::TestCase
|
|
213
213
|
{ :long => 'aaa', :short => 'a', :argument => :optional }
|
214
214
|
]
|
215
215
|
|
216
|
-
|
216
|
+
parser = Cri::OptionParser.parse(input, definitions)
|
217
217
|
|
218
|
-
assert(
|
219
|
-
assert_equal([ 'foo' ],
|
218
|
+
assert(parser.options[:aaa])
|
219
|
+
assert_equal([ 'foo' ], parser.arguments)
|
220
220
|
end
|
221
221
|
|
222
222
|
def test_parse_with_short_valueful_option_with_optional_value
|
@@ -225,10 +225,10 @@ class Cri::OptionParserTestCase < Cri::TestCase
|
|
225
225
|
{ :long => 'aaa', :short => 'a', :argument => :optional }
|
226
226
|
]
|
227
227
|
|
228
|
-
|
228
|
+
parser = Cri::OptionParser.parse(input, definitions)
|
229
229
|
|
230
|
-
assert_equal({ :aaa => 'xxx' },
|
231
|
-
assert_equal([ 'foo' ],
|
230
|
+
assert_equal({ :aaa => 'xxx' }, parser.options)
|
231
|
+
assert_equal([ 'foo' ], parser.arguments)
|
232
232
|
end
|
233
233
|
|
234
234
|
def test_parse_with_short_valueless_option_with_optional_value_and_more_options
|
@@ -239,32 +239,32 @@ class Cri::OptionParserTestCase < Cri::TestCase
|
|
239
239
|
{ :long => 'ccc', :short => 'c', :argument => :forbidden }
|
240
240
|
]
|
241
241
|
|
242
|
-
|
242
|
+
parser = Cri::OptionParser.parse(input, definitions)
|
243
243
|
|
244
|
-
assert(
|
245
|
-
assert(
|
246
|
-
assert(
|
247
|
-
assert_equal([ 'foo' ],
|
244
|
+
assert(parser.options[:aaa])
|
245
|
+
assert(parser.options[:bbb])
|
246
|
+
assert(parser.options[:ccc])
|
247
|
+
assert_equal([ 'foo' ], parser.arguments)
|
248
248
|
end
|
249
249
|
|
250
250
|
def test_parse_with_single_hyphen
|
251
251
|
input = %w( foo - bar )
|
252
252
|
definitions = []
|
253
253
|
|
254
|
-
|
254
|
+
parser = Cri::OptionParser.parse(input, definitions)
|
255
255
|
|
256
|
-
assert_equal({},
|
257
|
-
assert_equal([ 'foo', '-', 'bar' ],
|
256
|
+
assert_equal({}, parser.options)
|
257
|
+
assert_equal([ 'foo', '-', 'bar' ], parser.arguments)
|
258
258
|
end
|
259
259
|
|
260
260
|
def test_parse_with_end_marker
|
261
261
|
input = %w( foo bar -- -x --yyy -abc )
|
262
262
|
definitions = []
|
263
263
|
|
264
|
-
|
264
|
+
parser = Cri::OptionParser.parse(input, definitions)
|
265
265
|
|
266
|
-
assert_equal({},
|
267
|
-
assert_equal([ 'foo', 'bar', '-x', '--yyy', '-abc' ],
|
266
|
+
assert_equal({}, parser.options)
|
267
|
+
assert_equal([ 'foo', 'bar', '-x', '--yyy', '-abc' ], parser.arguments)
|
268
268
|
end
|
269
269
|
|
270
270
|
def test_parse_with_end_marker_between_option_key_and_value
|
@@ -274,7 +274,7 @@ class Cri::OptionParserTestCase < Cri::TestCase
|
|
274
274
|
]
|
275
275
|
|
276
276
|
assert_raises(Cri::OptionParser::OptionRequiresAnArgumentError) do
|
277
|
-
|
277
|
+
parser = Cri::OptionParser.parse(input, definitions)
|
278
278
|
end
|
279
279
|
end
|
280
280
|
|
metadata
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
name: cri
|
3
3
|
version: !ruby/object:Gem::Version
|
4
4
|
prerelease: 3
|
5
|
-
version: 2.
|
5
|
+
version: 2.0rc1
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
8
8
|
- Denis Defreyne
|
@@ -10,7 +10,7 @@ autorequire:
|
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
12
|
|
13
|
-
date: 2011-06-
|
13
|
+
date: 2011-06-26 00:00:00 Z
|
14
14
|
dependencies: []
|
15
15
|
|
16
16
|
description: Cri allows building easy-to-use commandline interfaces with support for subcommands.
|