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