cri 1.0.1 → 2.0a1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/.gemtest ADDED
File without changes
data/NEWS.md ADDED
@@ -0,0 +1,18 @@
1
+ Cri News
2
+ ========
3
+
4
+ 2.0
5
+ ---
6
+
7
+ * Added DSL
8
+ * Added support for nested commands
9
+
10
+ 1.0.1
11
+ -----
12
+
13
+ * Made gem actually include code. D'oh.
14
+
15
+ 1.0.0
16
+ -----
17
+
18
+ * Initial release!
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
- ##### Packaging
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
- s.authors = [ 'Denis Defreyne' ]
21
- s.email = "denis.defreyne@stoneship.org"
15
+ require 'yard'
22
16
 
23
- s.files = FileList['[A-Z]*', 'lib/**/*']
24
- end
25
- rescue LoadError
26
- warn "Jeweler (or a dependency) is not available. Install it with `gem install jeweler`"
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 = '1.0'
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
- # Load Cri
9
- require 'cri/base'
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 an abstract superclass for all commands.
6
+ # It is also used for the commandline tool itself.
5
7
  class Command
6
8
 
7
- attr_accessor :base
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
- # Returns a string containing the name of thi command. Subclasses must
10
- # implement this method.
11
- def name
12
- raise NotImplementedError.new("Command subclasses should override #name")
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
- # Returns an array of strings containing the aliases for this command.
16
- # Subclasses must implement this method.
17
- def aliases
18
- raise NotImplementedError.new("Command subclasses should override #aliases")
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
- # Returns a string containing this command's short description, which
22
- # should not be longer than 50 characters. Subclasses must implement this
23
- # method.
24
- def short_desc
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
- # Returns a string containing this command's complete description, which
29
- # should explain what this command does and how it works in detail.
30
- # Subclasses must implement this method.
31
- def long_desc
32
- raise NotImplementedError.new("Command subclasses should override #long_desc")
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
- # Returns a string containing this command's usage. Subclasses must
36
- # implement this method.
37
- def usage
38
- raise NotImplementedError.new("Command subclasses should override #usage")
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
- # Returns an array containing this command's option definitions. See the
42
- # documentation for Cri::OptionParser for details on what option
43
- # definitions look like. Subclasses may implement this method if the
44
- # command has options.
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
- # Executes the command. Subclasses must implement this method
50
- # (obviously... what's the point of a command that can't be run?).
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
- # +options+:: A hash containing the parsed commandline options. For
53
- # example, '--foo=bar' will be converted into { :foo => 'bar'
54
- # }. See the Cri::OptionParser documentation for details.
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
- # +arguments+:: An array of strings representing the commandline arguments
57
- # given to this command.
58
- def run(options, arguments)
59
- raise NotImplementedError.new("Command subclasses should override #run")
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
- # Returns the help text for this command.
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
- all_option_definitions = base.global_option_definitions + option_definitions
85
- unless all_option_definitions.empty?
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
- all_option_definitions.sort { |x,y| x[:long] <=> y[:long] }.each do |opt_def|
90
- text << sprintf(" -%1s --%-10s %s\n", opt_def[:short], opt_def[:long], opt_def[:desc])
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