benry-cmdopt 1.0.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.
- 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
|