opt 0.1.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/LICENSE.txt +165 -0
- data/README.md +85 -0
- data/doc/file.README.html +107 -0
- data/lib/opt.rb +65 -0
- data/lib/opt/command.rb +282 -0
- data/lib/opt/option.rb +198 -0
- data/lib/opt/program.rb +9 -0
- data/lib/opt/switch.rb +119 -0
- data/lib/opt/types/io.rb +4 -0
- data/lib/opt/version.rb +3 -0
- data/opt.gemspec +22 -0
- data/spec/opt/flag_spec.rb +100 -0
- data/spec/opt/option_spec.rb +232 -0
- data/spec/opt/switch_spec.rb +60 -0
- data/spec/opt_spec.rb +124 -0
- data/spec/readme_spec.rb +18 -0
- data/spec/spec_helper.rb +19 -0
- metadata +81 -0
data/lib/opt/option.rb
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
module Opt
|
|
2
|
+
#
|
|
3
|
+
# A command line option consisting of multiple switches,
|
|
4
|
+
# possibly arguments and options about allowed numbers etc.
|
|
5
|
+
#
|
|
6
|
+
# @api private
|
|
7
|
+
#
|
|
8
|
+
class Option
|
|
9
|
+
#
|
|
10
|
+
# Option's name.
|
|
11
|
+
#
|
|
12
|
+
# @return [String] Frozen name string.
|
|
13
|
+
#
|
|
14
|
+
attr_reader :name
|
|
15
|
+
|
|
16
|
+
# Set of switches triggering this option.
|
|
17
|
+
#
|
|
18
|
+
# Avoid direct manipulation.
|
|
19
|
+
#
|
|
20
|
+
# @return [Set<Switch>] Set of switches.
|
|
21
|
+
#
|
|
22
|
+
attr_reader :switches
|
|
23
|
+
|
|
24
|
+
# Options passed to {#initialize}.
|
|
25
|
+
#
|
|
26
|
+
# @return [Hash] Option hash.
|
|
27
|
+
#
|
|
28
|
+
attr_reader :options
|
|
29
|
+
|
|
30
|
+
# Option default value.
|
|
31
|
+
#
|
|
32
|
+
# @return [Object] Default value.
|
|
33
|
+
#
|
|
34
|
+
attr_reader :default
|
|
35
|
+
|
|
36
|
+
# Option value returned if switch is given.
|
|
37
|
+
#
|
|
38
|
+
# Will be ignored if option takes arguments.
|
|
39
|
+
#
|
|
40
|
+
# @return [Object] Option value.
|
|
41
|
+
#
|
|
42
|
+
attr_reader :value
|
|
43
|
+
|
|
44
|
+
# Number of arguments as a range.
|
|
45
|
+
#
|
|
46
|
+
# @return [Range] Argument number range.
|
|
47
|
+
#
|
|
48
|
+
attr_reader :nargs
|
|
49
|
+
|
|
50
|
+
def initialize(definition, options = {})
|
|
51
|
+
@options = options
|
|
52
|
+
@default = options.fetch(:default, nil)
|
|
53
|
+
@value = options.fetch(:value, true)
|
|
54
|
+
@nargs = Option.parse_nargs options.fetch(:nargs, 0)
|
|
55
|
+
|
|
56
|
+
if definition.to_s =~ /\A[[:word:]]+\z/
|
|
57
|
+
@switches = Set.new
|
|
58
|
+
@name = options.fetch(:name, definition).to_s.freeze
|
|
59
|
+
|
|
60
|
+
unless nargs.first > 0 || nargs.size > 1
|
|
61
|
+
raise 'A text option must consist of at least one argument.'
|
|
62
|
+
end
|
|
63
|
+
else
|
|
64
|
+
@switches = Switch.parse(definition)
|
|
65
|
+
@name = options.fetch(:name, switches.first.name).to_s.freeze
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Check if option is triggered by at least on CLI switch.
|
|
70
|
+
#
|
|
71
|
+
def switch?
|
|
72
|
+
switches.any?
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Check if option is a free-text option.
|
|
76
|
+
#
|
|
77
|
+
def text?
|
|
78
|
+
!switch?
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def collide?(option)
|
|
82
|
+
name == option.name || !switches.disjoint?(option.switches)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def parse!(argv, result)
|
|
86
|
+
if text?
|
|
87
|
+
parse_text!(argv, result)
|
|
88
|
+
else
|
|
89
|
+
parse_switches!(argv, result)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def parse_text!(argv, result)
|
|
94
|
+
return false unless argv.first.text?
|
|
95
|
+
|
|
96
|
+
parse_args!(argv, result)
|
|
97
|
+
true
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def parse_switches!(argv, result)
|
|
101
|
+
switches.each do |switch|
|
|
102
|
+
next unless switch.match!(argv)
|
|
103
|
+
|
|
104
|
+
parse_args!(argv, result)
|
|
105
|
+
return true
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
false
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def parse_args!(argv, result)
|
|
112
|
+
if nargs.first == 0 && nargs.last == 0
|
|
113
|
+
result[name] = value
|
|
114
|
+
else
|
|
115
|
+
args = []
|
|
116
|
+
if argv.any? && argv.first.text?
|
|
117
|
+
while argv.any? && argv.first.text? && args.size < nargs.last
|
|
118
|
+
args << argv.shift.value
|
|
119
|
+
end
|
|
120
|
+
elsif argv.any? && argv.first.short?
|
|
121
|
+
args << argv.shift.value
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
if nargs.include?(args.size)
|
|
125
|
+
if nargs.first == 1 && nargs.last == 1
|
|
126
|
+
result[name] = args.first
|
|
127
|
+
else
|
|
128
|
+
result[name] = args
|
|
129
|
+
end
|
|
130
|
+
else
|
|
131
|
+
# raise Opt::MissingArgument
|
|
132
|
+
raise "wrong number of arguments (#{args.size} for #{nargs})"
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
class << self
|
|
138
|
+
def parse_nargs(num)
|
|
139
|
+
case num
|
|
140
|
+
when Range
|
|
141
|
+
parse_nargs_range(num)
|
|
142
|
+
when Array
|
|
143
|
+
parse_nargs_array(num)
|
|
144
|
+
else
|
|
145
|
+
parse_nargs_obj(num)
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def parse_nargs_obj(obj)
|
|
150
|
+
case obj.to_s.downcase
|
|
151
|
+
when '+'
|
|
152
|
+
1..Float::INFINITY
|
|
153
|
+
when '*', 'inf', 'infinity'
|
|
154
|
+
0..Float::INFINITY
|
|
155
|
+
else
|
|
156
|
+
i = Integer(obj.to_s)
|
|
157
|
+
parse_nargs_range i..i
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def parse_nargs_range(range)
|
|
162
|
+
if range.first > range.last
|
|
163
|
+
if range.exclude_end?
|
|
164
|
+
range = Range.new(range.last + 1, range.first)
|
|
165
|
+
else
|
|
166
|
+
range = Range.new(range.last, range.first)
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
if range.first < 0
|
|
171
|
+
raise RuntimeError.new 'Argument number must not be less than zero.'
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
range
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def parse_nargs_array(obj)
|
|
178
|
+
if obj.size == 2
|
|
179
|
+
parse_nargs_range Range.new(parse_nargs_array_obj(obj[0]),
|
|
180
|
+
parse_nargs_array_obj(obj[1]))
|
|
181
|
+
else
|
|
182
|
+
|
|
183
|
+
raise ArgumentError.new \
|
|
184
|
+
'Argument number array count must be exactly two.'
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def parse_nargs_array_obj(obj)
|
|
189
|
+
case obj.to_s.downcase
|
|
190
|
+
when '*', 'inf', 'infinity'
|
|
191
|
+
Float::INFINITY
|
|
192
|
+
else
|
|
193
|
+
Integer(obj.to_s)
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|
data/lib/opt/program.rb
ADDED
data/lib/opt/switch.rb
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
require 'set'
|
|
2
|
+
|
|
3
|
+
module Opt
|
|
4
|
+
#
|
|
5
|
+
# A single command line switch.
|
|
6
|
+
#
|
|
7
|
+
class Switch
|
|
8
|
+
class << self
|
|
9
|
+
#
|
|
10
|
+
# Parse an object or string into a Set of switches.
|
|
11
|
+
#
|
|
12
|
+
# @example
|
|
13
|
+
# Opt::Switch.parse '-h, --help'
|
|
14
|
+
# #=> Set{<Switch: -h>, <Switch: --help>}
|
|
15
|
+
#
|
|
16
|
+
def parse(object)
|
|
17
|
+
if object.is_a?(self)
|
|
18
|
+
object
|
|
19
|
+
else
|
|
20
|
+
if object.respond_to?(:to_str)
|
|
21
|
+
parse_str object.to_str
|
|
22
|
+
else
|
|
23
|
+
parse_str object.to_s
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Create new command line switch.
|
|
29
|
+
#
|
|
30
|
+
# If a Switch object is given it will be returned instead.
|
|
31
|
+
#
|
|
32
|
+
# @see #initialize
|
|
33
|
+
#
|
|
34
|
+
def new(object)
|
|
35
|
+
if object.is_a?(self)
|
|
36
|
+
object
|
|
37
|
+
else
|
|
38
|
+
super object
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def parse_str(str)
|
|
45
|
+
Set.new str.split(/\s*,\s*/).map{|s| new s }
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Switch name.
|
|
50
|
+
#
|
|
51
|
+
attr_reader :name
|
|
52
|
+
|
|
53
|
+
# Regular expression matching an accepted command line switch
|
|
54
|
+
REGEXP = /\A(?<dash>--?)(?<name>[[:word:]]+([[[:word:]]-]*[[:word:]])?)\z/
|
|
55
|
+
|
|
56
|
+
def initialize(str)
|
|
57
|
+
match = REGEXP.match(str)
|
|
58
|
+
raise "Invalid command line switch: #{str.inspect}" unless match
|
|
59
|
+
|
|
60
|
+
@name = match[:name].freeze
|
|
61
|
+
|
|
62
|
+
case match[:dash].to_s.size
|
|
63
|
+
when 0
|
|
64
|
+
@short = name.size == 1
|
|
65
|
+
when 1
|
|
66
|
+
@short = true
|
|
67
|
+
else
|
|
68
|
+
@short = false
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def short?
|
|
73
|
+
@short
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def long?
|
|
77
|
+
!short?
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def eql?(object)
|
|
81
|
+
if object.is_a?(self.class)
|
|
82
|
+
name == object.name
|
|
83
|
+
else
|
|
84
|
+
super
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def hash
|
|
89
|
+
name.hash
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def match?(argv)
|
|
93
|
+
case (arg = argv.first).type
|
|
94
|
+
when :long
|
|
95
|
+
long? && arg.value.split('=')[0] == name
|
|
96
|
+
when :short
|
|
97
|
+
short? && arg.value[0] == name[0]
|
|
98
|
+
else
|
|
99
|
+
false
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def match!(argv)
|
|
104
|
+
return false unless match?(argv)
|
|
105
|
+
|
|
106
|
+
if short? && argv.first.value.size > 1
|
|
107
|
+
argv.first.value.slice!(0, 1)
|
|
108
|
+
else
|
|
109
|
+
arg = argv.shift
|
|
110
|
+
|
|
111
|
+
if arg.value.include?('=')
|
|
112
|
+
argv.unshift Command::Token.new(:text, arg.value.split('=', 2)[1])
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
true
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
data/lib/opt/types/io.rb
ADDED
data/lib/opt/version.rb
ADDED
data/opt.gemspec
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
4
|
+
require 'opt/version'
|
|
5
|
+
|
|
6
|
+
Gem::Specification.new do |spec|
|
|
7
|
+
spec.name = 'opt'
|
|
8
|
+
spec.version = Opt::VERSION
|
|
9
|
+
spec.authors = ['Jan Graichen']
|
|
10
|
+
spec.email = ['jg@altimos.de']
|
|
11
|
+
spec.summary = %q(An option parsing library.)
|
|
12
|
+
spec.description = %q(An option parsing library. Optional.)
|
|
13
|
+
spec.homepage = ''
|
|
14
|
+
spec.license = 'LGPLv3'
|
|
15
|
+
|
|
16
|
+
spec.files = Dir['**/*'].grep(%r{^((bin|lib|test|spec|features)/|.*\.gemspec|.*LICENSE.*|.*README.*|.*CHANGELOG.*)})
|
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
|
19
|
+
spec.require_paths = %w(lib)
|
|
20
|
+
|
|
21
|
+
spec.add_development_dependency 'bundler', '~> 1.5'
|
|
22
|
+
end
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
# describe Opt::Flag do
|
|
4
|
+
# shared_examples 'defined flag' do
|
|
5
|
+
# it 'should match short code' do
|
|
6
|
+
# ret = option.parse %w(-v)
|
|
7
|
+
# expect(ret).to eq [true]
|
|
8
|
+
# end
|
|
9
|
+
|
|
10
|
+
# it 'should match long code' do
|
|
11
|
+
# ret = option.parse %w(--verbose)
|
|
12
|
+
# expect(ret).to eq [true]
|
|
13
|
+
# end
|
|
14
|
+
|
|
15
|
+
# it 'should match negated long code' do
|
|
16
|
+
# ret = option.parse %w(--no-verbose)
|
|
17
|
+
# expect(ret).to eq [false]
|
|
18
|
+
# end
|
|
19
|
+
|
|
20
|
+
# it 'should not match something else' do
|
|
21
|
+
# ret = option.parse %w(--fuu)
|
|
22
|
+
# expect(ret).to be_false
|
|
23
|
+
# end
|
|
24
|
+
# end
|
|
25
|
+
|
|
26
|
+
# context 'as simple flag' do
|
|
27
|
+
# let(:option) { Opt::Flag.new :name, '-v, --verbose' }
|
|
28
|
+
|
|
29
|
+
# it_should_behave_like 'defined flag'
|
|
30
|
+
# end
|
|
31
|
+
|
|
32
|
+
# context 'with undashed flag' do
|
|
33
|
+
# let(:option) { Opt::Flag.new :name, 'v, verbose' }
|
|
34
|
+
|
|
35
|
+
# it_should_behave_like 'defined flag'
|
|
36
|
+
# end
|
|
37
|
+
|
|
38
|
+
# context 'with multiple flags' do
|
|
39
|
+
# let(:option) { Opt::Flag.new :name, 'v, verbose, a, ausführlich' }
|
|
40
|
+
|
|
41
|
+
# it_should_behave_like 'defined flag'
|
|
42
|
+
|
|
43
|
+
# it 'should match second short code' do
|
|
44
|
+
# ret = option.parse %w(-a)
|
|
45
|
+
# expect(ret).to eq [true]
|
|
46
|
+
# end
|
|
47
|
+
|
|
48
|
+
# it 'should match second long code' do
|
|
49
|
+
# ret = option.parse %w(--ausführlich)
|
|
50
|
+
# expect(ret).to eq [true]
|
|
51
|
+
# end
|
|
52
|
+
|
|
53
|
+
# it 'should match second negated long code' do
|
|
54
|
+
# ret = option.parse %w(--no-ausführlich)
|
|
55
|
+
# expect(ret).to eq [false]
|
|
56
|
+
# end
|
|
57
|
+
# end
|
|
58
|
+
|
|
59
|
+
# context 'with single-dash long flag' do
|
|
60
|
+
# let(:option) { Opt::Flag.new :name, '-with-recommended' }
|
|
61
|
+
|
|
62
|
+
# it 'should match code' do
|
|
63
|
+
# ret = option.parse %w(-with-recommended)
|
|
64
|
+
# expect(ret).to eq [true]
|
|
65
|
+
# end
|
|
66
|
+
|
|
67
|
+
# it 'should negated code' do
|
|
68
|
+
# ret = option.parse %w(-without-recommended)
|
|
69
|
+
# expect(ret).to eq [false]
|
|
70
|
+
# end
|
|
71
|
+
# end
|
|
72
|
+
|
|
73
|
+
# context 'with already negated flag' do
|
|
74
|
+
# let(:option) { Opt::Flag.new :name, '--without-recommended' }
|
|
75
|
+
|
|
76
|
+
# it 'should match code' do
|
|
77
|
+
# ret = option.parse %w(--with-recommended)
|
|
78
|
+
# expect(ret).to eq [false]
|
|
79
|
+
# end
|
|
80
|
+
|
|
81
|
+
# it 'should negated code' do
|
|
82
|
+
# ret = option.parse %w(--without-recommended)
|
|
83
|
+
# expect(ret).to eq [true]
|
|
84
|
+
# end
|
|
85
|
+
|
|
86
|
+
# context 'with default: false' do
|
|
87
|
+
# let(:option) { Opt::Flag.new :name, '--without-recommended', default: false }
|
|
88
|
+
|
|
89
|
+
# it 'should match code' do
|
|
90
|
+
# ret = option.parse %w(--with-recommended)
|
|
91
|
+
# expect(ret).to eq [true]
|
|
92
|
+
# end
|
|
93
|
+
|
|
94
|
+
# it 'should negated code' do
|
|
95
|
+
# ret = option.parse %w(--without-recommended)
|
|
96
|
+
# expect(ret).to eq [false]
|
|
97
|
+
# end
|
|
98
|
+
# end
|
|
99
|
+
# end
|
|
100
|
+
# end
|