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.
@@ -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