cri 1.0.0 → 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/NEWS ADDED
@@ -0,0 +1,9 @@
1
+ = Cri News
2
+
3
+ == 1.0.1
4
+
5
+ * Made gem actually include code. D'oh.
6
+
7
+ == 1.0.0
8
+
9
+ * Initial release!
data/Rakefile ADDED
@@ -0,0 +1,43 @@
1
+ ##### Requirements
2
+
3
+ # Rake etc
4
+ require 'rake'
5
+ require 'minitest/unit'
6
+
7
+ # Cri itself
8
+ $LOAD_PATH.unshift(File.expand_path(File.dirname(__FILE__) + '/lib'))
9
+ require 'cri'
10
+
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."
19
+
20
+ s.authors = [ 'Denis Defreyne' ]
21
+ s.email = "denis.defreyne@stoneship.org"
22
+
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`"
27
+ end
28
+
29
+ ##### Testing
30
+
31
+ desc 'Runs all tests'
32
+ task :test do
33
+ ENV['QUIET'] ||= 'true'
34
+
35
+ $LOAD_PATH.unshift(File.expand_path(File.dirname(__FILE__) + '/..'))
36
+
37
+ MiniTest::Unit.autorun
38
+
39
+ test_files = Dir['test/test_*.rb']
40
+ test_files.each { |f| require f }
41
+ end
42
+
43
+ task :default => :test
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 1.0.1
data/lib/cri.rb ADDED
@@ -0,0 +1,12 @@
1
+ module Cri
2
+
3
+ # The current Cri version.
4
+ VERSION = '1.0'
5
+
6
+ end
7
+
8
+ # Load Cri
9
+ require 'cri/base'
10
+ require 'cri/command'
11
+ require 'cri/core_ext'
12
+ require 'cri/option_parser'
data/lib/cri/base.rb ADDED
@@ -0,0 +1,153 @@
1
+ module Cri
2
+
3
+ # Cri::Base is the central class representing a commandline tool. It has a
4
+ # list of commands.
5
+ class Base
6
+
7
+ # The CLI's list of commands (should also contain the help command)
8
+ attr_reader :commands
9
+
10
+ # The CLI's help command (required)
11
+ attr_accessor :help_command
12
+
13
+ # Creates a new instance of the commandline tool.
14
+ def initialize(tool_name)
15
+ @tool_name = tool_name
16
+
17
+ @commands = []
18
+ end
19
+
20
+ # Parses the given commandline arguments and executes the requested
21
+ # command.
22
+ def run(args)
23
+ # Check arguments
24
+ if args.length == 0
25
+ @help_command.run([], [])
26
+ exit 1
27
+ end
28
+
29
+ # Partition options
30
+ opts_before_command = []
31
+ command_name = nil
32
+ opts_and_args_after_command = []
33
+ stage = 0
34
+ args.each do |arg|
35
+ # Update stage if necessary
36
+ stage = 1 if stage == 0 && !is_option?(arg)
37
+
38
+ # Add
39
+ opts_before_command << arg if stage == 0
40
+ command_name = arg if stage == 1
41
+ opts_and_args_after_command << arg if stage == 2
42
+
43
+ # Update stage if necessary
44
+ stage = 2 if stage == 1
45
+ end
46
+
47
+ # Handle options before command
48
+ begin
49
+ parsed_arguments = Cri::OptionParser.parse(opts_before_command, global_option_definitions)
50
+ rescue Cri::OptionParser::IllegalOptionError => e
51
+ $stderr.puts "illegal option -- #{e}"
52
+ exit 1
53
+ end
54
+ parsed_arguments[:options].keys.each do |option|
55
+ handle_option(option)
56
+ end
57
+
58
+ # Get command
59
+ if command_name.nil?
60
+ $stderr.puts "no command given"
61
+ exit 1
62
+ end
63
+ command = command_named(command_name)
64
+ if command.nil?
65
+ $stderr.puts "no such command: #{command_name}"
66
+ exit 1
67
+ end
68
+
69
+ # Parse arguments
70
+ option_definitions = command.option_definitions + global_option_definitions
71
+ begin
72
+ parsed_arguments = Cri::OptionParser.parse(opts_and_args_after_command, option_definitions)
73
+ rescue Cri::OptionParser::IllegalOptionError => e
74
+ $stderr.puts "illegal option -- #{e}"
75
+ exit 1
76
+ rescue Cri::OptionParser::OptionRequiresAnArgumentError => e
77
+ $stderr.puts "option requires an argument -- #{e}"
78
+ exit 1
79
+ end
80
+
81
+ # Handle global options
82
+ global_options = global_option_definitions.map { |o| o[:long] }
83
+ global_options.delete_if { |o| !parsed_arguments[:options].keys.include?(o.to_sym) }
84
+ global_options.each { |o| handle_option(o.to_sym) }
85
+
86
+ if parsed_arguments[:options].has_key?(:help)
87
+ # Show help for this command
88
+ show_help(command)
89
+ else
90
+ # Run command
91
+ command.run(parsed_arguments[:options], parsed_arguments[:arguments])
92
+ end
93
+ end
94
+
95
+ # Returns the command with the given name.
96
+ def command_named(name)
97
+ # Find by exact name or alias
98
+ command = @commands.find { |c| c.name == name or c.aliases.include?(name) }
99
+ return command unless command.nil?
100
+
101
+ # Find by approximation
102
+ commands = @commands.select { |c| c.name[0, name.length] == name }
103
+ if commands.length > 1
104
+ $stderr.puts "#{@tool_name}: '#{name}' is ambiguous:"
105
+ $stderr.puts " #{commands.map { |c| c.name }.join(' ') }"
106
+ exit 1
107
+ elsif commands.length == 0
108
+ $stderr.puts "#{@tool_name}: unknown command '#{name}'\n"
109
+ show_help
110
+ exit 1
111
+ else
112
+ return commands[0]
113
+ end
114
+ end
115
+
116
+ # Shows the help text for the given command, or shows the general help
117
+ # text if no command is given.
118
+ def show_help(command=nil)
119
+ if command.nil?
120
+ @help_command.run([], [])
121
+ else
122
+ @help_command.run([], [ command.name ])
123
+ end
124
+ end
125
+
126
+ # Returns the list of global option definitions.
127
+ def global_option_definitions
128
+ []
129
+ end
130
+
131
+ # Adds the given command to the list of commands. Adding a command will
132
+ # also cause the command's +base+ to be set to this instance.
133
+ def add_command(command)
134
+ @commands << command
135
+ command.base = self
136
+ end
137
+
138
+ # Handles the given option.
139
+ def handle_option(option)
140
+ false
141
+ end
142
+
143
+ private
144
+
145
+ # Returns true if the given string is an option (i.e. -foo or --foo),
146
+ # false otherwise.
147
+ def is_option?(string)
148
+ string =~ /^-/
149
+ end
150
+
151
+ end
152
+
153
+ end
@@ -0,0 +1,105 @@
1
+ module Cri
2
+
3
+ # Cri::Command represents a command that can be executed on the commandline.
4
+ # It is an abstract superclass for all commands.
5
+ class Command
6
+
7
+ attr_accessor :base
8
+
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")
13
+ end
14
+
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")
19
+ end
20
+
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")
26
+ end
27
+
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")
33
+ end
34
+
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")
39
+ end
40
+
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
+ []
47
+ end
48
+
49
+ # Executes the command. Subclasses must implement this method
50
+ # (obviously... what's the point of a command that can't be run?).
51
+ #
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.
55
+ #
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")
60
+ end
61
+
62
+ # Returns the help text for this command.
63
+ def help
64
+ text = ''
65
+
66
+ # Append usage
67
+ text << usage + "\n"
68
+
69
+ # Append aliases
70
+ unless aliases.empty?
71
+ text << "\n"
72
+ text << "aliases: #{aliases.join(' ')}\n"
73
+ end
74
+
75
+ # Append short description
76
+ text << "\n"
77
+ text << short_desc + "\n"
78
+
79
+ # Append long description
80
+ text << "\n"
81
+ text << long_desc.wrap_and_indent(78, 4) + "\n"
82
+
83
+ # Append options
84
+ all_option_definitions = base.global_option_definitions + option_definitions
85
+ unless all_option_definitions.empty?
86
+ text << "\n"
87
+ text << "options:\n"
88
+ 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])
91
+ end
92
+ end
93
+
94
+ # Return text
95
+ text
96
+ end
97
+
98
+ # Compares this command's name to the other given command's name.
99
+ def <=>(other)
100
+ self.name <=> other.name
101
+ end
102
+
103
+ end
104
+
105
+ end
@@ -0,0 +1,8 @@
1
+ module Cri::CoreExtensions
2
+ end
3
+
4
+ require 'cri/core_ext/string'
5
+
6
+ class String
7
+ include Cri::CoreExtensions::String
8
+ end
@@ -0,0 +1,41 @@
1
+ module Cri::CoreExtensions
2
+
3
+ module String
4
+
5
+ # Word-wraps and indents the string.
6
+ #
7
+ # +width+:: The maximal width of each line. This also includes indentation,
8
+ # i.e. the actual maximal width of the text is width-indentation.
9
+ #
10
+ # +indentation+:: The number of spaces to indent each wrapped line.
11
+ def wrap_and_indent(width, indentation)
12
+ # Split into paragraphs
13
+ paragraphs = self.split("\n").map { |p| p.strip }.reject { |p| p == '' }
14
+
15
+ # Wrap and indent each paragraph
16
+ paragraphs.map do |paragraph|
17
+ # Initialize
18
+ lines = []
19
+ line = ''
20
+
21
+ # Split into words
22
+ paragraph.split(/\s/).each do |word|
23
+ # Begin new line if it's too long
24
+ if (line + ' ' + word).length >= width
25
+ lines << line
26
+ line = ''
27
+ end
28
+
29
+ # Add word to line
30
+ line += (line == '' ? '' : ' ' ) + word
31
+ end
32
+ lines << line
33
+
34
+ # Join lines
35
+ lines.map { |l| ' '*indentation + l }.join("\n")
36
+ end.join("\n\n")
37
+ end
38
+
39
+ end
40
+
41
+ end
@@ -0,0 +1,186 @@
1
+ module Cri
2
+
3
+ # Cri::OptionParser is used for parsing commandline options.
4
+ class OptionParser
5
+
6
+ # Error that will be raised when an unknown option is encountered.
7
+ class IllegalOptionError < RuntimeError ; end
8
+
9
+ # Error that will be raised when an option without argument is
10
+ # encountered.
11
+ class OptionRequiresAnArgumentError < RuntimeError ; end
12
+
13
+ # Parses the commandline arguments in +arguments_and_options+, using the
14
+ # commandline option definitions in +definitions+.
15
+ #
16
+ # +arguments_and_options+ is an array of commandline arguments and
17
+ # options. This will usually be +ARGV+.
18
+ #
19
+ # +definitions+ contains a list of hashes defining which options are
20
+ # allowed and how they will be handled. Such a hash has three keys:
21
+ #
22
+ # :short:: The short name of the option, e.g. +a+. Do not include the '-'
23
+ # prefix.
24
+ #
25
+ # :long:: The long name of the option, e.g. +all+. Do not include the '--'
26
+ # prefix.
27
+ #
28
+ # :argument:: Whether this option's argument is required (:required),
29
+ # optional (:optional) or forbidden (:forbidden).
30
+ #
31
+ # A sample array of definition hashes could look like this:
32
+ #
33
+ # [
34
+ # { :short => 'a', :long => 'all', :argument => :forbidden },
35
+ # { :short => 'p', :long => 'port', :argument => :required },
36
+ # ]
37
+ #
38
+ # During parsing, two errors can be raised:
39
+ #
40
+ # IllegalOptionError:: An unrecognised option was encountered, i.e. an
41
+ # option that is not present in the list of option
42
+ # definitions.
43
+ #
44
+ # OptionRequiresAnArgumentError:: An option was found that did not have a
45
+ # value, even though this value was
46
+ # required.
47
+ #
48
+ # What will be returned, is a hash with two keys, :arguments and :options.
49
+ # The :arguments value contains a list of arguments, and the :options
50
+ # value contains a hash with key-value pairs for each option. Options
51
+ # without values will have a +nil+ value instead.
52
+ #
53
+ # For example, the following commandline options (which should not be
54
+ # passed as a string, but as an array of strings):
55
+ #
56
+ # foo -xyz -a hiss -s -m please --level 50 --father=ani -n luke squeak
57
+ #
58
+ # with the following option definitions:
59
+ #
60
+ # [
61
+ # { :short => 'x', :long => 'xxx', :argument => :forbidden },
62
+ # { :short => 'y', :long => 'yyy', :argument => :forbidden },
63
+ # { :short => 'z', :long => 'zzz', :argument => :forbidden },
64
+ # { :short => 'a', :long => 'all', :argument => :forbidden },
65
+ # { :short => 's', :long => 'stuff', :argument => :optional },
66
+ # { :short => 'm', :long => 'more', :argument => :optional },
67
+ # { :short => 'l', :long => 'level', :argument => :required },
68
+ # { :short => 'f', :long => 'father', :argument => :required },
69
+ # { :short => 'n', :long => 'name', :argument => :required }
70
+ # ]
71
+ #
72
+ # will be translated into:
73
+ #
74
+ # {
75
+ # :arguments => [ 'foo', 'hiss', 'squeak' ],
76
+ # :options => {
77
+ # :xxx => true,
78
+ # :yyy => true,
79
+ # :zzz => true,
80
+ # :all => true,
81
+ # :stuff => true,
82
+ # :more => 'please',
83
+ # :level => '50',
84
+ # :father => 'ani',
85
+ # :name => 'luke'
86
+ # }
87
+ # }
88
+ def self.parse(arguments_and_options, definitions)
89
+ # Don't touch original argument
90
+ unprocessed_arguments_and_options = arguments_and_options.dup
91
+
92
+ # Initialize
93
+ arguments = []
94
+ options = {}
95
+
96
+ # Determines whether we've passed the '--' marker or not
97
+ no_more_options = false
98
+
99
+ loop do
100
+ # Get next item
101
+ e = unprocessed_arguments_and_options.shift
102
+ break if e.nil?
103
+
104
+ # Handle end-of-options marker
105
+ if e == '--'
106
+ no_more_options = true
107
+ # Handle incomplete options
108
+ elsif e =~ /^--./ and !no_more_options
109
+ # Get option key, and option value if included
110
+ if e =~ /^--([^=]+)=(.+)$/
111
+ option_key = $1
112
+ option_value = $2
113
+ else
114
+ option_key = e[2..-1]
115
+ option_value = nil
116
+ end
117
+
118
+ # Find definition
119
+ definition = definitions.find { |d| d[:long] == option_key }
120
+ raise IllegalOptionError.new(option_key) if definition.nil?
121
+
122
+ if [ :required, :optional ].include?(definition[:argument])
123
+ # Get option value if necessary
124
+ if option_value.nil?
125
+ option_value = unprocessed_arguments_and_options.shift
126
+ if option_value.nil? || option_value =~ /^-/
127
+ if definition[:argument] == :required
128
+ raise OptionRequiresAnArgumentError.new(option_key)
129
+ else
130
+ unprocessed_arguments_and_options.unshift(option_value)
131
+ option_value = true
132
+ end
133
+ end
134
+ end
135
+
136
+ # Store option
137
+ options[definition[:long].to_sym] = option_value
138
+ else
139
+ # Store option
140
+ options[definition[:long].to_sym] = true
141
+ end
142
+ # Handle -xyz options
143
+ elsif e =~ /^-./ and !no_more_options
144
+ # Get option keys
145
+ option_keys = e[1..-1].scan(/./)
146
+
147
+ # For each key
148
+ option_keys.each do |option_key|
149
+ # Find definition
150
+ definition = definitions.find { |d| d[:short] == option_key }
151
+ raise IllegalOptionError.new(option_key) if definition.nil?
152
+
153
+ if option_keys.length > 1 and definition[:argument] == :required
154
+ # This is a combined option and it requires an argument, so complain
155
+ raise OptionRequiresAnArgumentError.new(option_key)
156
+ elsif [ :required, :optional ].include?(definition[:argument])
157
+ # Get option value
158
+ option_value = unprocessed_arguments_and_options.shift
159
+ if option_value.nil? || option_value =~ /^-/
160
+ if definition[:argument] == :required
161
+ raise OptionRequiresAnArgumentError.new(option_key)
162
+ else
163
+ unprocessed_arguments_and_options.unshift(option_value)
164
+ option_value = true
165
+ end
166
+ end
167
+
168
+ # Store option
169
+ options[definition[:long].to_sym] = option_value
170
+ else
171
+ # Store option
172
+ options[definition[:long].to_sym] = true
173
+ end
174
+ end
175
+ # Handle normal arguments
176
+ else
177
+ arguments << e
178
+ end
179
+ end
180
+
181
+ { :options => options, :arguments => arguments }
182
+ end
183
+
184
+ end
185
+
186
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cri
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Denis Defreyne
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-11-01 00:00:00 +01:00
12
+ date: 2009-11-02 00:00:00 +01:00
13
13
  default_executable:
14
14
  dependencies: []
15
15
 
@@ -26,7 +26,16 @@ extra_rdoc_files:
26
26
  files:
27
27
  - ChangeLog
28
28
  - LICENSE
29
+ - NEWS
29
30
  - README
31
+ - Rakefile
32
+ - VERSION
33
+ - lib/cri.rb
34
+ - lib/cri/base.rb
35
+ - lib/cri/command.rb
36
+ - lib/cri/core_ext.rb
37
+ - lib/cri/core_ext/string.rb
38
+ - lib/cri/option_parser.rb
30
39
  has_rdoc: true
31
40
  homepage:
32
41
  licenses: []