argparser 1.0.0 → 1.0.1
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 +4 -4
- data/.gitignore +1 -0
- data/.hound.yml +4 -0
- data/.rubocop.yml +1063 -0
- data/.travis.yml +13 -0
- data/Gemfile +5 -0
- data/HISTORY.md +10 -0
- data/README.md +9 -8
- data/Rakefile +17 -0
- data/argparser.gemspec +20 -17
- data/argparser.sublime-project +27 -0
- data/lib/argparser.rb +45 -269
- data/lib/argparser/default_parser.rb +116 -0
- data/lib/argparser/option.rb +104 -0
- data/lib/argparser/tools.rb +56 -0
- data/spec/argparser_spec.rb +201 -0
- data/spec/spec_helper.rb +23 -0
- data/spec/tools_spec.rb +28 -0
- metadata +64 -8
- data/lib/argparser/examples/example.rb +0 -32
@@ -0,0 +1,116 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
#
|
3
|
+
|
4
|
+
class ArgParser
|
5
|
+
module DefaultParser
|
6
|
+
# Uses ARGV by default, but you may supply your own arguments
|
7
|
+
# It exits if bad arguments given or they aren't validated.
|
8
|
+
def parse!(arguments = ARGV)
|
9
|
+
options.each(&:reset!)
|
10
|
+
_check_manifest!
|
11
|
+
|
12
|
+
OPTS_RESERVED.each { |o|
|
13
|
+
next unless arguments.include?("--#{o}")
|
14
|
+
self[o].set_value(nil)
|
15
|
+
self[o].validate!(self)
|
16
|
+
self[o].reset! # If it didn't terminate while validating
|
17
|
+
}
|
18
|
+
|
19
|
+
args = arguments.dup
|
20
|
+
enough = false
|
21
|
+
while (a = args.shift)
|
22
|
+
if a == OPT_ENOUGH
|
23
|
+
enough = true
|
24
|
+
elsif enough || (a =~ /^[^-]/) || (a == '-')
|
25
|
+
_set_argument!(a)
|
26
|
+
elsif a =~ /^--(.+)/
|
27
|
+
_set_long_option!(a, args)
|
28
|
+
elsif a =~ /^-([^-].*)/
|
29
|
+
_set_short_options!(a, args)
|
30
|
+
else
|
31
|
+
terminate(2, OUT_UNKNOWN_OPTION % a)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
options.each { |o|
|
36
|
+
o.set_default!
|
37
|
+
o.on_first_error {|msg| terminate(2, msg)}
|
38
|
+
}
|
39
|
+
|
40
|
+
options.each { |o|
|
41
|
+
terminate(2, OUT_INVALID_OPTION % o.name) unless o.validate!(self)
|
42
|
+
}
|
43
|
+
|
44
|
+
self
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def _set_argument!(a)
|
50
|
+
if (input = inputs.find{|i| !i.value || i.multiple})
|
51
|
+
input.set_value(a)
|
52
|
+
else
|
53
|
+
terminate(2, OUT_UNEXPECTED_ARGUMENT % a)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def _set_long_option!(a, tail)
|
58
|
+
a = a[2..-1]
|
59
|
+
if a.size > 1 && (option = self[a]) && !option.input
|
60
|
+
if option.argument
|
61
|
+
terminate(2, OUT_OPTION_ARGUMENT_EXPECTED % a) if tail.empty?
|
62
|
+
option.set_value(tail.shift)
|
63
|
+
else
|
64
|
+
option.set_value(nil)
|
65
|
+
end
|
66
|
+
else
|
67
|
+
terminate(2, OUT_UNKNOWN_OPTION % $1)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def _set_short_options!(a, tail)
|
72
|
+
(a = a[1..-1]).chars.each_with_index do |char, index|
|
73
|
+
unless (option = self[char]) && !option.input
|
74
|
+
terminate(2, OUT_UNKNOWN_OPTION % char)
|
75
|
+
end
|
76
|
+
if option.argument
|
77
|
+
if a.size-1 == index
|
78
|
+
terminate(2, OUT_OPTION_ARGUMENT_EXPECTED % char) if tail.empty?
|
79
|
+
option.set_value(tail.shift)
|
80
|
+
else
|
81
|
+
option.set_value(a[index+1..-1])
|
82
|
+
break
|
83
|
+
end
|
84
|
+
else
|
85
|
+
option.set_value(nil)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def _check_manifest!
|
91
|
+
{:program => program, :version => version}.each do |k, v|
|
92
|
+
terminate(2, OUT_MANIFEST_EXPECTED % k) if !v || v.to_s.strip.empty?
|
93
|
+
end
|
94
|
+
|
95
|
+
is = inputs
|
96
|
+
is.each_with_index do |i, index|
|
97
|
+
if index < is.length-1 && i.multiple
|
98
|
+
terminate(2, OUT_MULTIPLE_INPUTS % i.name)
|
99
|
+
elsif i.names.size > 1
|
100
|
+
terminate(2, OUT_MULTIPLE_NAMES % i.name)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
opt = is.index{|i| !i.required} || is.size
|
104
|
+
req = is.rindex{|i| i.required} || 0
|
105
|
+
terminate(2, OUT_REQUIRED % is[req].name) if req > opt
|
106
|
+
|
107
|
+
names = {}
|
108
|
+
options.each do |option|
|
109
|
+
option.names.each do |name|
|
110
|
+
terminate(2, OUT_UNIQUE_NAME % name) if names.has_key?(name)
|
111
|
+
names[name] = option
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
class ArgParser
|
4
|
+
class Option
|
5
|
+
include Tools
|
6
|
+
attr_reader :names # Names of an option (short, long, etc.)
|
7
|
+
attr_reader :argument # Name of an argument, if present
|
8
|
+
attr_reader :help # Help string for an option
|
9
|
+
attr_reader :validate # Lambda(option, parser) to validate an option
|
10
|
+
attr_reader :default # Default value for an option
|
11
|
+
attr_reader :input # Option is an input argument
|
12
|
+
attr_reader :required # Option required
|
13
|
+
attr_reader :multiple # Option may occure multiple times
|
14
|
+
attr_reader :count # Option occucences
|
15
|
+
attr_reader :env # Default option set by this ENV VAR, if any
|
16
|
+
attr_reader :eval # Default option set by this eval,
|
17
|
+
# superseded by :env, if any
|
18
|
+
# So, in order: value - env - eval - default
|
19
|
+
attr_accessor :value # Values of an option, Array if multiple
|
20
|
+
|
21
|
+
# Returns most lengthy name as a 'default' name of this option
|
22
|
+
def name
|
23
|
+
names.first
|
24
|
+
end
|
25
|
+
|
26
|
+
# Constructs option from Hash of properties (see attr_readers)
|
27
|
+
def initialize(o_manifest)
|
28
|
+
hash2vars!(o_manifest)
|
29
|
+
@names = Array(names).map{|n| n.to_s.strip}.
|
30
|
+
sort{|n1, n2| n1.size <=> n2.size}
|
31
|
+
reset!
|
32
|
+
end
|
33
|
+
|
34
|
+
# Sets value. Do not use this directly
|
35
|
+
def set_value(v)
|
36
|
+
@count += 1
|
37
|
+
multiple ? (@value << v).flatten! : @value = v
|
38
|
+
end
|
39
|
+
|
40
|
+
# Does option contain it's value?
|
41
|
+
def value?
|
42
|
+
multiple ? !value.compact.empty? : !!value
|
43
|
+
end
|
44
|
+
|
45
|
+
def to_s
|
46
|
+
if multiple
|
47
|
+
value.empty? ? '' : value.map(&:to_s).join(', ')
|
48
|
+
else
|
49
|
+
value.to_s
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def synopsis
|
54
|
+
if input
|
55
|
+
s = name.dup
|
56
|
+
s << '...' if multiple
|
57
|
+
s = "[#{s}]" if !required
|
58
|
+
s
|
59
|
+
else
|
60
|
+
s = names.map{|n| n.size == 1 ? "-#{n}" : "--#{n}"}.join(', ')
|
61
|
+
s << " #{argument}" if argument
|
62
|
+
s = "[#{s}]" if !required
|
63
|
+
s << '...' if multiple
|
64
|
+
s
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def validate!(parser)
|
69
|
+
!validate || validate.call(self, parser)
|
70
|
+
end
|
71
|
+
|
72
|
+
def reset!
|
73
|
+
@value = multiple ? [] : nil
|
74
|
+
@count = 0
|
75
|
+
end
|
76
|
+
|
77
|
+
def printed_help
|
78
|
+
s = help || ''
|
79
|
+
s << "\n\tDefaults to: #{default}" if default
|
80
|
+
"%s\n\t%s" % [synopsis, s]
|
81
|
+
end
|
82
|
+
|
83
|
+
def set_default!
|
84
|
+
return self unless !value? && (argument || input)
|
85
|
+
# rubocop:disable Lint/Eval
|
86
|
+
e = ((env && ENV[env]) || (eval && safe_return(eval)) || default)
|
87
|
+
# rubocop:enable Lint/Eval
|
88
|
+
set_value(e) if e
|
89
|
+
self
|
90
|
+
end
|
91
|
+
|
92
|
+
def on_first_error
|
93
|
+
if !multiple && count > 1
|
94
|
+
yield(OUT_SINGLE_OPTION % name)
|
95
|
+
elsif required && count < 1
|
96
|
+
yield((input ? OUT_ARGUMENT_EXPECTED : OUT_OPTION_EXPECTED) % name)
|
97
|
+
elsif names.find(&:empty?)
|
98
|
+
yield(OUT_OPTION_NULL)
|
99
|
+
elsif required && default
|
100
|
+
yield(OUT_REQUIRED_DEFAULT % name)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
class ArgParser
|
4
|
+
# Aux tools intented to include into a class
|
5
|
+
module Tools
|
6
|
+
# Sets self state from a hash given
|
7
|
+
def hash2vars!(hash)
|
8
|
+
if hash.kind_of?(Hash) || (hash.respond_to?(:to_h) && (hash = hash.to_h))
|
9
|
+
hash.each do |k, v|
|
10
|
+
next unless self.respond_to?(k)
|
11
|
+
instance_variable_set("@#{k}", v)
|
12
|
+
end
|
13
|
+
else
|
14
|
+
raise 'Hash expected'
|
15
|
+
end
|
16
|
+
self
|
17
|
+
end
|
18
|
+
|
19
|
+
# Returns a hash of self state, packing all objects to hashes
|
20
|
+
def to_hash
|
21
|
+
instance_variables.reduce({}) { |hash, var|
|
22
|
+
hash[var[1..-1]] = instance_variable_get(var)
|
23
|
+
hash }
|
24
|
+
end
|
25
|
+
|
26
|
+
# Eval ruby code.
|
27
|
+
# Returns result of Kernel.eval or nil if some errors occure
|
28
|
+
def safe_return(str)
|
29
|
+
# rubocop:disable Lint/Eval
|
30
|
+
eval(str)
|
31
|
+
# rubocop:enable Lint/Eval
|
32
|
+
rescue NameError, NoMethodError
|
33
|
+
nil
|
34
|
+
end
|
35
|
+
|
36
|
+
=begin Deep pack
|
37
|
+
def to_hash(deep = true)
|
38
|
+
instance_variables.reduce({}) { |hash, var|
|
39
|
+
value = instance_variable_get(var)
|
40
|
+
hash[var[1..-1]] = deep ? value_to_hash(value) : value
|
41
|
+
hash }
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
def value_to_hash value
|
46
|
+
if value.respond_to?(:to_hash)
|
47
|
+
(v = value.to_hash).kind_of?(Hash) ? v : {}
|
48
|
+
elsif value.respond_to?(:to_a)
|
49
|
+
(v = value.to_a).kind_of?(Array) ? v.map{|v| value_to_hash(v)} : []
|
50
|
+
else
|
51
|
+
value
|
52
|
+
end
|
53
|
+
end
|
54
|
+
=end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,201 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
require 'spec_helper'
|
3
|
+
|
4
|
+
a_manifest = {
|
5
|
+
:program => 'a_example', # Use additional properties like these:
|
6
|
+
:version => '1.0', # :info, :copyright, :license,
|
7
|
+
:options => [{ # :package, :bugs, :homepage
|
8
|
+
:names => %w[m mode],
|
9
|
+
:argument => 'first|second|third',
|
10
|
+
:default => 'first',
|
11
|
+
:multiple => true,
|
12
|
+
:help => 'Example mode.',
|
13
|
+
:validate => (lambda {|this, _parser| # Validating value in-line
|
14
|
+
possible = this.argument.split('|')
|
15
|
+
this.value.select{|v| possible.include?(v)}.size == this.value.size })
|
16
|
+
}, {
|
17
|
+
:names => 'file',
|
18
|
+
:input => true,
|
19
|
+
:required => false,
|
20
|
+
:default => '-',
|
21
|
+
:help => 'Filename or - for stdin.'
|
22
|
+
}]
|
23
|
+
}
|
24
|
+
|
25
|
+
describe 'manifest' do
|
26
|
+
it 'should compile option objects' do
|
27
|
+
args = ArgParser.new(a_manifest)
|
28
|
+
args.options.each {|o|
|
29
|
+
o.must_be_instance_of(ArgParser::Option)
|
30
|
+
args[o.name].must_be_same_as(o)
|
31
|
+
}
|
32
|
+
(a = args.parse!(%w[-])).must_be_instance_of(ArgParser)
|
33
|
+
a.must_be_same_as(args)
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'should require program' do
|
37
|
+
b_manifest = a_manifest.merge(:program => '')
|
38
|
+
args = ArgParser.new(b_manifest)
|
39
|
+
lambda {
|
40
|
+
args.parse!([])
|
41
|
+
}.must_raise(ExitStub).status.must_equal(2)
|
42
|
+
end
|
43
|
+
|
44
|
+
it 'should require version' do
|
45
|
+
b_manifest = a_manifest.merge(:version => nil)
|
46
|
+
lambda {
|
47
|
+
ArgParser.new(b_manifest).parse!([])
|
48
|
+
}.must_raise(ExitStub).status.must_equal(2)
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'requires nothing but but program & version' do
|
52
|
+
b_manifest = a_manifest.reduce({}) {|h, (k, v)|
|
53
|
+
h[k] = v if [:program, :version].include?(k); h}
|
54
|
+
ArgParser.new(b_manifest).parse!([]).must_be_instance_of(ArgParser)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
describe 'Built-in options' do
|
59
|
+
it 'prints out version and terminates' do
|
60
|
+
a = ArgParser.new(a_manifest)
|
61
|
+
e = lambda {
|
62
|
+
a.parse!(%w[any --othelp --version])
|
63
|
+
}.must_raise(ExitStub)
|
64
|
+
e.status.must_equal(0)
|
65
|
+
e.message.must_match(/#{a_manifest[:program]}.*#{a_manifest[:version]}/)
|
66
|
+
end
|
67
|
+
|
68
|
+
it 'prints out help and terminates' do
|
69
|
+
a = ArgParser.new(a_manifest)
|
70
|
+
e = lambda {
|
71
|
+
a.parse!(%w[any --oth --help])
|
72
|
+
}.must_raise(ExitStub)
|
73
|
+
e.status.must_equal(0)
|
74
|
+
e.message.must_match(/#{a_manifest[:options].last[:help]}/)
|
75
|
+
end
|
76
|
+
|
77
|
+
it 'prints synopsys on argument error' do
|
78
|
+
a = ArgParser.new(a_manifest)
|
79
|
+
e = lambda {
|
80
|
+
a.parse!(%w[any --1287])
|
81
|
+
}.must_raise(ExitStub)
|
82
|
+
e.status.must_equal(2)
|
83
|
+
e.message.must_match(/#{a.synopsis}/)
|
84
|
+
end
|
85
|
+
|
86
|
+
it 'prints user-defined help if specified' do
|
87
|
+
b_manifest = a_manifest.merge(:help => 'User-defined help')
|
88
|
+
a = ArgParser.new(b_manifest)
|
89
|
+
e = lambda {
|
90
|
+
a.parse!(%w[--help])
|
91
|
+
}.must_raise(ExitStub)
|
92
|
+
e.status.must_equal(0)
|
93
|
+
e.message.must_match(/#{b_manifest[:help]}/)
|
94
|
+
end
|
95
|
+
|
96
|
+
it 'understands -- argument' do
|
97
|
+
@args = ArgParser.new(a_manifest)
|
98
|
+
mode = @args.parse!(%w[-- --mode])['mode']
|
99
|
+
mode.count.must_equal(1)
|
100
|
+
mode.value.first.must_equal(mode.default)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
describe 'Multiple argumented options w/default value' do
|
105
|
+
before do
|
106
|
+
@args = ArgParser.new(a_manifest)
|
107
|
+
end
|
108
|
+
|
109
|
+
it 'reads them and gives out values in order' do
|
110
|
+
@args.parse!(%w[--mode second -m first -msecond -- -])
|
111
|
+
@args['mode'].value.must_equal(%w[second first second])
|
112
|
+
end
|
113
|
+
|
114
|
+
it 'doesn''t validate unknown value' do
|
115
|
+
lambda {
|
116
|
+
@args.parse!(%w[--mode second -m foo -msecond -])
|
117
|
+
}.must_raise(ExitStub).status.must_equal(2)
|
118
|
+
end
|
119
|
+
|
120
|
+
it 'doesn''t understand unknown options' do
|
121
|
+
lambda {
|
122
|
+
@args.parse!(%w[--mode second -abm foo -m first -- -])
|
123
|
+
}.must_raise(ExitStub).status.must_equal(2)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
describe 'required option' do
|
128
|
+
before do
|
129
|
+
@b_manifest = a_manifest.merge({
|
130
|
+
:options => (a_manifest[:options] + [{
|
131
|
+
:names => %w[r required legacy-required],
|
132
|
+
:required => true,
|
133
|
+
:multiple => true
|
134
|
+
}])
|
135
|
+
})
|
136
|
+
@args = ArgParser.new(@b_manifest)
|
137
|
+
end
|
138
|
+
|
139
|
+
it 'is really not optional' do
|
140
|
+
e = lambda { @args.parse!(%w[-- file]) }.must_raise(ExitStub)
|
141
|
+
e.status.must_equal(2)
|
142
|
+
end
|
143
|
+
|
144
|
+
it 'is actually multiple too' do
|
145
|
+
@args.parse!(%w[-rrrrr --legacy-required --required -r -- file])
|
146
|
+
@args['required'].count.must_equal(8)
|
147
|
+
end
|
148
|
+
|
149
|
+
it 'lives with an optional one' do
|
150
|
+
@args.parse!(%w[--mode first -rmsecond])
|
151
|
+
@args['required'].count.must_equal(1)
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
describe 'input argument' do
|
156
|
+
before do
|
157
|
+
@b_manifest = a_manifest.merge({
|
158
|
+
:options => (a_manifest[:options] + [{
|
159
|
+
:names => %w[file2],
|
160
|
+
:input => true
|
161
|
+
}])
|
162
|
+
})
|
163
|
+
@args = ArgParser.new(@b_manifest)
|
164
|
+
end
|
165
|
+
|
166
|
+
it 'terminates if name used as an option' do
|
167
|
+
lambda { @args.parse!(%w[--file2 --]) }.must_raise(ExitStub)
|
168
|
+
end
|
169
|
+
|
170
|
+
it 'survives second optional argument' do
|
171
|
+
@args.parse!(%w[file2])
|
172
|
+
@args['file'].value.must_equal('file2')
|
173
|
+
@args['file2'].value.must_equal(@args['file2'].default)
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
describe 'optional tiny features' do
|
178
|
+
before do
|
179
|
+
@b_manifest = a_manifest.merge({
|
180
|
+
:options => (a_manifest[:options] + [{
|
181
|
+
:names => ['a', "\n aaaaa \t "],
|
182
|
+
:multiple => true,
|
183
|
+
:argument => 'arg'
|
184
|
+
}])
|
185
|
+
})
|
186
|
+
@args = ArgParser.new(@b_manifest)
|
187
|
+
end
|
188
|
+
|
189
|
+
it 'ignores whitespace in option names' do
|
190
|
+
"\n aaaaa \t ".strip.must_equal('aaaaa')
|
191
|
+
@args.parse!(%w[--aaaaa foobar])
|
192
|
+
@args['aaaaa'].value.must_equal(['foobar'])
|
193
|
+
end
|
194
|
+
|
195
|
+
it 'allows to get value as string' do
|
196
|
+
@args.parse!(%w[--aaaaa foo])
|
197
|
+
"#{@args['aaaaa']}".must_equal('foo')
|
198
|
+
@args.parse!(%w[--aaaaa foo -abar])
|
199
|
+
"#{@args['aaaaa']}".must_equal('foo, bar')
|
200
|
+
end
|
201
|
+
end
|