benry-cmdopt 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGES.md +9 -0
- data/README.md +163 -0
- data/Rakefile.rb +92 -0
- data/benry-cmdopt.gemspec +30 -0
- data/lib/benry/cmdopt.rb +504 -0
- data/test/cmdopt_test.rb +922 -0
- metadata +79 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 51be94488014a32dabfc2f524a850fa657a11c4c1d9d9b92761d00e57e24969e
|
4
|
+
data.tar.gz: d2078058fec806f194f0bfd1d5d6705a78430c7989d7eb9ddccadea2daa3311b
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 41e0e1a914cb6022f31244d6770067bd38636f27c0ce9e30dbcd37a731bc8d3f485329e0c0b49ff3aa322250f6d475ee0c28032a1053132e0f8c2bf061b2aa7c
|
7
|
+
data.tar.gz: e5e3922ccf2d06e9b56997cc07e7616dfa905fedb1974953f7bc9138d7b888cab6d5b5c8025000cd574b8b608ead71ef772571fa71486c5467294d20376bdb9e
|
data/CHANGES.md
ADDED
data/README.md
ADDED
@@ -0,0 +1,163 @@
|
|
1
|
+
Benry::Cmdopt README
|
2
|
+
====================
|
3
|
+
|
4
|
+
($Release: 1.0.0 $)
|
5
|
+
|
6
|
+
Benry::Cmdopt is a command option parser library, like `optparse.rb`.
|
7
|
+
|
8
|
+
Compared to `optparse.rb`:
|
9
|
+
|
10
|
+
* Easy to use, easy to extend, easy to understand.
|
11
|
+
* Not add `-h` nor `--help` automatically.
|
12
|
+
* Not add `-v` nor `--version` automatically.
|
13
|
+
* Not regard `-x` as short cut of `--xxx`.
|
14
|
+
(`optparser.rb` regards `-x` as short cut of `--xxx` automatically.)
|
15
|
+
* Provides very simple feature to build custom help message.
|
16
|
+
* Separates command option schema class from parser class.
|
17
|
+
|
18
|
+
(Benry::Cmdopt requires Ruby >= 2.3)
|
19
|
+
|
20
|
+
|
21
|
+
Usage
|
22
|
+
=====
|
23
|
+
|
24
|
+
|
25
|
+
Define, parse, and print help
|
26
|
+
-----------------------------
|
27
|
+
|
28
|
+
```ruby
|
29
|
+
require 'benry/cmdopt'
|
30
|
+
|
31
|
+
## define
|
32
|
+
cmdopt = Benry::Cmdopt.new
|
33
|
+
cmdopt.add(:help , "-h, --help" , "print help message")
|
34
|
+
cmdopt.add(:version, " --version", "print version")
|
35
|
+
|
36
|
+
## parse with error handling
|
37
|
+
options = cmdopt.parse(ARGV) do |err|
|
38
|
+
$stderr.puts "ERROR: #{err.message}"
|
39
|
+
exit(1)
|
40
|
+
end
|
41
|
+
p options # ex: {:help => true, :version => true}
|
42
|
+
p ARGV # options are removed from ARGV
|
43
|
+
|
44
|
+
## help
|
45
|
+
if options[:help]
|
46
|
+
puts "Usage: foobar [<options>] [<args>...]"
|
47
|
+
puts ""
|
48
|
+
puts "Options:"
|
49
|
+
puts cmdopt.build_option_help()
|
50
|
+
## or
|
51
|
+
#format = " %-20s : %s"
|
52
|
+
#cmdopt.each_option_help {|opt, help| puts format % [opt, help] }
|
53
|
+
end
|
54
|
+
```
|
55
|
+
|
56
|
+
|
57
|
+
Command option parameter
|
58
|
+
------------------------
|
59
|
+
|
60
|
+
```ruby
|
61
|
+
## required parameter
|
62
|
+
cmdopt.add(:file, "-f, --file=<FILE>", "filename")
|
63
|
+
cmdopt.add(:file, " --file=<FILE>", "filename")
|
64
|
+
cmdopt.add(:file, "-f <FILE>" , "filename")
|
65
|
+
|
66
|
+
## optional parameter
|
67
|
+
cmdopt.add(:file, "-f, --file[=<FILE>]", "filename")
|
68
|
+
cmdopt.add(:file, " --file[=<FILE>]", "filename")
|
69
|
+
cmdopt.add(:file, "-f[<FILE>]" , "filename")
|
70
|
+
```
|
71
|
+
|
72
|
+
|
73
|
+
Argument varidation
|
74
|
+
-------------------
|
75
|
+
|
76
|
+
```ruby
|
77
|
+
## type
|
78
|
+
cmdopt.add(:indent , "-i <N>", "indent width", type: Integer)
|
79
|
+
## pattern
|
80
|
+
cmdopt.add(:indent , "-i <N>", "indent width", pattern: /\A\d+\z/)
|
81
|
+
## enum
|
82
|
+
cmdopt.add(:indent , "-i <N>", "indent width", enum: [2, 4, 8])
|
83
|
+
## callback
|
84
|
+
cmdopt.add(:indent , "-i <N>", "indent width") {|val|
|
85
|
+
val =~ /\A\d+\z/ or
|
86
|
+
raise "integer expected." # raise without exception class.
|
87
|
+
val.to_i # convert argument value.
|
88
|
+
}
|
89
|
+
```
|
90
|
+
|
91
|
+
|
92
|
+
Available types
|
93
|
+
---------------
|
94
|
+
|
95
|
+
* Integer (`/\A[-+]?\d+\z/`)
|
96
|
+
* Float (`/\A[-+]?(\d+\.\d*\|\.\d+)z/`)
|
97
|
+
* TrueClass (`/\A(true|on|yes|false|off|no)\z/`)
|
98
|
+
* Date (`/\A\d\d\d\d-\d\d?-\d\d?\z/`)
|
99
|
+
|
100
|
+
|
101
|
+
Multiple parameters
|
102
|
+
-------------------
|
103
|
+
|
104
|
+
```ruby
|
105
|
+
cmdopt.add(:lib , "-I <NAME>", "library names") {|optdict, key, val|
|
106
|
+
arr = optdict[key] || []
|
107
|
+
arr << val
|
108
|
+
arr
|
109
|
+
}
|
110
|
+
```
|
111
|
+
|
112
|
+
|
113
|
+
Not support
|
114
|
+
-----------
|
115
|
+
|
116
|
+
* default value
|
117
|
+
* `--no-xxx` style option
|
118
|
+
|
119
|
+
|
120
|
+
Internal classes
|
121
|
+
================
|
122
|
+
|
123
|
+
* `Benry::Cmdopt::Schema` -- command option schema.
|
124
|
+
* `Benry::Cmdopt::Parser` -- command option parser.
|
125
|
+
* `Benry::Cmdopt::Facade` -- facade object including schema and parser.
|
126
|
+
|
127
|
+
```ruby
|
128
|
+
require 'benry/cmdopt'
|
129
|
+
|
130
|
+
## define schema
|
131
|
+
schema = Benry::Cmdopt::Schema.new
|
132
|
+
schema.add(:help , '-h, --help' , "show help message")
|
133
|
+
schema.add(:file , '-f, --file=<FILE>' , "filename")
|
134
|
+
schema.add(:indent, '-i, --indent[=<WIDTH>]', "enable indent", type: Integer)
|
135
|
+
|
136
|
+
## parse options
|
137
|
+
parser = Benry::Cmdopt::Parser.new(schema)
|
138
|
+
argv = ['-hi2', '--file=blabla.txt', 'aaa', 'bbb']
|
139
|
+
opts = parser.parse(argv) do |err|
|
140
|
+
$stderr.puts "ERROR: #{err.message}"
|
141
|
+
exit 1
|
142
|
+
end
|
143
|
+
p opts #=> [:help=>true, :indent=>2, :file=>"blabla.txt"]
|
144
|
+
p argv #=> ["aaa", "bbb"]
|
145
|
+
```
|
146
|
+
|
147
|
+
Notice that `Benry::Cmdopt.new()` returns facade object.
|
148
|
+
|
149
|
+
```ruby
|
150
|
+
require 'benry/cmdopt'
|
151
|
+
|
152
|
+
cmdopt = Benry::Cmdopt.new() # new facade object
|
153
|
+
cmdopt.add(:help, '-h', "help message") # same as schema.add(...)
|
154
|
+
opts = cmdopt.parse(ARGV) # same as parser.parse(...)
|
155
|
+
```
|
156
|
+
|
157
|
+
|
158
|
+
License and Copyright
|
159
|
+
=====================
|
160
|
+
|
161
|
+
$License: MIT License $
|
162
|
+
|
163
|
+
$Copyright: copyright(c) 2021 kuwata-lab.com all rights reserved $
|
data/Rakefile.rb
ADDED
@@ -0,0 +1,92 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
|
3
|
+
|
4
|
+
project = "benry-cmdopt"
|
5
|
+
release = ENV['RELEASE'] || "0.0.0"
|
6
|
+
copyright = "copyright(c) 2021 kuwata-lab.com all rights reserved"
|
7
|
+
license = "MIT License"
|
8
|
+
|
9
|
+
target_files = Dir[*%W[
|
10
|
+
README.md CHANGES.md MIT-LICENSE.txt Rakefile.rb
|
11
|
+
lib/**/*.rb
|
12
|
+
test/**/*_test.rb
|
13
|
+
#{project}.gemspec
|
14
|
+
]]
|
15
|
+
|
16
|
+
|
17
|
+
require 'rake/clean'
|
18
|
+
CLEAN << "build"
|
19
|
+
CLOBBER << Dir.glob("#{project}-*.gem")
|
20
|
+
|
21
|
+
|
22
|
+
task :default => :help
|
23
|
+
|
24
|
+
|
25
|
+
desc "show help"
|
26
|
+
task :help do
|
27
|
+
puts "rake help # help"
|
28
|
+
puts "rake test # run test"
|
29
|
+
puts "rake package RELEASE=X.X.X # create gem file"
|
30
|
+
puts "rake publish RELEASE=X.X.X # upload gem file"
|
31
|
+
puts "rake clean # remove files"
|
32
|
+
end
|
33
|
+
|
34
|
+
|
35
|
+
desc "do test"
|
36
|
+
task :test do
|
37
|
+
sh "ruby", *Dir.glob("test/*.rb")
|
38
|
+
end
|
39
|
+
|
40
|
+
|
41
|
+
desc "create package"
|
42
|
+
task :package do
|
43
|
+
release != "0.0.0" or
|
44
|
+
raise "specify $RELEASE"
|
45
|
+
## copy
|
46
|
+
dir = "build"
|
47
|
+
rm_rf dir if File.exist?(dir)
|
48
|
+
mkdir dir
|
49
|
+
target_files.each do |file|
|
50
|
+
dest = File.join(dir, File.dirname(file))
|
51
|
+
mkdir_p dest, :verbose=>false unless File.exist?(dest)
|
52
|
+
cp file, "#{dir}/#{file}"
|
53
|
+
end
|
54
|
+
## edit
|
55
|
+
Dir.glob("#{dir}/**/*").each do |file|
|
56
|
+
next unless File.file?(file)
|
57
|
+
File.open(file, 'rb+') do |f|
|
58
|
+
s1 = f.read()
|
59
|
+
s2 = s1
|
60
|
+
s2 = s2.gsub(/\$Release[:].*?\$/, "$"+"Release: #{release} $")
|
61
|
+
s2 = s2.gsub(/\$Copyright[:].*?\$/, "$"+"Copyright: #{copyright} $")
|
62
|
+
s2 = s2.gsub(/\$License[:].*?\$/, "$"+"License: #{license} $")
|
63
|
+
#
|
64
|
+
if s1 != s2
|
65
|
+
f.rewind()
|
66
|
+
f.truncate(0)
|
67
|
+
f.write(s2)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
## build
|
72
|
+
chdir dir do
|
73
|
+
sh "gem build #{project}.gemspec"
|
74
|
+
end
|
75
|
+
mv "#{dir}/#{project}-#{release}.gem", "."
|
76
|
+
end
|
77
|
+
|
78
|
+
|
79
|
+
desc "upload gem file to rubygems.org"
|
80
|
+
task :publish do
|
81
|
+
release != "0.0.0" or
|
82
|
+
raise "specify $RELEASE"
|
83
|
+
#
|
84
|
+
gemfile = "#{project}-#{release}.gem"
|
85
|
+
print "** Are you sure to publish #{gemfile}? [y/N]: "
|
86
|
+
answer = $stdin.gets().strip()
|
87
|
+
if answer.downcase == "y"
|
88
|
+
sh "gem push #{gemfile}"
|
89
|
+
sh "git tag ruby-#{project}-#{release}"
|
90
|
+
sh "git push --tags"
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
|
3
|
+
Gem::Specification.new do |spec|
|
4
|
+
spec.name = 'benry-cmdopt'
|
5
|
+
spec.version = '$Release: 1.0.0 $'.split()[1]
|
6
|
+
spec.author = 'kwatch'
|
7
|
+
spec.email = 'kwatch@gmail.com'
|
8
|
+
spec.platform = Gem::Platform::RUBY
|
9
|
+
spec.homepage = 'https://github.com/kwatch/benry/tree/ruby/benry-cmdopt'
|
10
|
+
spec.summary = "Command option parser, like `optparse.rb`"
|
11
|
+
spec.description = <<-'END'
|
12
|
+
Command option parser, like `optparse.rb`.
|
13
|
+
END
|
14
|
+
spec.license = 'MIT'
|
15
|
+
spec.files = Dir[
|
16
|
+
'README.md', 'CHANGES.md', 'MIT-LICENSE',
|
17
|
+
'Rakefile.rb', 'benry-cmdopt.gemspec',
|
18
|
+
'bin/*',
|
19
|
+
'lib/**/*.rb',
|
20
|
+
'test/**/*.rb',
|
21
|
+
]
|
22
|
+
#spec.executables = ['benry-cmdopt']
|
23
|
+
spec.bindir = 'bin'
|
24
|
+
spec.require_path = 'lib'
|
25
|
+
spec.test_files = Dir['test/**/*_test.rb']
|
26
|
+
#spec.extra_rdoc_files = ['README.md', 'CHANGES.md']
|
27
|
+
|
28
|
+
spec.add_development_dependency 'minitest' , '~> 5.8'
|
29
|
+
spec.add_development_dependency 'minitest-ok' , '~> 0.3'
|
30
|
+
end
|
data/lib/benry/cmdopt.rb
ADDED
@@ -0,0 +1,504 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
|
3
|
+
###
|
4
|
+
### $Release$
|
5
|
+
### $Copyright$
|
6
|
+
### $License$
|
7
|
+
###
|
8
|
+
|
9
|
+
require 'date'
|
10
|
+
require 'set'
|
11
|
+
|
12
|
+
|
13
|
+
module Benry
|
14
|
+
|
15
|
+
|
16
|
+
##
|
17
|
+
## Command option parser.
|
18
|
+
##
|
19
|
+
## Usage:
|
20
|
+
## ## define
|
21
|
+
## cmdopt = Benry::Cmdopt.new
|
22
|
+
## cmdopt.add(:help , "-h, --help" , "print help message")
|
23
|
+
## cmdopt.add(:version, " --version", "print version")
|
24
|
+
## ## parse
|
25
|
+
## options = cmdopt.parse(ARGV) do |err|
|
26
|
+
## $stderr.puts "ERROR: #{err.message}"
|
27
|
+
## exit(1)
|
28
|
+
## end
|
29
|
+
## p options # ex: {:help => true, :version => true}
|
30
|
+
## p ARGV # options are removed from ARGV
|
31
|
+
## ## help
|
32
|
+
## if options[:help]
|
33
|
+
## puts "Usage: foobar [<options>] [<args>...]"
|
34
|
+
## puts ""
|
35
|
+
## puts "Options:"
|
36
|
+
## puts cmdopt.build_option_help()
|
37
|
+
## ## or
|
38
|
+
## #format = " %-20s : %s"
|
39
|
+
## #cmdopt.each_option_help {|opt, help| puts format % [opt, help] }
|
40
|
+
## end
|
41
|
+
##
|
42
|
+
## Command option parameter:
|
43
|
+
## ## required
|
44
|
+
## cmdopt.add(:file, "-f, --file=<FILE>", "filename")
|
45
|
+
## cmdopt.add(:file, " --file=<FILE>", "filename")
|
46
|
+
## cmdopt.add(:file, "-f <FILE>" , "filename")
|
47
|
+
## ## optional
|
48
|
+
## cmdopt.add(:file, "-f, --file[=<FILE>]", "filename")
|
49
|
+
## cmdopt.add(:file, " --file[=<FILE>]", "filename")
|
50
|
+
## cmdopt.add(:file, "-f[<FILE>]" , "filename")
|
51
|
+
##
|
52
|
+
## Validation:
|
53
|
+
## ## type
|
54
|
+
## cmdopt.add(:indent , "-i <N>", "indent width", type: Integer)
|
55
|
+
## ## pattern
|
56
|
+
## cmdopt.add(:indent , "-i <N>", "indent width", pattern: /\A\d+\z/)
|
57
|
+
## ## enum
|
58
|
+
## cmdopt.add(:indent , "-i <N>", "indent width", enum: [2, 4, 8])
|
59
|
+
## ## callback
|
60
|
+
## cmdopt.add(:indent , "-i <N>", "indent width") {|val|
|
61
|
+
## val =~ /\A\d+\z/ or
|
62
|
+
## raise "integer expected." # raise without exception class.
|
63
|
+
## val.to_i # convert argument value.
|
64
|
+
## }
|
65
|
+
##
|
66
|
+
## Available types:
|
67
|
+
## * Integer (`/\A[-+]?\d+\z/`)
|
68
|
+
## * Float (`/\A[-+]?(\d+\.\d*\|\.\d+)z/`)
|
69
|
+
## * TrueClass (`/\A(true|on|yes|false|off|no)\z/`)
|
70
|
+
## * Date (`/\A\d\d\d\d-\d\d?-\d\d?\z/`)
|
71
|
+
##
|
72
|
+
## Multiple parameters:
|
73
|
+
## cmdopt.add(:lib , "-I <NAME>", "library names") {|optdict, key, val|
|
74
|
+
## arr = optdict[key] || []
|
75
|
+
## arr << val
|
76
|
+
## arr
|
77
|
+
## }
|
78
|
+
##
|
79
|
+
## Not support:
|
80
|
+
## * default value
|
81
|
+
## * `--no-xxx` style option
|
82
|
+
##
|
83
|
+
module Cmdopt
|
84
|
+
|
85
|
+
|
86
|
+
VERSION = '$Release: 1.0.0 $'.split()[1]
|
87
|
+
|
88
|
+
|
89
|
+
def self.new
|
90
|
+
#; [!7kkqv] creates Facade object.
|
91
|
+
return Facade.new
|
92
|
+
end
|
93
|
+
|
94
|
+
|
95
|
+
class Facade
|
96
|
+
|
97
|
+
def initialize
|
98
|
+
@schema = SCHEMA_CLASS.new
|
99
|
+
end
|
100
|
+
|
101
|
+
def add(key, optdef, help, type: nil, pattern: nil, enum: nil, &callback)
|
102
|
+
#; [!vmb3r] defines command option.
|
103
|
+
@schema.add(key, optdef, help, type: type, pattern: pattern, enum: enum, &callback)
|
104
|
+
#; [!tu4k3] returns self.
|
105
|
+
self
|
106
|
+
end
|
107
|
+
|
108
|
+
def build_option_help(width_or_format=nil, all: false)
|
109
|
+
#; [!dm4p8] returns option help message.
|
110
|
+
return @schema.build_option_help(width_or_format, all: all)
|
111
|
+
end
|
112
|
+
|
113
|
+
def each_option_help(&block)
|
114
|
+
#; [!bw9qx] yields each option definition string and help message.
|
115
|
+
@schema.each_option_help(&block)
|
116
|
+
self
|
117
|
+
end
|
118
|
+
|
119
|
+
def parse(argv, &error_handler)
|
120
|
+
#; [!7gc2m] parses command options.
|
121
|
+
#; [!no4xu] returns option values as dict.
|
122
|
+
#; [!areof] handles only OptionError when block given.
|
123
|
+
#; [!peuva] returns nil when OptionError handled.
|
124
|
+
parser = PARSER_CLASS.new(@schema)
|
125
|
+
return parser.parse(argv, &error_handler)
|
126
|
+
end
|
127
|
+
|
128
|
+
end
|
129
|
+
|
130
|
+
|
131
|
+
class Schema
|
132
|
+
|
133
|
+
def initialize()
|
134
|
+
@items = []
|
135
|
+
end
|
136
|
+
|
137
|
+
def add(key, optdef, help, type: nil, pattern: nil, enum: nil, &callback)
|
138
|
+
#; [!rhhji] raises SchemaError when key is not a Symbol.
|
139
|
+
key.nil? || key.is_a?(Symbol) or
|
140
|
+
raise error("add(#{key.inspect}): 1st arg should be a Symbol as an option key.")
|
141
|
+
#; [!vq6eq] raises SchemaError when help message is missing."
|
142
|
+
help.nil? || help.is_a?(String) or
|
143
|
+
raise error("add(#{key.inspect}, #{optdef.inspect}): help message required as 3rd argument.")
|
144
|
+
#; [!7hi2d] takes command option definition string.
|
145
|
+
short, long, param, optional = parse_optdef(optdef)
|
146
|
+
#; [!p9924] option key is omittable only when long option specified.
|
147
|
+
#; [!jtp7z] raises SchemaError when key is nil and no long option.
|
148
|
+
key || long or
|
149
|
+
raise error("add(#{key.inspect}, #{optdef.inspect}): long option required when option key (1st arg) not specified.")
|
150
|
+
key ||= long.gsub(/-/, '_').intern
|
151
|
+
#; [!7xmr5] raises SchemaError when type is not registered.
|
152
|
+
#; [!s2aaj] raises SchemaError when option has no params but type specified.
|
153
|
+
if type
|
154
|
+
PARAM_TYPES.key?(type) or
|
155
|
+
raise error("#{type.inspect}: unregistered type.")
|
156
|
+
param or
|
157
|
+
raise error("#{type.inspect}: type specified in spite of option has no params.")
|
158
|
+
end
|
159
|
+
#; [!bi2fh] raises SchemaError when pattern is not a regexp.
|
160
|
+
#; [!01fmt] raises SchmeaError when option has no params but pattern specified.
|
161
|
+
if pattern
|
162
|
+
pattern.is_a?(Regexp) or
|
163
|
+
raise error("#{pattern.inspect}: regexp expected.")
|
164
|
+
param or
|
165
|
+
raise error("#{pattern.inspect}: pattern specified in spite of option has no params.")
|
166
|
+
end
|
167
|
+
#; [!melyd] raises SchmeaError when enum is not a Array nor Set.
|
168
|
+
#; [!xqed8] raises SchemaError when enum specified for no param option.
|
169
|
+
if enum
|
170
|
+
enum.is_a?(Array) || enum.is_a?(Set) or
|
171
|
+
raise error("#{enum.inspect}: array or set expected.")
|
172
|
+
param or
|
173
|
+
raise error("#{enum.inspect}: enum specified in spite of option has no params.")
|
174
|
+
end
|
175
|
+
#; [!yht0v] keeps command option definitions.
|
176
|
+
item = SchemaItem.new(key, optdef, short, long, param, help,
|
177
|
+
optional: optional, type: type, pattern: pattern, enum: enum, &callback)
|
178
|
+
@items << item
|
179
|
+
item
|
180
|
+
end
|
181
|
+
|
182
|
+
def build_option_help(width_or_format=nil, all: false)
|
183
|
+
#; [!0aq0i] can take integer as width.
|
184
|
+
#; [!pcsah] can take format string.
|
185
|
+
#; [!dndpd] detects option width automatically when nothing specified.
|
186
|
+
case width_or_format
|
187
|
+
when nil ; format = _default_format()
|
188
|
+
when Integer; format = " %-#{width_or_format}s: %s"
|
189
|
+
when String ; format = width_or_format
|
190
|
+
else
|
191
|
+
raise ArgumentError.new("#{width_or_format.inspect}: width (integer) or format (string) expected.")
|
192
|
+
end
|
193
|
+
#; [!v7z4x] skips option help if help message is not specified.
|
194
|
+
#; [!to1th] includes all option help when `all` is true.
|
195
|
+
buf = []
|
196
|
+
width = nil
|
197
|
+
each_option_help do |opt, help|
|
198
|
+
#buf << format % [opt, help] << "\n" if help || all
|
199
|
+
if help
|
200
|
+
#; [!848rm] supports multi-lines help message.
|
201
|
+
n = 0
|
202
|
+
help.each_line do |line|
|
203
|
+
if (n += 1) == 1
|
204
|
+
buf << format % [opt, line.chomp] << "\n"
|
205
|
+
else
|
206
|
+
width ||= (format % ['', '']).length
|
207
|
+
buf << (' ' * width) << line.chomp << "\n"
|
208
|
+
end
|
209
|
+
end
|
210
|
+
elsif all
|
211
|
+
buf << format % [opt, ''] << "\n"
|
212
|
+
end
|
213
|
+
end
|
214
|
+
return buf.join()
|
215
|
+
end
|
216
|
+
|
217
|
+
def _default_format(min_width=20, max_width=35)
|
218
|
+
#; [!hr45y] detects preffered option width.
|
219
|
+
w = 0
|
220
|
+
each_option_help do |opt, help|
|
221
|
+
w = opt.length if w < opt.length
|
222
|
+
end
|
223
|
+
w = min_width if w < min_width
|
224
|
+
w = max_width if w > max_width
|
225
|
+
#; [!kkh9t] returns format string.
|
226
|
+
return " %-#{w}s : %s"
|
227
|
+
end
|
228
|
+
private :_default_format
|
229
|
+
|
230
|
+
def each_option_help(&block)
|
231
|
+
#; [!4b911] yields each optin definition str and help message.
|
232
|
+
@items.each do |item|
|
233
|
+
yield item.optdef, item.help
|
234
|
+
end
|
235
|
+
#; [!zbxyv] returns self.
|
236
|
+
self
|
237
|
+
end
|
238
|
+
|
239
|
+
def find_short_option(short)
|
240
|
+
#; [!b4js1] returns option definition matched to short name.
|
241
|
+
#; [!s4d1y] returns nil when nothing found.
|
242
|
+
return @items.find {|item| item.short == short }
|
243
|
+
end
|
244
|
+
|
245
|
+
def find_long_option(long)
|
246
|
+
#; [!atmf9] returns option definition matched to long name.
|
247
|
+
#; [!6haoo] returns nil when nothing found.
|
248
|
+
return @items.find {|item| item.long == long }
|
249
|
+
end
|
250
|
+
|
251
|
+
private
|
252
|
+
|
253
|
+
def error(msg)
|
254
|
+
return SchemaError.new(msg)
|
255
|
+
end
|
256
|
+
|
257
|
+
def parse_optdef(optdef)
|
258
|
+
#; [!qw0ac] parses command option definition string.
|
259
|
+
#; [!ae733] parses command option definition which has a required param.
|
260
|
+
#; [!4h05c] parses command option definition which has an optional param.
|
261
|
+
#; [!b7jo3] raises SchemaError when command option definition is invalid.
|
262
|
+
case optdef
|
263
|
+
when /\A[ \t]*-(\w),[ \t]*--(\w[-\w]*)(?:=(\S*?)|\[=(\S*?)\])?\z/
|
264
|
+
short, long, param1, param2 = $1, $2, $3, $4
|
265
|
+
when /\A[ \t]*-(\w)(?:[ \t]+(\S+)|\[(\S+)\])?\z/
|
266
|
+
short, long, param1, param2 = $1, nil, $2, $3
|
267
|
+
when /\A[ \t]*--(\w[-\w]*)(?:=(\S*?)|\[=(\S*?)\])?\z/
|
268
|
+
short, long, param1, param2 = nil, $1, $2, $3
|
269
|
+
when /(--\w[-\w])*[ \t]+(\S+)/
|
270
|
+
raise error("#{optdef}: invalid option definition (use '#{$1}=#{$2}' instead of '#{$1} #{$2}').")
|
271
|
+
else
|
272
|
+
raise error("#{optdef}: invalid option definition.")
|
273
|
+
end
|
274
|
+
return short, long, param1 || param2, !!param2
|
275
|
+
end
|
276
|
+
|
277
|
+
end
|
278
|
+
|
279
|
+
|
280
|
+
class SchemaItem # avoid Struct
|
281
|
+
|
282
|
+
def initialize(key, optdef, short, long, param, help, optional: nil, type: nil, pattern: nil, enum: nil, &callback)
|
283
|
+
@key = key
|
284
|
+
@optdef = optdef
|
285
|
+
@short = short
|
286
|
+
@long = long
|
287
|
+
@param = param
|
288
|
+
@help = help
|
289
|
+
@optional = optional
|
290
|
+
@type = type
|
291
|
+
@pattern = pattern
|
292
|
+
@enum = enum
|
293
|
+
@callback = callback
|
294
|
+
end
|
295
|
+
|
296
|
+
attr_reader :key, :optdef, :short, :long, :param, :help, :optional, :type, :pattern, :enum, :callback
|
297
|
+
|
298
|
+
def optional_param?
|
299
|
+
@optional
|
300
|
+
end
|
301
|
+
|
302
|
+
def validate_and_convert(val, optdict)
|
303
|
+
#; [!h0s0o] raises RuntimeError when value not matched to pattern.
|
304
|
+
if @pattern && val != true
|
305
|
+
val =~ @pattern or
|
306
|
+
raise "pattern unmatched."
|
307
|
+
end
|
308
|
+
#; [!j4fuz] calls type-specific callback when type specified.
|
309
|
+
if @type && val != true
|
310
|
+
proc_ = PARAM_TYPES[@type]
|
311
|
+
val = proc_.call(val)
|
312
|
+
end
|
313
|
+
#; [!5jrdf] raises RuntimeError when value not in enum.
|
314
|
+
if @enum && val != true
|
315
|
+
@enum.include?(val) or
|
316
|
+
raise "expected one of #{@enum.join('/')}."
|
317
|
+
end
|
318
|
+
#; [!jn9z3] calls callback when callback specified.
|
319
|
+
#; [!iqalh] calls callback with different number of args according to arity.
|
320
|
+
if @callback
|
321
|
+
n_args = @callback.arity
|
322
|
+
val = n_args == 1 ? @callback.call(val) \
|
323
|
+
: @callback.call(optdict, @key, val)
|
324
|
+
end
|
325
|
+
#; [!x066l] returns new value.
|
326
|
+
return val
|
327
|
+
end
|
328
|
+
|
329
|
+
end
|
330
|
+
|
331
|
+
|
332
|
+
PARAM_TYPES = {
|
333
|
+
String => proc {|val|
|
334
|
+
val
|
335
|
+
},
|
336
|
+
Integer => proc {|val|
|
337
|
+
#; [!6t8cs] converts value into integer.
|
338
|
+
#; [!nzwc9] raises error when failed to convert value into integer.
|
339
|
+
val =~ /\A[-+]?\d+\z/ or
|
340
|
+
raise "integer expected."
|
341
|
+
val.to_i
|
342
|
+
},
|
343
|
+
Float => proc {|val|
|
344
|
+
#; [!gggy6] converts value into float.
|
345
|
+
#; [!t4elj] raises error when faield to convert value into float.
|
346
|
+
val =~ /\A[-+]?(\d+\.\d*|\.\d+)\z/ or
|
347
|
+
raise "float expected."
|
348
|
+
val.to_f
|
349
|
+
},
|
350
|
+
TrueClass => proc {|val|
|
351
|
+
#; [!47kx4] converts 'true'/'on'/'yes' into true.
|
352
|
+
#; [!3n810] converts 'false'/'off'/'no' into false.
|
353
|
+
#; [!h8ayh] raises error when failed to convert value into true nor false.
|
354
|
+
case val
|
355
|
+
when /\A(?:true|on|yes)\z/i
|
356
|
+
true
|
357
|
+
when /\A(?:false|off|no)\z/i
|
358
|
+
false
|
359
|
+
else
|
360
|
+
raise "boolean expected."
|
361
|
+
end
|
362
|
+
},
|
363
|
+
Date => proc {|val|
|
364
|
+
#; [!sru5j] converts 'YYYY-MM-DD' into date object.
|
365
|
+
#; [!h9q9y] raises error when failed to convert into date object.
|
366
|
+
#; [!i4ui8] raises error when specified date not exist.
|
367
|
+
val =~ /\A(\d\d\d\d)-(\d\d?)-(\d\d?)\z/ or
|
368
|
+
raise "invalid date format (ex: '2000-01-01')"
|
369
|
+
begin
|
370
|
+
Date.new($1.to_i, $2.to_i, $3.to_i)
|
371
|
+
rescue ArgumentError => ex
|
372
|
+
raise "date not exist."
|
373
|
+
end
|
374
|
+
},
|
375
|
+
}
|
376
|
+
|
377
|
+
|
378
|
+
class Parser
|
379
|
+
|
380
|
+
def initialize(schema)
|
381
|
+
@schema = schema
|
382
|
+
end
|
383
|
+
|
384
|
+
def parse(argv, &error_handler)
|
385
|
+
optdict = new_options_dict()
|
386
|
+
while !argv.empty? && argv[0] =~ /\A-/
|
387
|
+
optstr = argv.shift
|
388
|
+
#; [!y04um] skips rest options when '--' found in argv.
|
389
|
+
if optstr == '--'
|
390
|
+
break
|
391
|
+
elsif optstr =~ /\A--/
|
392
|
+
#; [!uh7j8] parses long options.
|
393
|
+
parse_long_option(optstr, optdict, argv)
|
394
|
+
else
|
395
|
+
#; [!nwnjc] parses short options.
|
396
|
+
parse_short_options(optstr, optdict, argv)
|
397
|
+
end
|
398
|
+
end
|
399
|
+
#; [!3wmsy] returns command option values as a dict.
|
400
|
+
return optdict
|
401
|
+
rescue OptionError => ex
|
402
|
+
#; [!qpuxh] handles only OptionError when block given.
|
403
|
+
raise unless block_given?()
|
404
|
+
yield ex
|
405
|
+
#; [!dhpw1] returns nil when OptionError handled.
|
406
|
+
nil
|
407
|
+
end
|
408
|
+
|
409
|
+
def error(msg)
|
410
|
+
return OptionError.new(msg)
|
411
|
+
end
|
412
|
+
|
413
|
+
protected
|
414
|
+
|
415
|
+
def parse_long_option(optstr, optdict, _argv)
|
416
|
+
#; [!3i994] raises OptionError when invalid long option format.
|
417
|
+
optstr =~ /\A--(\w[-\w]*)(?:=(.*))?\z/ or
|
418
|
+
raise error("#{optstr}: invalid long option.")
|
419
|
+
name = $1; val = $2
|
420
|
+
#; [!er7h4] raises OptionError when unknown long option.
|
421
|
+
item = @schema.find_long_option(name) or
|
422
|
+
raise error("#{optstr}: unknown long option.")
|
423
|
+
#; [!2jd9w] raises OptionError when no arguments specified for arg required long option.
|
424
|
+
#; [!qyq8n] raises optionError when an argument specified for no arg long option.
|
425
|
+
if item.optional_param?
|
426
|
+
# do nothing
|
427
|
+
elsif item.param
|
428
|
+
val or raise error("#{optstr}: argument required.")
|
429
|
+
else
|
430
|
+
val.nil? or raise error("#{optstr}: unexpected argument.")
|
431
|
+
end
|
432
|
+
#; [!o596x] validates argument value.
|
433
|
+
val ||= true
|
434
|
+
begin
|
435
|
+
val = item.validate_and_convert(val, optdict)
|
436
|
+
rescue RuntimeError => ex
|
437
|
+
raise error("#{optstr}: #{ex.message}")
|
438
|
+
end
|
439
|
+
optdict[item.key] = val
|
440
|
+
end
|
441
|
+
|
442
|
+
def parse_short_options(optstr, optdict, argv)
|
443
|
+
n = optstr.length
|
444
|
+
i = 0
|
445
|
+
while (i += 1) < n
|
446
|
+
char = optstr[i]
|
447
|
+
#; [!4eh49] raises OptionError when unknown short option specified.
|
448
|
+
item = @schema.find_short_option(char) or
|
449
|
+
raise error("-#{char}: unknown option.")
|
450
|
+
#
|
451
|
+
if !item.param
|
452
|
+
val = true
|
453
|
+
elsif !item.optional_param?
|
454
|
+
#; [!utdbf] raises OptionError when argument required but not specified.
|
455
|
+
#; [!f63hf] short option arg can be specified without space separator.
|
456
|
+
val = i+1 < n ? optstr[(i+1)..-1] : argv.shift or
|
457
|
+
raise error("-#{char}: argument required.")
|
458
|
+
i = n
|
459
|
+
else
|
460
|
+
#; [!yjq6b] optional arg should be specified without space separator.
|
461
|
+
#; [!wape4] otpional arg can be omit.
|
462
|
+
val = i+1 < n ? optstr[(i+1)..-1] : true
|
463
|
+
i = n
|
464
|
+
end
|
465
|
+
#; [!yu0kc] validates short option argument.
|
466
|
+
begin
|
467
|
+
val = item.validate_and_convert(val, optdict)
|
468
|
+
rescue RuntimeError => ex
|
469
|
+
if val == true
|
470
|
+
raise error("-#{char}: #{ex.message}")
|
471
|
+
else
|
472
|
+
s = item.optional_param? ? '' : ' '
|
473
|
+
raise error("-#{char}#{s}#{val}: #{ex.message}")
|
474
|
+
end
|
475
|
+
end
|
476
|
+
optdict[item.key] = val
|
477
|
+
end
|
478
|
+
end
|
479
|
+
|
480
|
+
def new_options_dict()
|
481
|
+
#; [!vm6h0] returns new hash object.
|
482
|
+
return OPTIONS_CLASS.new
|
483
|
+
end
|
484
|
+
|
485
|
+
end
|
486
|
+
|
487
|
+
|
488
|
+
OPTIONS_CLASS = Hash
|
489
|
+
SCHEMA_CLASS = Schema
|
490
|
+
PARSER_CLASS = Parser
|
491
|
+
|
492
|
+
|
493
|
+
class SchemaError < StandardError
|
494
|
+
end
|
495
|
+
|
496
|
+
|
497
|
+
class OptionError < StandardError
|
498
|
+
end
|
499
|
+
|
500
|
+
|
501
|
+
end
|
502
|
+
|
503
|
+
|
504
|
+
end
|