cri 1.0.1 → 2.0a1
Sign up to get free protection for your applications and to get access to all the features.
- 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
data/.gemtest
ADDED
File without changes
|
data/NEWS.md
ADDED
data/README.md
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
Cri
|
2
|
+
===
|
3
|
+
|
4
|
+
Cri is a library for building easy-to-use commandline tools.
|
5
|
+
|
6
|
+
Contributors
|
7
|
+
------------
|
8
|
+
|
9
|
+
(In alphabetical order)
|
10
|
+
|
11
|
+
* Toon Willems
|
12
|
+
|
13
|
+
Thanks for Lee “injekt” Jarvis for [Slop][1], which has inspired the design of Cri 2.0.
|
14
|
+
|
15
|
+
[1]: https://github.com/injekt/slop
|
data/Rakefile
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# encoing: utf-8
|
2
|
+
|
1
3
|
##### Requirements
|
2
4
|
|
3
5
|
# Rake etc
|
@@ -8,22 +10,18 @@ require 'minitest/unit'
|
|
8
10
|
$LOAD_PATH.unshift(File.expand_path(File.dirname(__FILE__) + '/lib'))
|
9
11
|
require 'cri'
|
10
12
|
|
11
|
-
#####
|
12
|
-
|
13
|
-
begin
|
14
|
-
require 'jeweler'
|
15
|
-
Jeweler::Tasks.new do |s|
|
16
|
-
s.name = "cri"
|
17
|
-
s.summary = "Cri is a library for building easy-to-use commandline tools."
|
18
|
-
s.description = "Cri is a library for building easy-to-use commandline tools."
|
13
|
+
##### Documentation
|
19
14
|
|
20
|
-
|
21
|
-
s.email = "denis.defreyne@stoneship.org"
|
15
|
+
require 'yard'
|
22
16
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
17
|
+
YARD::Rake::YardocTask.new(:doc) do |yard|
|
18
|
+
yard.files = Dir['lib/**/*.rb']
|
19
|
+
yard.options = [
|
20
|
+
'--markup', 'markdown',
|
21
|
+
'--readme', 'README.md',
|
22
|
+
'--files', 'NEWS.md,LICENSE',
|
23
|
+
'--output-dir', 'doc/yardoc',
|
24
|
+
]
|
27
25
|
end
|
28
26
|
|
29
27
|
##### Testing
|
@@ -32,10 +30,12 @@ desc 'Runs all tests'
|
|
32
30
|
task :test do
|
33
31
|
ENV['QUIET'] ||= 'true'
|
34
32
|
|
35
|
-
$LOAD_PATH.unshift(File.expand_path(File.dirname(__FILE__)
|
33
|
+
$LOAD_PATH.unshift(File.expand_path(File.dirname(__FILE__)))
|
36
34
|
|
37
35
|
MiniTest::Unit.autorun
|
38
36
|
|
37
|
+
require 'test/helper.rb'
|
38
|
+
|
39
39
|
test_files = Dir['test/test_*.rb']
|
40
40
|
test_files.each { |f| require f }
|
41
41
|
end
|
data/cri.gemspec
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
$LOAD_PATH.unshift(File.expand_path('../lib/', __FILE__))
|
4
|
+
require 'cri'
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = 'cri'
|
8
|
+
s.version = Cri::VERSION
|
9
|
+
s.homepage = 'http://stoneship.org/software/cri/' # TODO CREATE A WEB SITE YOU SILLY PERSON
|
10
|
+
s.summary = 'a library for building easy-to-use commandline tools'
|
11
|
+
s.description = 'Cri allows building easy-to-use commandline interfaces with support for subcommands.'
|
12
|
+
|
13
|
+
s.author = 'Denis Defreyne'
|
14
|
+
s.email = 'denis.defreyne@stoneship.org'
|
15
|
+
|
16
|
+
s.files = Dir['[A-Z]*'] +
|
17
|
+
Dir['{lib,test}/**/*'] +
|
18
|
+
[ 'cri.gemspec', '.gemtest' ]
|
19
|
+
s.require_paths = [ 'lib' ]
|
20
|
+
|
21
|
+
s.rdoc_options = [ '--main', 'README.md' ]
|
22
|
+
s.extra_rdoc_files = [ 'ChangeLog', 'LICENSE', 'README.md', 'NEWS.md' ]
|
23
|
+
end
|
data/lib/cri.rb
CHANGED
@@ -1,12 +1,16 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
1
3
|
module Cri
|
2
4
|
|
3
5
|
# The current Cri version.
|
4
|
-
VERSION = '
|
6
|
+
VERSION = '2.0a1'
|
7
|
+
|
8
|
+
autoload 'Command', 'cri/command'
|
9
|
+
autoload 'CommandDSL', 'cri/command_dsl'
|
10
|
+
autoload 'OptionParser', 'cri/option_parser'
|
5
11
|
|
6
12
|
end
|
7
13
|
|
8
|
-
|
9
|
-
|
10
|
-
require 'cri/command'
|
14
|
+
require 'set'
|
15
|
+
|
11
16
|
require 'cri/core_ext'
|
12
|
-
require 'cri/option_parser'
|
data/lib/cri/command.rb
CHANGED
@@ -1,65 +1,216 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
1
3
|
module Cri
|
2
4
|
|
3
5
|
# Cri::Command represents a command that can be executed on the commandline.
|
4
|
-
# It is
|
6
|
+
# It is also used for the commandline tool itself.
|
5
7
|
class Command
|
6
8
|
|
7
|
-
|
9
|
+
# Delegate used for partitioning the list of arguments and options. This
|
10
|
+
# delegate will stop the parser as soon as the first argument, i.e. the
|
11
|
+
# command, is found.
|
12
|
+
#
|
13
|
+
# @api private
|
14
|
+
class OptionParserPartitioningDelegate
|
15
|
+
|
16
|
+
# Returns the last parsed argument, which, in this case, will be the
|
17
|
+
# first argument, which will be either nil or the command name.
|
18
|
+
#
|
19
|
+
# @return [String] The last parsed argument.
|
20
|
+
attr_reader :last_argument
|
21
|
+
|
22
|
+
# Called when an option is parsed.
|
23
|
+
#
|
24
|
+
# @param [Symbol] key The option key (derived from the long format)
|
25
|
+
#
|
26
|
+
# @param value The option value
|
27
|
+
#
|
28
|
+
# @param [Cri::OptionParser] option_parser The option parser
|
29
|
+
#
|
30
|
+
# @return [void]
|
31
|
+
def option_added(key, value, option_parser)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Called when an argument is parsed.
|
35
|
+
#
|
36
|
+
# @param [String] argument The argument
|
37
|
+
#
|
38
|
+
# @param [Cri::OptionParser] option_parser The option parser
|
39
|
+
#
|
40
|
+
# @return [void]
|
41
|
+
def argument_added(argument, option_parser)
|
42
|
+
@last_argument = argument
|
43
|
+
option_parser.stop
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
|
48
|
+
# @todo Document
|
49
|
+
attr_accessor :supercommand
|
50
|
+
|
51
|
+
# @todo document
|
52
|
+
attr_accessor :commands
|
53
|
+
alias_method :subcommands, :commands
|
54
|
+
|
55
|
+
# @todo Document
|
56
|
+
attr_accessor :name
|
57
|
+
|
58
|
+
# @todo Document
|
59
|
+
attr_accessor :aliases
|
60
|
+
|
61
|
+
# @todo Document
|
62
|
+
attr_accessor :short_desc
|
8
63
|
|
9
|
-
#
|
10
|
-
|
11
|
-
|
12
|
-
|
64
|
+
# @todo Document
|
65
|
+
attr_accessor :long_desc
|
66
|
+
|
67
|
+
# @todo Document
|
68
|
+
attr_accessor :usage
|
69
|
+
|
70
|
+
# @todo Document
|
71
|
+
attr_accessor :option_definitions
|
72
|
+
|
73
|
+
# @todo Document
|
74
|
+
attr_accessor :block
|
75
|
+
|
76
|
+
# @todo Document
|
77
|
+
def self.define(string=nil, &block)
|
78
|
+
dsl = Cri::CommandDSL.new
|
79
|
+
if block
|
80
|
+
dsl.instance_eval(&block)
|
81
|
+
else
|
82
|
+
dsl.instance_eval(string)
|
83
|
+
end
|
84
|
+
dsl.command
|
85
|
+
end
|
86
|
+
|
87
|
+
# @todo Document
|
88
|
+
def self.new_basic_root
|
89
|
+
filename = File.dirname(__FILE__) + '/commands/basic_root.rb'
|
90
|
+
self.define(File.read(filename))
|
13
91
|
end
|
14
92
|
|
15
|
-
#
|
16
|
-
|
17
|
-
|
18
|
-
|
93
|
+
# @todo Document
|
94
|
+
def self.new_basic_help
|
95
|
+
filename = File.dirname(__FILE__) + '/commands/basic_help.rb'
|
96
|
+
self.define(File.read(filename))
|
19
97
|
end
|
20
98
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
raise NotImplementedError.new("Command subclasses should override #short_desc")
|
99
|
+
def initialize
|
100
|
+
@aliases = Set.new
|
101
|
+
@commands = Set.new # TODO make this a hash (name -> cmd)
|
102
|
+
@option_definitions = Set.new
|
26
103
|
end
|
27
104
|
|
28
|
-
#
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
105
|
+
# @todo Document
|
106
|
+
def modify(&block)
|
107
|
+
dsl = Cri::CommandDSL.new(self)
|
108
|
+
dsl.instance_eval(&block)
|
109
|
+
self
|
33
110
|
end
|
34
111
|
|
35
|
-
#
|
36
|
-
|
37
|
-
|
38
|
-
|
112
|
+
# @todo Document
|
113
|
+
def global_option_definitions
|
114
|
+
res = Set.new
|
115
|
+
res.merge(option_definitions)
|
116
|
+
res.merge(supercommand.global_option_definitions) if supercommand
|
117
|
+
res
|
39
118
|
end
|
40
119
|
|
41
|
-
#
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
def option_definitions
|
46
|
-
[]
|
120
|
+
# @todo Document
|
121
|
+
def add_command(command)
|
122
|
+
@commands << command
|
123
|
+
command.supercommand = self
|
47
124
|
end
|
48
125
|
|
49
|
-
#
|
50
|
-
|
126
|
+
# @todo Document
|
127
|
+
def define_command(name=nil, &block)
|
128
|
+
# Execute DSL
|
129
|
+
dsl = Cri::CommandDSL.new
|
130
|
+
dsl.name name unless name.nil?
|
131
|
+
dsl.instance_eval(&block)
|
132
|
+
|
133
|
+
# Create command
|
134
|
+
cmd = dsl.command
|
135
|
+
self.add_command(cmd)
|
136
|
+
cmd
|
137
|
+
end
|
138
|
+
|
139
|
+
# Returns the commands that could be referred to with the given name. If
|
140
|
+
# the result contains more than one command, the name is ambiguous.
|
51
141
|
#
|
52
|
-
#
|
53
|
-
|
54
|
-
|
142
|
+
# @todo Document
|
143
|
+
def commands_named(name)
|
144
|
+
# Find by exact name or alias
|
145
|
+
@commands.each do |cmd|
|
146
|
+
found = cmd.name == name || cmd.aliases.include?(name)
|
147
|
+
return [ cmd ] if found
|
148
|
+
end
|
149
|
+
|
150
|
+
# Find by approximation
|
151
|
+
@commands.select do |cmd|
|
152
|
+
cmd.name[0, name.length] == name
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
# Returns the command with the given name.
|
55
157
|
#
|
56
|
-
#
|
57
|
-
|
58
|
-
|
59
|
-
|
158
|
+
# @todo Document
|
159
|
+
def command_named(name)
|
160
|
+
commands = commands_named(name)
|
161
|
+
|
162
|
+
if commands.size < 1
|
163
|
+
$stderr.puts "#{self.name}: unknown command '#{name}'\n"
|
164
|
+
exit 1
|
165
|
+
elsif commands.size > 1
|
166
|
+
$stderr.puts "#{self.name}: '#{name}' is ambiguous:"
|
167
|
+
$stderr.puts " #{commands.map { |c| c.name }.join(' ') }"
|
168
|
+
exit 1
|
169
|
+
else
|
170
|
+
commands[0]
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
# @todo Document
|
175
|
+
def run(opts_and_args, parent_opts={})
|
176
|
+
if subcommands.empty?
|
177
|
+
# Parse
|
178
|
+
parser = Cri::OptionParser.new(
|
179
|
+
opts_and_args, global_option_definitions)
|
180
|
+
self.handle_parser_errors_while { parser.run }
|
181
|
+
local_opts = parser.options
|
182
|
+
global_opts = parent_opts.merge(parser.options)
|
183
|
+
args = parser.arguments
|
184
|
+
|
185
|
+
# Handle options
|
186
|
+
handle_options(local_opts)
|
187
|
+
|
188
|
+
# Execute
|
189
|
+
if @block.nil?
|
190
|
+
raise "No implementation available for '#{self.name}'"
|
191
|
+
end
|
192
|
+
self.instance_exec(global_opts, args, &block)
|
193
|
+
else
|
194
|
+
# Parse up to command name
|
195
|
+
stuff = partition(opts_and_args)
|
196
|
+
opts_before_cmd, cmd_name, opts_and_args_after_cmd = *stuff
|
197
|
+
|
198
|
+
# Handle options
|
199
|
+
handle_options(opts_before_cmd)
|
200
|
+
|
201
|
+
# Get command
|
202
|
+
if cmd_name.nil?
|
203
|
+
$stderr.puts "#{name}: no command given"
|
204
|
+
exit 1
|
205
|
+
end
|
206
|
+
command = command_named(cmd_name)
|
207
|
+
|
208
|
+
# Run
|
209
|
+
command.run(opts_and_args_after_cmd, opts_before_cmd)
|
210
|
+
end
|
60
211
|
end
|
61
212
|
|
62
|
-
#
|
213
|
+
# @return [String] The help text for this command
|
63
214
|
def help
|
64
215
|
text = ''
|
65
216
|
|
@@ -80,14 +231,32 @@ module Cri
|
|
80
231
|
text << "\n"
|
81
232
|
text << long_desc.wrap_and_indent(78, 4) + "\n"
|
82
233
|
|
234
|
+
# Append subcommands
|
235
|
+
unless self.commands.empty?
|
236
|
+
text << "\n"
|
237
|
+
text << (self.supercommand ? 'subcommands' : 'commands') << ":\n"
|
238
|
+
text << "\n"
|
239
|
+
length = self.commands.inject(0) { |m,c| [ m, c.name.size ].max }
|
240
|
+
self.commands.each do |cmd|
|
241
|
+
text << sprintf(" %-#{length+4}s %s\n",
|
242
|
+
cmd.name,
|
243
|
+
cmd.short_desc)
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
83
247
|
# Append options
|
84
|
-
|
85
|
-
unless
|
248
|
+
defs = global_option_definitions.sort { |x,y| x[:long] <=> y[:long] }
|
249
|
+
unless defs.empty?
|
86
250
|
text << "\n"
|
87
251
|
text << "options:\n"
|
88
252
|
text << "\n"
|
89
|
-
|
90
|
-
|
253
|
+
length = defs.inject(0) { |m,o| [ m, o[:long].size ].max }
|
254
|
+
defs.each do |opt_def|
|
255
|
+
text << sprintf(
|
256
|
+
" -%1s --%-#{length+4}s %s\n",
|
257
|
+
opt_def[:short],
|
258
|
+
opt_def[:long],
|
259
|
+
opt_def[:desc])
|
91
260
|
end
|
92
261
|
end
|
93
262
|
|
@@ -100,6 +269,44 @@ module Cri
|
|
100
269
|
self.name <=> other.name
|
101
270
|
end
|
102
271
|
|
272
|
+
protected
|
273
|
+
|
274
|
+
def handle_options(opts)
|
275
|
+
opts.each_pair do |key, value|
|
276
|
+
opt_def = global_option_definitions.find { |o| o[:long] == key.to_s }
|
277
|
+
block = opt_def[:block]
|
278
|
+
self.instance_exec(value, &block) if block
|
279
|
+
end
|
280
|
+
end
|
281
|
+
|
282
|
+
def partition(opts_and_args)
|
283
|
+
# Parse
|
284
|
+
delegate = Cri::Command::OptionParserPartitioningDelegate.new
|
285
|
+
parser = Cri::OptionParser.new(opts_and_args, global_option_definitions)
|
286
|
+
parser.delegate = delegate
|
287
|
+
self.handle_parser_errors_while { parser.run }
|
288
|
+
parser
|
289
|
+
|
290
|
+
# Extract
|
291
|
+
[
|
292
|
+
parser.options,
|
293
|
+
delegate.last_argument,
|
294
|
+
parser.unprocessed_arguments_and_options
|
295
|
+
]
|
296
|
+
end
|
297
|
+
|
298
|
+
def handle_parser_errors_while(&block)
|
299
|
+
begin
|
300
|
+
block.call
|
301
|
+
rescue Cri::OptionParser::IllegalOptionError => e
|
302
|
+
$stderr.puts "#{name}: illegal option -- #{e}"
|
303
|
+
exit 1
|
304
|
+
rescue Cri::OptionParser::OptionRequiresAnArgumentError => e
|
305
|
+
$stderr.puts "#{name}: option requires an argument -- #{e}"
|
306
|
+
exit 1
|
307
|
+
end
|
308
|
+
end
|
309
|
+
|
103
310
|
end
|
104
311
|
|
105
312
|
end
|