CommandLine 0.6.0

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.
@@ -0,0 +1,15 @@
1
+ # $Id$
2
+ # $Source$
3
+ #
4
+ # Author: Jim Freeze
5
+ # Copyright (c) 2005
6
+ #
7
+ # =DESCRIPTION
8
+ # Loader
9
+ #
10
+ # =Revision History
11
+ # Jim.Freeze 2005/06/17 Birthday
12
+ #
13
+
14
+ require 'commandline/utils'
15
+ require 'commandline/application'
@@ -0,0 +1,320 @@
1
+ # $Id$
2
+ # $Source$
3
+ #
4
+ # Author: Jim Freeze
5
+ # Copyright (c) 2005
6
+ #
7
+ # =DESCRIPTION
8
+ # Framework for commandline applications
9
+ #
10
+ # =Revision History
11
+ # Jim.Freeze 06/02/2005 Birthday - kinda
12
+ #
13
+
14
+ require 'commandline/utils'
15
+ require 'commandline/optionparser'
16
+
17
+ module CommandLine
18
+ class Application
19
+ class ApplicationError < StandardError; end
20
+ class OptionError < ApplicationError; end
21
+ class MissingMainError < ApplicationError; end
22
+ class InvalidArgumentArityError < ApplicationError; end
23
+ class ArgumentError < ApplicationError; end
24
+
25
+ param_accessor :version, :author, :copyright, :synopsis,
26
+ :short_description, :long_description,
27
+ :option_parser
28
+
29
+ #
30
+ # TODO: Consolidate these with OptionParser - put in command line
31
+ #
32
+ DEFAULT_CONSOLE_WIDTH = 70
33
+ MIN_CONSOLE_WIDTH = 10
34
+ DEFAULT_BODY_INDENT = 4
35
+
36
+ #def options
37
+ # raise(OptionError,
38
+ # "Options must be over-written with a valid (or empty) options list.")
39
+ #end
40
+
41
+ def initialize
42
+ # Ensure initializations have taken place
43
+ @arg_arity ||= [0,0]
44
+ @options ||= []
45
+ @arg_names ||= []
46
+ @option_parser ||= CommandLine::OptionParser.new(@options)
47
+ _init_format
48
+
49
+ if ARGV.empty? && [0,0] != @arg_arity
50
+ puts usage
51
+ exit(0)
52
+ end
53
+
54
+ @option_data = @option_parser.parse
55
+
56
+ validate_args(@option_data.args)
57
+ @arg_names.each_with_index { |name, idx|
58
+ instance_variable_set("@#{name}", @option_data.args[idx])
59
+ }
60
+ end
61
+
62
+ def _init_format
63
+ #
64
+ # Formatting defaults
65
+ #
66
+ console_width = ENV["COLUMNS"]
67
+ @columns =
68
+ if console_width.nil?
69
+ DEFAULT_CONSOLE_WIDTH
70
+ elsif console_width < MIN_CONSOLE_WIDTH
71
+ console_width
72
+ else
73
+ console_width - DEFAULT_BODY_INDENT
74
+ end
75
+ @body_indent = DEFAULT_BODY_INDENT
76
+ @tag_paragraph = false
77
+ @order = :index # | :alpha
78
+ end
79
+
80
+ def validate_args(od_args)
81
+ size = od_args.size
82
+ min, max = @arg_arity
83
+ max = 1.0/0.0 if -1 == max
84
+ raise(ArgumentError,
85
+ "Missing expected arguments. Found #{size} but expected #{min}.\n"+
86
+ "#{usage}") if size < min
87
+ raise(ArgumentError, "Too many arguments. Found #{size} but "+
88
+ "expected #{max}.\n#{usage}") if size > max
89
+ end
90
+
91
+ def option(*args)
92
+ @options ||= []
93
+ new_list = []
94
+ args.each { |arg|
95
+ new_list <<
96
+ case arg
97
+ when :help then _help
98
+ when :debug then _debug
99
+ when :verbose then _verbose
100
+ when :version then _version
101
+ else arg
102
+ end
103
+ }
104
+ #p new_list
105
+ @options << CommandLine::Option.new(*new_list)
106
+ end
107
+
108
+ #
109
+ # Args tells the application how many arguments (not belonging
110
+ # any option) are expected to be seen on the command line
111
+ # The names of the args are used for describing the synopsis (or usage).
112
+ # If there is an indeterminant amount of arguments, they are not
113
+ # named, but returned in an array.
114
+ # Many forms are valid. Some examples follow:
115
+ # args 0
116
+ # synopsis: Usage: app
117
+
118
+ # args :none
119
+ # synopsis: Usage: app
120
+
121
+ # args 1 #=> args is array
122
+ # synopsis: Usage: app arg
123
+
124
+ # args 2 #=> args is array
125
+ # synopsis: Usage: app arg1 arg2
126
+
127
+ # args 10 #=> args is array
128
+ # synopsis: Usage: app arg1 ... arg10
129
+
130
+ # args :file #=> @file = <arg>
131
+ # synopsis: Usage: app file
132
+
133
+ # args :file1, :file2 #=> @file1 = <arg1>, @file2 = <arg2>
134
+ # synopsis: Usage: app file1 file2
135
+
136
+ # args [0,1] #=> args is array
137
+ # synopsis: Usage: app [arg [arg]]
138
+
139
+ # args [2,3] #=> args is array
140
+ # synopsis: Usage: app arg[2,3]
141
+
142
+ # args [0,-1] #=> args is array
143
+ # synopsis: Usage: app [arg [arg...]]
144
+ #
145
+ def args(*expected_args)
146
+ @arg_names = []
147
+ case expected_args.size
148
+ when 0 then @arg_arity = [0,0]
149
+ when 1
150
+ case expected_args[0]
151
+ when Fixnum
152
+ v = expected_args[0]
153
+ @arg_arity = [v,v]
154
+ when Symbol
155
+ @arg_names = expected_args
156
+ @arg_arity = [1,1]
157
+ when Array
158
+ v = expected_args[0]
159
+ validate_arg_arity(v)
160
+ @arg_arity = v
161
+ else
162
+ raise(InvalidArgumentArityError,
163
+ "Args must be a Fixnum or Array: #{expected_args[0].inspect}.")
164
+ end
165
+ else
166
+ @arg_names = expected_args
167
+ size = expected_args.size
168
+ @arg_arity = [size, size]
169
+ end
170
+ end
171
+
172
+ def validate_arg_arity(arity)
173
+ min, max = *arity
174
+ raise(InvalidArgumentArityError, "Minimum argument arity '#{min}' must be "+
175
+ "greater than or equal to 0.") unless min >= 0
176
+ raise(InvalidArgumentArityError, "Maximum argument arity '#{max}' must be "+
177
+ "greater than or equal to -1.") if max < -1
178
+ raise(InvalidArgumentArityError, "Maximum argument arity '#{max}' must be "+
179
+ "greater than minimum arg_arity '#{min}'.") if max < min && max != -1
180
+ end
181
+
182
+ def usage
183
+ " Usage: #{name} #{synopsis}"
184
+ end
185
+
186
+ def man
187
+ require 'commandline/text/format'
188
+ f = Text::Format.new
189
+ f = Text::Format.new
190
+ f.columns = @columns
191
+ f.first_indent = 4
192
+ f.body_indent = @body_indent
193
+ f.tag_paragraph = false
194
+
195
+ s = []
196
+ s << ["NAME\n"]
197
+
198
+ nm = "#{short_description}".empty? ? name : "#{name} - #{short_description}"
199
+ s << f.format(nm)
200
+
201
+ sn = "#{synopsis}"
202
+ unless sn.empty?
203
+ s << "SYNOPSIS\n"
204
+ s << f.format(sn)
205
+ end
206
+
207
+ dc = "#{long_description}"
208
+ unless dc.empty?
209
+ s << "DESCRIPTION\n"
210
+ s << f.format(dc)
211
+ end
212
+
213
+ op = option_parser.to_s
214
+ unless op.empty?
215
+ s << option_parser.to_s
216
+ end
217
+
218
+ ar = "#{author}"
219
+ unless ar.empty?
220
+ s << "AUTHOR: #{ar}"
221
+ end
222
+
223
+
224
+ ct = "#{copyright}"
225
+ unless ct.empty?
226
+ s << ct
227
+ end
228
+
229
+ s.join("\n")
230
+ end
231
+ alias :help :man
232
+
233
+ def pathname
234
+ @@appname
235
+ end
236
+
237
+ def name
238
+ File.basename(pathname)
239
+ end
240
+
241
+ def get_arg
242
+ CommandLine::OptionParser::GET_ARGS
243
+ end
244
+ alias :get_args :get_arg
245
+
246
+ def append_arg
247
+ CommandLine::OptionParser::GET_ARG_ARRAY
248
+ end
249
+
250
+ def self.run
251
+ @@app.new.main if ($0 == @@appname)
252
+ rescue => err
253
+ puts "ERROR: #{err}"
254
+ exit(-1)
255
+ end
256
+
257
+ def self.inherited(klass)
258
+ @@appname = caller[0][/.*:/][0..-2]
259
+ @@app = klass
260
+ at_exit { @@app.run }
261
+ end
262
+
263
+ def main
264
+ #raise(MissingMainError, "Method #main must be defined in class #{@@app}.")
265
+ @@app.class_eval %{ def main; end }
266
+ end
267
+
268
+ def _help
269
+ {
270
+ :names => %w(--help -h),
271
+ :arg_arity => [0,0],
272
+ :opt_description => "Displays help page.",
273
+ :arg_description => "",
274
+ :opt_found => lambda { puts man; exit },
275
+ :opt_not_found => false
276
+ }
277
+ end
278
+
279
+ def _verbose
280
+ {
281
+ :names => %w(--verbose -v),
282
+ :arg_arity => [0,0],
283
+ :opt_description => "Sets verbosity level. Subsequent "+
284
+ "flags increase verbosity level",
285
+ :arg_description => "",
286
+ :opt_found => lambda { @verbose ||= -1; @verbose += 1 },
287
+ :opt_not_found => nil
288
+ }
289
+ end
290
+
291
+ def _version
292
+ {
293
+ :names => %w(--version -V),
294
+ :arg_arity => [0,0],
295
+ :opt_description => "Displays application version.",
296
+ :arg_description => "",
297
+ :opt_found => lambda {
298
+ begin
299
+ version
300
+ rescue
301
+ puts "No version specified"
302
+ end;
303
+ exit
304
+ },
305
+ :opt_not_found => nil
306
+ }
307
+ end
308
+
309
+ def _debug
310
+ {
311
+ :names => %w(--debug -d),
312
+ :arg_arity => [0,0],
313
+ :opt_description => "Sets debug to true.",
314
+ :arg_description => "",
315
+ :opt_found => lambda { $DEBUG = true }
316
+ }
317
+ end
318
+ end#class Application
319
+ end#module CommandLine
320
+
@@ -0,0 +1,16 @@
1
+ # $Id$
2
+ # $Source$
3
+ #
4
+ # Author: Jim Freeze
5
+ # Copyright (c) 2005
6
+ #
7
+ # =DESCRIPTION
8
+ # Loader
9
+ #
10
+ # =Revision History
11
+ # Jim.Freeze 2005/06/14 Birthday
12
+ #
13
+
14
+ require 'commandline/optionparser/option'
15
+ require 'commandline/optionparser/optionparser'
16
+ require 'commandline/optionparser/optiondata'
@@ -0,0 +1,180 @@
1
+ # $Id$
2
+ # $Source$
3
+ #
4
+ # Author: Jim Freeze
5
+ # Copyright (c) 2005 Jim Freeze
6
+ #
7
+ # =DESCRIPTION
8
+ # A very flexible commandline parser
9
+ #
10
+ # =Revision History
11
+ # Jim.Freeze 04/01/2005 Birthday
12
+ #
13
+
14
+ module CommandLine
15
+
16
+ class Option
17
+ class OptionError < StandardError; end
18
+ class InvalidOptionNameError < OptionError; end
19
+ class InvalidArgumentError < OptionError; end
20
+ class MissingOptionNameError < OptionError; end
21
+ class InvalidArgumentArityError < OptionError; end
22
+ class MissingPropertyError < OptionError; end
23
+ class InvalidPropertyError < OptionError; end
24
+ class InvalidConstructionError < OptionError; end
25
+
26
+ attr_accessor :posix
27
+
28
+ #
29
+ GENERAL_OPT_EQ_ARG_RE = /^(-{1,2}[a-zA-Z]+[-_a-zA-Z0-9]*)=(.*)$/ # :nodoc:
30
+ GNU_OPT_EQ_ARG_RE = /^(--[a-zA-Z]+[-_a-zA-Z0-9]*)=(.*)$/
31
+ #OPTION_RE = /^-{1,2}([a-zA-Z]+\w*)(.*)/
32
+ #UNIX_OPT_EQ_ARG_RE = /^(-[a-zA-Z])=(.*)$/
33
+ #UNIX_OPT_EQorSP_ARG_RE = /^(-[a-zA-Z])(=|\s+)(.*)$/
34
+
35
+ POSIX_OPTION_RE = /^-[a-zA-Z]?$/
36
+ # need to change this to support - and --
37
+ NON_POSIX_OPTION_RE = /^(-|-{1,2}[a-zA-Z_]+[-_a-zA-Z0-9]*)/
38
+
39
+ PROPERTIES = [ :arg_arity, :opt_description, :arg_description,
40
+ :opt_found, :opt_not_found, :posix
41
+ ]
42
+
43
+ FLAG_BASE_OPTS = {
44
+ :arg_arity => [0,0],
45
+ # :opt_description => nil,
46
+ :arg_description => "",
47
+ :opt_found => true,
48
+ :opt_not_found => false
49
+ }
50
+
51
+ # You get these without asking for them
52
+ DEFAULT_OPTS = {
53
+ :arg_arity => [1,1],
54
+ :opt_description => "",
55
+ :arg_description => "",
56
+ :opt_found => true,
57
+ :opt_not_found => false
58
+ }
59
+
60
+ #
61
+ # Option.new(:flag, :posix => true, :names => %w(--opt))
62
+ #
63
+ # TODO: Should we test and raise key is not one of :names, opt_description, ...
64
+ # This will prevent typos. Can users add properties to an Option that are their own?
65
+ def initialize(*all)
66
+ @posix = false
67
+
68
+ raise(MissingPropertyError,
69
+ "No properties specified for new #{self.class}.") if all.empty?
70
+
71
+ until Hash === all[0]
72
+ case (prop = all.shift)
73
+ when :flag then @flag = true
74
+ when :posix then @posix = true
75
+ else
76
+ raise(InvalidPropertyError, "Unknown option setting '#{prop}'.")
77
+ end
78
+ end
79
+
80
+ type = @flag.nil? ? :default : :flag
81
+ merge_hash =
82
+ case type
83
+ when :flag then FLAG_BASE_OPTS
84
+ when :default then DEFAULT_OPTS
85
+ else raise(InvalidConstructionError,
86
+ "Invalid arguments to Option.new. Must be a property hash with "+
87
+ "keys [:names, :arg_arity, :opt_description, :arg_description, "+
88
+ ":opt_found, :opt_not_found] or "+
89
+ "an option type [:flag, :default].")
90
+ end
91
+
92
+ @properties = {}.merge(merge_hash)
93
+ all.each { |properties|
94
+ raise(InvalidPropertyError,
95
+ "Don't understand argument of type '#{properties.class}' => "+
96
+ "#{properties.inspect} passed to #{self.class}.new. Looking "+
97
+ "for type Hash.") unless properties.kind_of?(Hash)
98
+
99
+ @properties.merge!(properties)
100
+ }
101
+
102
+ @properties[:names] = [@properties[:names]] unless
103
+ @properties[:names].kind_of?(Array)
104
+
105
+ arg_arity = @properties[:arg_arity]
106
+ @properties[:arg_arity] = [arg_arity, arg_arity] unless
107
+ arg_arity.kind_of?(Array)
108
+
109
+ raise "Invalid value for arg_arity '#{arg_arity}'." unless
110
+ arg_arity.kind_of?(Array) || arg_arity.kind_of?(Fixnum)
111
+
112
+ raise(InvalidArgumentArityError,
113
+ "Conflicting value given to new option: :flag "+
114
+ "and :arg_arity = #{@properties[:arg_arity].inspect}.") if
115
+ :flag == type && [0,0] != @properties[:arg_arity]
116
+
117
+ names = @properties[:names]
118
+ raise(MissingOptionNameError,
119
+ "Attempt to create an Option without :names defined.") if
120
+ names.nil? || names.empty?
121
+
122
+ names.each { |name| check_option_name(name) }
123
+ validate_arity @properties[:arg_arity]
124
+
125
+ create_opt_description if :flag == type
126
+ end
127
+
128
+ def create_opt_description
129
+ return if @properties.has_key?(:opt_description)
130
+ word = @properties[:names].grep(/^--\w.+/)
131
+ if word.empty?
132
+ @properties[:opt_description] = ""
133
+ else
134
+ @properties[:opt_description] = "Sets #{word.first[2..-1]} to true."
135
+ end
136
+ end
137
+
138
+ def check_option_name(name)
139
+ raise(InvalidOptionNameError,
140
+ "Option name '#{name}' contains invalid space.") if /\s+/.match(name)
141
+
142
+ if @posix
143
+ raise(InvalidOptionNameError,
144
+ "Option name '#{name}' is invalid.") unless POSIX_OPTION_RE.match(name)
145
+ else
146
+ raise(InvalidOptionNameError,
147
+ "Option name '#{name}' is invalid.") unless NON_POSIX_OPTION_RE.match(name)
148
+ end
149
+ end
150
+
151
+ def validate_arity(arity)
152
+ raise ":arg_arity is nil" if arity.nil?
153
+ min, max = *arity
154
+
155
+ raise(InvalidArgumentArityError, "Minimum argument arity '#{min}' must be "+
156
+ "greater than or equal to 0.") unless min >= 0
157
+ raise(InvalidArgumentArityError, "Maximum argument arity '#{max}' must be "+
158
+ "greater than or equal to -1.") if max < -1
159
+ raise(InvalidArgumentArityError, "Maximum argument arity '#{max}' must be "+
160
+ "greater than minimum arg_arity '#{min}'.") if max < min && max != -1
161
+ if @posix
162
+ raise(InvalidArgumentArityError, "Posix options only support :arg_arity "+
163
+ "of [0,0] or [1,1].") unless ([0,0] == arity) || ([1,1] == arity)
164
+ end
165
+ end
166
+
167
+ def method_missing(sym, *args)
168
+ raise "Unknown property '#{sym}' for option
169
+ #{@properties[:names].inspect unless @properties[:names].nil?}." unless
170
+ @properties.has_key?(sym) || PROPERTIES.include?(sym)
171
+ @properties[sym, *args]
172
+ end
173
+
174
+ def to_hash
175
+ Marshal.load(Marshal.dump(@properties))
176
+ end
177
+
178
+ end#class Option
179
+
180
+ end#module CommandLine