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 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