CommandLine 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +31 -0
- data/README +380 -0
- data/docs/index.html +1005 -0
- data/lib/commandline.rb +15 -0
- data/lib/commandline/application.rb +320 -0
- data/lib/commandline/optionparser.rb +16 -0
- data/lib/commandline/optionparser/option.rb +180 -0
- data/lib/commandline/optionparser/optiondata.rb +54 -0
- data/lib/commandline/optionparser/optionparser.rb +521 -0
- data/lib/commandline/text/format.rb +1451 -0
- data/lib/commandline/utils.rb +12 -0
- data/lib/open4.rb +79 -0
- data/lib/test/unit/systemtest.rb +58 -0
- metadata +57 -0
data/lib/commandline.rb
ADDED
@@ -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
|