cri 2.0b1 → 2.0rc1
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/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.
|