optconfig 0.4.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (2) hide show
  1. data/lib/optconfig.rb +529 -0
  2. metadata +64 -0
data/lib/optconfig.rb ADDED
@@ -0,0 +1,529 @@
1
+ # $Id$
2
+ # Copyright (C) 2004-2009 TOMITA Masahiro
3
+ # mailto:tommy@tmtm.org
4
+ #
5
+ # = OptConfig
6
+ # Author:: TOMITA Masahiro <tommy@tmtm.org>
7
+ # License:: Ruby's. see http://www.ruby-lang.org/en/LICENSE.txt
8
+ #
9
+ # * OptConfig はコマンドラインオプションのパーサです。
10
+ # * ファイルからオプションを読み込むこともできます。
11
+ # * -s 形式と --long-option 形式の両方を扱うことができます。
12
+ # * 長い形式のオプションは曖昧でなければ補完されます。
13
+ # * usage に使用する文字列を自動的に生成します。
14
+ # * オプションの引数の形式を指定できます。
15
+ #
16
+ # == Download
17
+ # * http://rubyforge.org/frs/?group_id=4777
18
+ # * http://tmtm.org/downloads/ruby/optconfig/
19
+ #
20
+ # == Required
21
+ # * StringValidator http://stringvalidator.rubyforge.org/
22
+ #
23
+ # == Install
24
+ # $ make
25
+ # # make install
26
+ #
27
+ # == Usage
28
+ # require "optconfig"
29
+ # opt = OptConfig.new
30
+ # opt.option "p", "port=num", :format=>1..65535, :default=>110, :description=>"port number (%s)"
31
+ # opt.option "h", "hostname=name", :format=>true, :default=>"localhost", :description=>"hostname (%s)"
32
+ # opt.option "hogehoge", :description=>"enable hogehoge flag"
33
+ #
34
+ # argv = %w[-p 12345 -h 192.168.1.1 arg1 arg2]
35
+ # opt.parse argv # => ["arg1", "arg2"]
36
+ # opt["p"] # => 12345
37
+ # opt["port"] # => 12345 (same as opt["p"])
38
+ # opt["h"] # => "192.168.1.1"
39
+ # opt["hogehoge"] # => nil (means not set)
40
+ # opt["x"] # raise OptConfig::UnknownOption
41
+ # argv # => ["-p", "12345", "-h", "192.168.1.1", "arg1", "arg2"]
42
+ # opt.parse! argv # => ["arg1", "arg2"]
43
+ # argv # => ["arg1", "arg2"]
44
+ #
45
+ # opt.usage は次の文字列を生成します。
46
+ # -p, --port=num port number (12345)
47
+ # -h, --hostname=name hostname (192.168.1.1)
48
+ # --hogehoge enable hogehoge flag
49
+ #
50
+ # === オプション定義
51
+ # OptConfig#option でオプションを定義します。
52
+ # 引数は、オプション名のリストと、オプションの属性を表す Hash です。
53
+ # [オプション名]
54
+ # 1文字の英数字、または2文字以上の英数字と「-」「_」。
55
+ # 同じオプションに複数の名前をつける場合は複数指定します。
56
+ # "long-name=val" の形式で指定すると :argument 属性が true になります。
57
+ # "long-name[=val]" の形式で指定すると :argument 属性が :optional になります。
58
+ #
59
+ # オプション属性(Hash)のキーは以下の通りです。
60
+ # [:argument]
61
+ # オプションが引数を取るかどうか。
62
+ # nil :: format が真の場合は引数必須、偽の場合は引数不要 (デフォルト)。
63
+ # true :: 引数必須。
64
+ # false :: 引数不要。
65
+ # :optional :: 引数を省略可能。省略時は OptConfig#[] は true になる。
66
+ # [:format]
67
+ # オプション引数の形式。
68
+ # true :: 任意。
69
+ # false/nil :: オプションが引数を必要としない (デフォルト)。
70
+ # :boolean :: オプション引数が "1", "true", "enable", "yes", "y", "on" で true、"0", "false", "disable", "no", "n", "off" で false を返す。
71
+ # その他 :: StringValidator の rule オブジェクトとみなして引数の形式をチェックする。
72
+ # [:default]
73
+ # オプションが指定されなかった場合のデフォルト値。
74
+ # [:description]
75
+ # オプションの説明文字列。説明は usage() で出力されます。
76
+ # nil の場合は usage() でオプションについて出力されません。
77
+ # 文字列中の %s はオプションの値に置換されます。
78
+ # [:multiple]
79
+ # 複数指定された場合の振る舞い。
80
+ # true :: 複数指定可能。OptConfig#[] は配列を返す。
81
+ # false/nil :: 複数指定された場合はエラー。
82
+ # :last :: 最後に指定されたものが有効(デフォルト)。
83
+ # [:completion]
84
+ # 長いオプションを補完するかどうか (デフォルト: true)。
85
+ # [:underscore_is_hyphen]
86
+ # アンダースコアをハイフンとみなすかどうか (デフォルト: nil)
87
+ # [:in_config]
88
+ # オプションファイル内に記述可能かどうか (デフォルト: true)。
89
+ # [:proc]
90
+ # parse() 時にオプションを見つける度に実行される Proc オブジェクト。
91
+ # オプション引数の正当性を確認した後に実行されます。
92
+ # ブロック引数は、オプション名, Option オブジェクト, オプション引数です。
93
+ # ブロックの評価結果は OptConfig#[] の戻り値として使用されます。
94
+ # [:pre_proc]
95
+ # :proc と同じですが、オプション引数の正当性の確認前に実行されます。
96
+ # ブロック引数は、オプション名, Option オブジェクト, オプション引数です。
97
+ # ブロックの評価結果はオプション引数として使用されます。
98
+ #
99
+ # === オプションファイル
100
+ # parse よりも前に file= でファイル名を指定するか、OptConfig.new 時に
101
+ # :file 属性を指定すると、そのファイルからオプションを読み込みます。ファ
102
+ # イルで指定されたオプションよりも、parse の引数で指定されたオプション
103
+ # の方が優先度が高いです。
104
+ #
105
+ # オプションファイルの形式は次の通りです:
106
+ # option-name = value
107
+ #
108
+ # option-name には1文字のオプション名は指定できません。
109
+ #
110
+ # 「=」の前後の空白はあってもなくても構いません。「=」は省略可能です。
111
+ # その場合は、オプション名と値との間に一つ以上の空白が必要です。
112
+ #
113
+ # 「#」で始まる行はコメントとみなされます。空行も無視されます。
114
+ # 指定できるオプションは長い形式のオプションだけです。
115
+ #
116
+ # ファイル中に「[_section_name_]」という行を置くと、その行以降がセクショ
117
+ # ンとして扱われます。OptConfig#section= でセクション名を指定すると、指
118
+ # 定したセクションのオプションのみが読み込まれます。OptConfig#section=
119
+ # に配列を設定すると、複数のセクションから読み込みます。
120
+ # OptConfig#section を設定しない場合は、すべてのセクションからオプショ
121
+ # ンを読み込みます。
122
+
123
+ require "stringvalidator"
124
+
125
+ class OptConfig
126
+ class Error < StandardError; end
127
+ # 未知のオプション
128
+ class UnknownOption < Error; end
129
+ # オプションに引数が必要
130
+ class ArgumentRequired < Error; end
131
+ # オプションの引数が不正
132
+ class InvalidArgument < Error; end
133
+ # オプション名が曖昧
134
+ class AmbiguousOption < Error; end
135
+ # オプションに不要な引数が指定された
136
+ class UnnecessaryArgument < Error; end
137
+ # 同じオプションが複数指定された
138
+ class DuplicatedOption < Error; end
139
+
140
+ # == 初期化
141
+ # default_attr には各オプション属性のデフォルト値を Hash で指定可能。
142
+ # オプション属性以外にも以下のものを指定できる。これらは OptConfig オブジェクト自身に影響する。
143
+ # :file :: オプションファイル名 (String)。デフォルト: なし。
144
+ # :section :: オプションファイル名のセクション名 (String または String の配列)。デフォルト: なし。
145
+ # :ignore_unknown_file_option :: オプションファイル内に未知のオプションがあっtた時に無視するか(true)エラーにするか(false)。デフォルト: true。
146
+ # :stop_at_non_option_argument :: オプションでない引数でオプションの解釈をやめるか(true)、それ以降もオプションの解釈を続けるか(false)。デフォルト: false。
147
+ def initialize(default_attr={})
148
+ @default_attr = default_attr
149
+ @option_seq = []
150
+ @options = {}
151
+ @file = default_attr[:file]
152
+ @section = default_attr[:section]
153
+ @stop_at_non_option_argument = default_attr[:stop_at_non_option_argument]
154
+ @ignore_unknown_file_option = default_attr.key?(:ignore_unknown_file_option) ? default_attr[:ignore_unknown_file_option] : true
155
+ @obsolete_behavior = false
156
+ @specified = {} # オプションが指定されたかどうかを保持
157
+ end
158
+ attr_accessor :file, :section, :ignore_unknown_file_option
159
+
160
+ alias idlist section
161
+ alias idlist= section=
162
+
163
+ # == オプション定義
164
+ # args:: オプション名(String) のリスト、オプションの属性(Hash)
165
+ # === 戻り値
166
+ # Option オブジェクト
167
+ # === 例外
168
+ # RuntimeError:: オプションが既に定義されている
169
+ def option(*args)
170
+ args = args.dup
171
+ if args.last.is_a? Hash
172
+ attr = args.pop
173
+ attr = @default_attr.merge attr
174
+ else
175
+ attr = @default_attr
176
+ end
177
+ args.push attr
178
+ opt = Option.new *args
179
+ opt.name.each do |n|
180
+ raise "option #{n} is already defined" if @options.key? n
181
+ @options[n] = opt
182
+ end
183
+ @option_seq << opt
184
+ end
185
+
186
+ # == オプション定義(古いインタフェース)
187
+ # option:: オプション定義(ハッシュ)
188
+ def options=(option)
189
+ @options.clear
190
+ @option_seq.clear
191
+ option.each do |k,v|
192
+ v = [v] unless v.is_a? Array
193
+ arg = k.to_a
194
+ arg.push({:format=>v[0], :default=>v[1]})
195
+ opt = Option.new *arg
196
+ opt.name.each do |n|
197
+ raise "option #{n} is already defined" if @options.key? n
198
+ @options[n] = opt
199
+ end
200
+ @option_seq << opt
201
+ end
202
+ @obsolete_behavior = true
203
+ option
204
+ end
205
+
206
+ # == argv のオプションを解析し、オプションを取り除いたものに置き換える
207
+ # argv:: 配列
208
+ # === 戻り値
209
+ # argv:: 残りの引数
210
+ def parse!(argv=[])
211
+ orig_argv_size = argv.size
212
+ ret = []
213
+ @specified.clear
214
+ @option_seq.each do |opt|
215
+ opt.value = opt.ovalue = opt.default
216
+ begin
217
+ opt.value = check_option opt.name.first, opt.default if opt.default.is_a? String
218
+ rescue OptConfig::Error
219
+ # 無視
220
+ end
221
+ end
222
+ parse_file @file if @file
223
+ @specified.clear
224
+
225
+ until argv.empty?
226
+ arg = argv.shift
227
+ case arg
228
+ when "--"
229
+ ret.concat argv
230
+ break
231
+ when /\A--[a-zA-Z0-9_]/
232
+ parse_long_opt arg.sub(/\A--/, ""), argv
233
+ when /\A-[a-zA-Z0-9]/
234
+ parse_short_opt arg.sub(/\A-/, ""), argv
235
+ else
236
+ ret.push arg
237
+ if @stop_at_non_option_argument
238
+ ret.concat argv
239
+ break
240
+ end
241
+ end
242
+ end
243
+
244
+ if @obsolete_behavior
245
+ n = orig_argv_size - ret.size
246
+ argv.replace ret
247
+ return n
248
+ end
249
+ argv.replace ret
250
+ return argv
251
+ end
252
+
253
+ # == argv のオプションを解析する
254
+ # argv:: 文字列の配列
255
+ # === 戻り値
256
+ # argv からオプションを取り除いたもの
257
+ def parse(argv=[])
258
+ parse!(argv.dup)
259
+ end
260
+
261
+ # == ファイルからオプションを読み込む
262
+ # filename:: ファイル名
263
+ # === 例外
264
+ # UnknownOption
265
+ def parse_file(filename)
266
+ cur_sect = nil
267
+ File.open filename do |f|
268
+ f.each_line do |line|
269
+ line.chomp!
270
+ next if line =~ /\A#|\A\s*\z/
271
+ if line =~ /\A\[(.*)\]\z/
272
+ cur_sect = $1
273
+ next
274
+ end
275
+ if @section.nil? or @section.empty? or @section.to_a.include? cur_sect
276
+ name, value = line.split(/\s*=\s*|\s+/, 2)
277
+ begin
278
+ name, = long_option name, false
279
+ raise UnknownOption, "unknown option: #{name}" unless @options[name].in_config
280
+ if value
281
+ parse_long_opt "#{name}=#{value}", [], false
282
+ else
283
+ parse_long_opt name, [], false
284
+ end
285
+ rescue UnknownOption
286
+ raise unless @ignore_unknown_file_option
287
+ end
288
+ end
289
+ end
290
+ end
291
+ end
292
+
293
+ # == オプションの値を返す
294
+ # name:: オプション名
295
+ # === 例外
296
+ # UnknownOption
297
+ def [](name)
298
+ raise UnknownOption, "unknown option: #{name}" unless @options.key? name
299
+ @options[name].value
300
+ end
301
+
302
+ # == オプションの説明文字列を返す
303
+ def usage
304
+ ret = ""
305
+ @option_seq.each do |opt|
306
+ next unless opt.description
307
+ short = []
308
+ long = []
309
+ opt.usage_name.each do |n|
310
+ if n.size == 1
311
+ short << "-#{n}"
312
+ else
313
+ long << "--#{n}"
314
+ end
315
+ end
316
+ line = " "+(short+long).join(", ")
317
+ if opt.description.empty?
318
+ ret << line+"\n"
319
+ next
320
+ end
321
+ if line.length >= 25
322
+ line << "\n "
323
+ else
324
+ line << " "*(26-line.length)
325
+ end
326
+ desc = opt.description.gsub(/%s/, opt.ovalue.to_s).split "\n"
327
+ line << desc.shift+"\n"
328
+ desc.each do |d|
329
+ line << " #{d}\n"
330
+ end
331
+ ret << line
332
+ end
333
+ ret
334
+ end
335
+
336
+ private
337
+
338
+ # == オプションに値を設定
339
+ # name:: オプション名
340
+ # value:: 値(String,true,false)
341
+ # === 例外
342
+ # UnknownOption, DuplicatedOption
343
+ def set_option(name, value)
344
+ raise UnknownOption, "unknown option: #{name}" unless @options.key? name
345
+ opt = @options[name]
346
+ ovalue = value
347
+ value = opt.pre_proc.call name, opt, value if opt.pre_proc
348
+ raise DuplicatedOption, "duplicated option: #{name}" if !opt.multiple and @specified[@options[name]]
349
+ value = check_option name, value unless value == true or value == false
350
+ value = opt.proc.call name, opt, value if opt.proc
351
+ if opt.multiple == :last or !opt.multiple
352
+ opt.value = value
353
+ opt.ovalue = ovalue
354
+ else
355
+ unless @specified[@options[name]]
356
+ opt.value = []
357
+ opt.ovalue = []
358
+ end
359
+ opt.value << value
360
+ opt.ovalue << ovalue
361
+ end
362
+ @specified[@options[name]] = true
363
+ end
364
+
365
+ # == オプション名と値の正当性を確認
366
+ # name:: オプション名
367
+ # value:: 値 (String)
368
+ # === 戻り値
369
+ # _value_ の評価結果
370
+ # === 例外
371
+ # UnnecessaryArgument, ArgumentRequired, InvalidArgument
372
+ def check_option(name, value)
373
+ a = @options[name].argument
374
+ f = @options[name].format
375
+ raise UnnecessaryArgument, "option argument is unnecessary: #{name}" if a == false or (a.nil? and !f)
376
+ return true if a == :optional and value.nil?
377
+ raise ArgumentRequired, "argument required: #{name}" if value.nil?
378
+ return value if f == true
379
+ if f == :boolean
380
+ return true if ["1","true","enable","yes","y","on"].include? value.downcase
381
+ return false if ["0","false","disable","no","n","off"].include? value.downcase
382
+ raise InvalidArgument, "invalid boolean value: #{name}"
383
+ end
384
+ begin
385
+ return StringValidator.validate(f, value)
386
+ rescue StringValidator::Error => e
387
+ raise InvalidArgument, "invalid argument for option `#{name}': #{e.message}: #{value}"
388
+ end
389
+ end
390
+
391
+ # == 長い名前のオプションの解釈
392
+ # name:: オプション名
393
+ # argv:: それ以降の文字列配列
394
+ # completion:: 補完の有無
395
+ # === 戻り値
396
+ # Option オブジェクト
397
+ def parse_long_opt(name, argv, completion=true)
398
+ if name.include? "="
399
+ n, v = name.split "=", 2
400
+ n, invert = long_option n, completion
401
+ set_option n, v
402
+ @options[n].value = !@options[n].value if invert
403
+ return @options[n]
404
+ end
405
+ n, invert = long_option name, completion
406
+ opt = @options[n]
407
+ if opt.argument == false or opt.argument == :optional or
408
+ (opt.argument.nil? and !opt.format)
409
+ set_option n, !invert
410
+ return @options[n]
411
+ end
412
+ set_option n, argv.shift
413
+ @options[n].value = !@options[n].value if invert
414
+ return @options[n]
415
+ end
416
+
417
+ # == 短い名前のオプションの解釈
418
+ # name:: オプション名
419
+ # argv:: それ以降の文字列配列
420
+ # === 戻り値
421
+ # Option オブジェクト
422
+ def parse_short_opt(name, argv)
423
+ arg = name.dup
424
+ until arg.empty?
425
+ n = arg.slice!(0, 1)
426
+ raise UnknownOption, "unknown option: #{n}" unless @options.key? n
427
+ opt = @options[n]
428
+ if opt.argument == false or (opt.argument.nil? and !opt.format)
429
+ set_option n, true
430
+ next
431
+ end
432
+ unless arg.empty?
433
+ set_option n, arg
434
+ return @options[n]
435
+ end
436
+ if opt.argument == :optional
437
+ set_option n, true
438
+ return @options[n]
439
+ end
440
+ set_option n, argv.shift
441
+ return @options[n]
442
+ end
443
+ end
444
+
445
+ # == 長いオプション名の確認
446
+ # name:: オプション名
447
+ # completion:: 補完の有無
448
+ # === 戻り値
449
+ # String :: 補完後オプション名
450
+ # true/false :: "no-" prefix 指定?
451
+ # === 例外
452
+ # UnknownOption
453
+ def long_option(name, completion)
454
+ n = completion ? long_option_completion(name) : name.size > 1 && @options.key?(name) ? name : nil
455
+ return n, false if n
456
+ if name =~ /\Ano-([a-zA-Z0-9][a-zA-Z0-9_-]+)\z/
457
+ n = completion ? long_option_completion($1) : name.size > 1 && @options.key?(name) ? name : nil
458
+ return n, true if n and (@options[n].may_not_take_argument? or @options[n].format == :boolean)
459
+ end
460
+ name2 = name.gsub(/_/, "-")
461
+ n = completion ? long_option_completion(name2) : name.size > 1 && @options.key?(name2) ? name2 : nil
462
+ return n, false if n and @options[n].underscore_is_hyphen
463
+ if name2 =~ /\Ano-([a-zA-Z0-9][a-zA-Z0-9_-]+)\z/
464
+ n = completion ? long_option_completion($1) : name.size > 1 && @options.key?(name2) ? name2 : nil
465
+ return n, true if n and @options[n].underscore_is_hyphen and (@options[n].may_not_take_argument? or @options[n].format == :boolean)
466
+ end
467
+ raise UnknownOption, "unknown option: #{name}"
468
+ end
469
+
470
+ # == 長いオプション名の補完
471
+ # opt:: オプション名
472
+ # === 戻り値
473
+ # オプション名 / nil(未知のオプション)
474
+ # === 例外
475
+ # AmbiguousOption
476
+ def long_option_completion(opt)
477
+ return opt if @options.key? opt
478
+ candidate = @options.keys.sort.select{|k| opt == k[0, opt.size]}.select{|k| @options[k].completion}
479
+ raise AmbiguousOption, "ambiguous option: #{opt} (candidate are #{candidate.join(", ")})" if candidate.size > 1
480
+ return nil if candidate.empty? or not @options[candidate.first].completion
481
+ return candidate.first
482
+ end
483
+
484
+ # = オプション定義
485
+ class Option
486
+ # == 初期化
487
+ # args:: オプション名(String) のリスト、オプションの属性(Hash)
488
+ def initialize(*args)
489
+ name = args.dup
490
+ if name.last.is_a? Hash
491
+ attr = name.pop
492
+ else
493
+ attr = {}
494
+ end
495
+ raise "no option name: #{args.inspect}" if name.empty?
496
+ argument = nil
497
+ @usage_name = name
498
+ @name = name.to_a.map do |n|
499
+ unless n =~ /\A([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9_-]+)(\[?=.*)?\z/
500
+ raise "invalid option name: #{n.inspect}"
501
+ end
502
+ n = $1
503
+ argument = $2.nil? ? nil : $2 =~ /\A\[/ ? :optional : true
504
+ n
505
+ end
506
+ @argument = attr.key?(:argument) ? attr[:argument] : argument
507
+ @format = attr.key?(:format) ? attr[:format] : @argument ? true : nil
508
+ @default = attr[:default]
509
+ @description = attr[:description]
510
+ @multiple = attr.key?(:multiple) ? attr[:multiple] : :last
511
+ @completion = attr.key?(:completion) ? attr[:completion] : true
512
+ @underscore_is_hyphen = attr[:underscore_is_hyphen]
513
+ @in_config = attr.key?(:in_config) ? attr[:in_config] : true
514
+ @proc = attr[:proc]
515
+ @pre_proc = attr[:pre_proc]
516
+ @value = @default
517
+ @ovalue = @default
518
+ end
519
+ attr_reader :name, :argument, :format, :default, :description, :multiple
520
+ attr_reader :completion, :underscore_is_hyphen, :in_config, :proc, :pre_proc
521
+ attr_reader :usage_name
522
+ attr_accessor :value, :ovalue
523
+
524
+ def may_not_take_argument?
525
+ argument == false or argument == :optional or (argument.nil? and !format)
526
+ end
527
+ end
528
+
529
+ end
metadata ADDED
@@ -0,0 +1,64 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: optconfig
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.4.4
5
+ platform: ruby
6
+ authors:
7
+ - Tomita Masahiro
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-09-28 00:00:00 +09:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: stringvalidator
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: "0"
24
+ version:
25
+ description: OptConfig is a parser of command line option
26
+ email: tommy@tmtm.org
27
+ executables: []
28
+
29
+ extensions: []
30
+
31
+ extra_rdoc_files: []
32
+
33
+ files:
34
+ - lib/optconfig.rb
35
+ has_rdoc: true
36
+ homepage: http://github.com/tmtm/optconfig
37
+ licenses: []
38
+
39
+ post_install_message:
40
+ rdoc_options:
41
+ - --charset=utf-8
42
+ require_paths:
43
+ - lib
44
+ required_ruby_version: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: "0"
49
+ version:
50
+ required_rubygems_version: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: "0"
55
+ version:
56
+ requirements: []
57
+
58
+ rubyforge_project: optconfig
59
+ rubygems_version: 1.3.4
60
+ signing_key:
61
+ specification_version: 3
62
+ summary: OptConfig is a parser of command line option
63
+ test_files: []
64
+