hirobumi-argument_sensei 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc ADDED
@@ -0,0 +1,57 @@
1
+ = ArgumentSensei
2
+
3
+ ArgumentSensei is yet another command-line argument parser with a minimal set of features.
4
+
5
+ == Installation
6
+
7
+ sudo gem install hirobumi-argument_sensei -s http://gems.github.com --no-ri --no-rdoc
8
+
9
+ == Usage
10
+
11
+ require 'rubygems'
12
+ require 'argument_sensei'
13
+
14
+ commands = ArgumentSensei.define do
15
+
16
+ option 'help', :short => 'h' do
17
+ arguments 0
18
+ describe 'show this message'
19
+ action do
20
+ # commands to run when -h or --help is given.
21
+ end # action do
22
+ end # option 'help', :short => 'h' do
23
+
24
+ default 'help'
25
+
26
+ end
27
+
28
+ # ...
29
+
30
+ # in the place you want to run the codes...
31
+ commands.run!
32
+
33
+
34
+ == License
35
+
36
+ Copyright (c) 2009 Hirobumi Hama
37
+
38
+ Permission is hereby granted, free of charge, to any person
39
+ obtaining a copy of this software and associated documentation
40
+ files (the "Software"), to deal in the Software without
41
+ restriction, including without limitation the rights to use,
42
+ copy, modify, merge, publish, distribute, sublicense, and/or sell
43
+ copies of the Software, and to permit persons to whom the
44
+ Software is furnished to do so, subject to the following
45
+ conditions:
46
+
47
+ The above copyright notice and this permission notice shall be
48
+ included in all copies or substantial portions of the Software.
49
+
50
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
51
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
52
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
53
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
54
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
55
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
56
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
57
+ OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,40 @@
1
+ require 'rubygems'
2
+ require 'rake/gempackagetask'
3
+ require 'rake/testtask'
4
+
5
+ require 'lib/argument_sensei/version'
6
+
7
+ task :default => :test
8
+
9
+ spec = Gem::Specification.new do |s|
10
+ s.name = 'argument_sensei'
11
+ s.version = ArgumentSensei::Version.to_s
12
+ s.has_rdoc = true
13
+ s.extra_rdoc_files = %w(README.rdoc)
14
+ s.rdoc_options = %w(--main README.rdoc)
15
+ s.summary = "ArgumentSensei is yet another command-line argument parser with a minimal set of features."
16
+ s.author = 'Hirobumi Hama'
17
+ s.email = 'hama@yoidore.org'
18
+ s.homepage = 'http://yoidore.org/argument_sensei'
19
+ s.files = %w(README.rdoc Rakefile) + Dir.glob("{lib,test}/**/*")
20
+ # s.executables = ['argument_sensei']
21
+
22
+ # s.add_dependency('gem_name', '~> 0.0.1')
23
+ end
24
+
25
+ Rake::GemPackageTask.new(spec) do |pkg|
26
+ pkg.gem_spec = spec
27
+ end
28
+
29
+ Rake::TestTask.new do |t|
30
+ t.libs << 'test'
31
+ t.test_files = FileList["test/**/*_test.rb"]
32
+ t.verbose = true
33
+ end
34
+
35
+ desc 'Generate the gemspec to serve this Gem from Github'
36
+ task :github do
37
+ file = File.dirname(__FILE__) + "/#{spec.name}.gemspec"
38
+ File.open(file, 'w') {|f| f << spec.to_ruby }
39
+ puts "Created gemspec: #{file}"
40
+ end
@@ -0,0 +1,259 @@
1
+ module ArgumentSensei
2
+
3
+ class Commands
4
+
5
+ include Enumerable
6
+
7
+ def initialize(opts = {})
8
+ @arguments = opts[:arguments] || ARGV || []
9
+ @options = opts[:options] || []
10
+ @queue = {}
11
+ @default = nil
12
+ end # def initialize(opts = {})
13
+
14
+ attr_reader :arguments, :options
15
+
16
+ def method_missing(method_name, *args, &block)
17
+ if @options.respond_to?(method_name)
18
+ @options.send(method_name, *args, &block)
19
+ else
20
+ raise(NameError.new("undefined local variable or method '#{method_name}' for both #{inspect} and its @options"))
21
+ end
22
+ end
23
+
24
+ # parses and runs actions if necessary, returns a hash of command-line options
25
+ def run!
26
+ count = 0
27
+
28
+ h = parse
29
+ keys = h.keys
30
+
31
+ opts = @options.select { |o| o[:priority] == :option && (keys.include?(o[:long]) || keys.include?(o[:short])) } || []
32
+ runs = @options.select { |o| o[:priority] == :runner && (keys.include?(o[:long]) || keys.include?(o[:short])) } || []
33
+
34
+ # TODO: Exception message can be more informative.
35
+ raise(RuntimeError.new(runs.map { |o| "--#{o[:long]}/-#{o[:short]}" }.join(' ') + ' cannot be ran at the same time')) if runs.length > 1
36
+
37
+ (opts + runs).each do |o|
38
+ next unless o[:action]
39
+ if h.keys.include?(o[:short]) || h.keys.include?(o[:long])
40
+ o[:action].call(h[o[:short] || o[:long]])
41
+ count += 1
42
+ end
43
+ end # @options.each do |o|
44
+
45
+ if count == 0 && !@default.nil? # runs default if given
46
+ definition(@default)[:action].call(nil)
47
+ end # if count == 0
48
+
49
+ end # def run!
50
+
51
+ # returns a hash of command-line options
52
+ def parse
53
+ arr = []
54
+ for_arguments = false
55
+ temp = []
56
+
57
+ # splits grouped booelean short options
58
+ @arguments.collect! do |x|
59
+ unless x =~ /^-\w{2,}/
60
+ x
61
+ else
62
+ x.gsub(/^-/, '').split(//).map do |y|
63
+ raise(ArgumentError.new("option #{y} (which takes #{definition(y)[:arguments]}) should not be combined (#{x})")) if definition(y) && definition(y)[:arguments] != 0
64
+ '-' + y
65
+ end # x.gsub(/^-/, '').split(//).map do |y|
66
+ end
67
+ end.flatten! # @arguments.collect! do |x|
68
+
69
+ @arguments.each do |x|
70
+
71
+ if for_arguments && ((argument_length(temp[0]) != :all && temp.length - 1 >= argument_length(temp[0])) || x =~ /^-+/)
72
+ arr << temp
73
+ for_arguments = false
74
+ temp = []
75
+ end # if for_arguments && temp.length - 1 >= argument_length(temp[0])
76
+
77
+ if x =~ /^-+/
78
+ unless option_defined?(x)
79
+ arr << x
80
+ else
81
+ for_arguments = true
82
+ temp << x
83
+ end
84
+ elsif for_arguments # if x =~ /^-+/
85
+ temp << x
86
+ else # elsif for_arguments if x =~ /^-+/
87
+ arr << x
88
+ end # end elsif for_arguments if x =~ /^-+/
89
+
90
+ end # each do |x|
91
+
92
+ arr << temp if temp.length > 0
93
+
94
+ hash = {}
95
+ hash[:orphans] = arr.select { |a| !a.is_a?(Array) }
96
+ (arr - hash[:orphans]).each do |a|
97
+ gsubbed = a[0].gsub(/^-+/, '')
98
+ if argument_length(gsubbed) == 0
99
+ hash[gsubbed] = true
100
+ else
101
+ hash[gsubbed] = a[1..-1]
102
+ end
103
+ end # (arr - hash[:orphans]).each do |a|
104
+
105
+ # make sure both :short and :long are filled if they are provided
106
+ each do |c|
107
+ short, long = [c[:short], c[:long]].map { |n| (n || '').gsub(/^-+/, '') }
108
+ next if short == '' || long == ''
109
+
110
+ if hash[short] || hash[long]
111
+ next if hash[short] == hash[long]
112
+ if hash[short].nil?
113
+ hash[short] = hash[long]
114
+ else
115
+ hash[long] = hash[short]
116
+ end
117
+ end
118
+
119
+ end # each do |c|
120
+
121
+ hash # returns
122
+ end # def parse
123
+
124
+ # finds a detailed option for given name
125
+ def definition(name)
126
+ n = name.gsub(/^-+/, '')
127
+ select { |h| h[:short] == n || h[:long] == n }[0] # can be nil
128
+ end
129
+
130
+ def option_defined?(name)
131
+ !!definition(name)
132
+ end # def defined?(name)
133
+
134
+ def argument_length(name)
135
+ definition(name)[:arguments]
136
+ end # def arguments(name)
137
+
138
+ def value(name)
139
+ definition(name)[:value]
140
+ end # def value(name)
141
+
142
+ def set_value(name, given_value)
143
+ hash = select { |h| h[:short] == name || h[:long] == name }[0]
144
+ raise(ArgumentError.new("option with #{name} could not be found")) if hash.nil?
145
+ @options[index(hash)] = hash.merge(:value => given_value) # [index(hash)]= gives you a syntax error
146
+ end # def set_value(name, given_value)
147
+
148
+ # shows help message to STDOUT
149
+ def help
150
+ STDERR.puts("USAGE: #{$0}")
151
+
152
+ @options.each do |o|
153
+
154
+ temp = ''
155
+
156
+ o[:short] ? temp << "-#{o[:short]}" : temp << "\t"
157
+ temp.length > 0 ? temp << ", " : temp << "\t"
158
+ o[:long] ? temp << "--#{o[:long]}" : temp << "\t"
159
+
160
+ temp << "\t\t"
161
+ temp << o[:description]
162
+
163
+ if o[:description]
164
+ temp << ' '
165
+ end
166
+
167
+ case o[:arguments]
168
+ when :all
169
+ temp << "(multiple arguments)"
170
+ when 0
171
+ temp << "(boolean)"
172
+ when 1
173
+ temp << "(1 argument)"
174
+ else
175
+ temp << "(#{o[:arguments]} arguments)"
176
+ end
177
+ puts temp
178
+
179
+ end # @options.each do |o|
180
+
181
+ nil
182
+ end # def help
183
+
184
+ def option(name, opts = {}, &block)
185
+ @queue = {
186
+ :short => opts[:short], # without hyphens
187
+ :long => opts[:long], # this too
188
+ :description => '',
189
+ :arguments => :all, # Integer, :all
190
+ :action => nil, # a Proc object, rarity should be :arguments, use *args if :all.
191
+ :priority => :option # priority, :option first then :runner. only one :runner should be called at a time.
192
+ } # defaults
193
+
194
+ if name.length > 1
195
+ @queue[:long] = name
196
+ else
197
+ @queue[:short] = name
198
+ end
199
+
200
+ instance_eval(&block) if block_given?
201
+
202
+ flush!
203
+ end
204
+
205
+ private
206
+
207
+ # flushes @queue into @commands array, used to implement DSL.
208
+ def flush!
209
+ return nil if @queue.empty?
210
+
211
+ self << @queue
212
+ @queue = {}
213
+ self[-1]
214
+ end
215
+
216
+ def describe(desc = '', opts = {})
217
+ @queue[:description] = desc
218
+ end
219
+
220
+ # Integer or :all (= default), defaults are set in #option method
221
+ def arguments(n, opts = {})
222
+ @queue[:arguments] = n
223
+ end
224
+
225
+ def action(opts = {}, &block)
226
+ @queue[:action] = block
227
+ end
228
+
229
+ # sets priority for the option, default is :option.
230
+ def priority(type)
231
+ raise(ArgumentError.new("priority must be :option or :runner but :#{type} given.")) unless [:option, :runner].include?(type)
232
+ @queue[:priority] = type
233
+ end # def priority(type)
234
+
235
+ def default(name)
236
+ @default = name
237
+ end
238
+
239
+ class << self
240
+
241
+ # Commands.define do
242
+ # option 'run', :short => 'S' do
243
+ # action do ||
244
+ # end
245
+ # end
246
+ def define(opts = {}, &block)
247
+ raise(ArgumentError.new('no block given')) unless block_given?
248
+
249
+ c = Commands.new(opts)
250
+ c.instance_eval(&block)
251
+
252
+ c
253
+ end
254
+
255
+ end
256
+
257
+ end # class Commands
258
+
259
+ end # module ArgumentSensei
@@ -0,0 +1,13 @@
1
+ module ArgumentSensei
2
+ module Version
3
+
4
+ MAJOR = 0
5
+ MINOR = 1
6
+ TINY = 0
7
+
8
+ def self.to_s # :nodoc:
9
+ [MAJOR, MINOR, TINY].join('.')
10
+ end
11
+
12
+ end
13
+ end
@@ -0,0 +1,7 @@
1
+ $:.unshift File.dirname(__FILE__)
2
+
3
+ module ArgumentSensei
4
+ end # module ArgumentSensei
5
+
6
+ require 'argument_sensei/version'
7
+ require 'argument_sensei/commands'
@@ -0,0 +1,9 @@
1
+ # http://sneaq.net/textmate-wtf
2
+ $:.reject! { |e| e.include? 'TextMate' }
3
+
4
+ require 'rubygems'
5
+ require 'test/unit'
6
+ gem 'thoughtbot-shoulda', '>= 2.10.1'
7
+ require 'shoulda'
8
+
9
+ require File.dirname(__FILE__) + '/../lib/argument_sensei'
@@ -0,0 +1,202 @@
1
+ require File.dirname(__FILE__) + '/../test_helper'
2
+
3
+ class ArgumentSenseiTest < Test::Unit::TestCase
4
+
5
+ Commands = ArgumentSensei::Commands
6
+
7
+ context 'Commands' do
8
+
9
+ should 'parses accordingly with defined options' do
10
+
11
+ c = Commands.define(:arguments => %w(orphan1 -S short1 short2 orphan2 --long1 long1 long2 long3 --long2 long4 long5 orphan3 --boolean)) do
12
+ option 'S' do
13
+ arguments 2
14
+ end
15
+ option 'long1'
16
+ option 'long2', :short => 'L' do
17
+ arguments 2
18
+ describe 'takes long4 and long5 then leaves orphan3'
19
+ end
20
+ option 'boolean' do
21
+ arguments 0
22
+ end
23
+ end # c = Commands.define do
24
+
25
+ assert_equal({
26
+ :orphans => %w(orphan1 orphan2 orphan3),
27
+ 'S' => %w(short1 short2),
28
+ 'long1' => %w(long1 long2 long3),
29
+ 'long2' => %w(long4 long5),
30
+ 'L' => %w(long4 long5),
31
+ 'boolean' => true
32
+ }, c.parse)
33
+
34
+ c = Commands.define(:arguments => %w(--what if there --are nothing defined)) do
35
+ end
36
+
37
+ assert_nothing_raised { c.parse.inspect }
38
+
39
+ end
40
+
41
+ should 'parses combined short boolean switches' do
42
+ # -abcDEF => -a -b -c -D -E -F, as long as they are boolean.
43
+ # if non-booleans included, returns error,
44
+ # unknowns will just be ignored.
45
+ c = Commands.define(:arguments => %w(-aBC -A forA)) do
46
+ option 'a' do
47
+ arguments 0
48
+ end
49
+ option 'B' do
50
+ arguments 0
51
+ end
52
+ option 'C' do
53
+ arguments 0
54
+ end
55
+ option 'A' do
56
+ arguments 1
57
+ end
58
+ end.parse # c = Commands.define(:arguments => %w()) do
59
+ assert(c['a'])
60
+ assert(c['B'])
61
+ assert(c['C'])
62
+ assert_equal(['forA'], c['A'])
63
+
64
+ # non boolean should not be combined.
65
+ assert_raise(ArgumentError) do
66
+ c = Commands.define(:arguments => %w(-aB)) do
67
+ option 'a' do
68
+ arguments 0
69
+ end
70
+ option 'B' do
71
+ arguments :all
72
+ end
73
+ end.parse
74
+ end
75
+ end
76
+
77
+ should 'runs given actions' do
78
+ done = false
79
+ should_not_do = true
80
+ Commands.define(:arguments => %w(--do)) do
81
+ option 'do' do
82
+ action do
83
+ done = true
84
+ end
85
+ end
86
+ option 'do not' do
87
+ action do
88
+ should_not_do = false
89
+ end
90
+ end
91
+ end.run! # c = Commands.define do
92
+ assert(done, 'action were not called')
93
+ assert(should_not_do, 'even other action got run')
94
+ end
95
+
96
+ should 'runs default action' do
97
+ done = false
98
+ Commands.define do
99
+ option 'D' do
100
+ action do
101
+ done = true
102
+ end
103
+ end
104
+ default 'D'
105
+ end.run!
106
+ assert(done, 'default action was not called')
107
+ end
108
+
109
+ should 'should not raise any error for undefined options' do
110
+ assert_nothing_raised(Exception) do
111
+ Commands.define(:arguments => %w(-these -commands --are not defined --yet)) do
112
+ option 't' do
113
+ arguments 0
114
+ end
115
+ option 'o' do
116
+ arguments 0
117
+ end
118
+ end
119
+ end
120
+ end
121
+
122
+ context 'Prioritization' do
123
+
124
+ should 'should be defined as option' do
125
+ assert_equal(:option, Commands.define { option('-t') { priority :option } }.options[0][:priority])
126
+ end
127
+
128
+ should 'should be defined as runner' do
129
+ assert_equal(:runner, Commands.define { option('-t') { priority :runner } }.options[0][:priority])
130
+ end
131
+
132
+ should 'runs options first then a runner' do
133
+ first = false; last = false
134
+ was_first_last = false
135
+ was_last_first = false
136
+
137
+ assert_nothing_raised(Exception) do
138
+ Commands.define(:arguments => %w(-l -f)) do
139
+ option 'l' do
140
+ priority :runner
141
+ action do
142
+
143
+ last = true
144
+ was_last_first = true unless first
145
+
146
+ end
147
+ end
148
+ option 'f' do
149
+ priority :option
150
+ action do
151
+
152
+ first = true
153
+ was_first_last = true if last
154
+
155
+ end
156
+ end
157
+ end.run!
158
+ end
159
+
160
+ assert(!was_last_first, 'Runner ran first.')
161
+ assert(!was_first_last, 'Option ran last.')
162
+
163
+ end
164
+
165
+ should 'should not accept multiple runners at a time' do
166
+ shouldnotrun1 = false
167
+ shouldnotrun2 = false
168
+ assert_raise(RuntimeError) do
169
+ Commands.define(:arguments => %w(-a -b)) do
170
+ option 'a' do
171
+ priority :runner
172
+ action do
173
+ shouldnotrun1 = true
174
+ end
175
+ end
176
+ option 'b' do
177
+ priority :runner
178
+ action do
179
+ shouldnotrun2 = true
180
+ end
181
+ end
182
+ end.run!
183
+ end
184
+ assert(!shouldnotrun1)
185
+ assert(!shouldnotrun2)
186
+ end
187
+
188
+ end # context 'Priority' do
189
+
190
+ end # context 'Commands' do
191
+
192
+ context 'ArgumentSensei' do
193
+
194
+ context 'Class' do
195
+ end # Class
196
+
197
+ context 'Instance' do
198
+ end # Instance
199
+
200
+ end # ArgumentSensei
201
+
202
+ end # class ArgumentSenseiTest < Test::Unit::TestCase
metadata ADDED
@@ -0,0 +1,62 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: hirobumi-argument_sensei
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Hirobumi Hama
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-04-10 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description:
17
+ email: hama@yoidore.org
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files:
23
+ - README.rdoc
24
+ files:
25
+ - README.rdoc
26
+ - Rakefile
27
+ - lib/argument_sensei
28
+ - lib/argument_sensei/commands.rb
29
+ - lib/argument_sensei/version.rb
30
+ - lib/argument_sensei.rb
31
+ - test/test_helper.rb
32
+ - test/unit
33
+ - test/unit/argument_sensei_test.rb
34
+ has_rdoc: true
35
+ homepage: http://yoidore.org/argument_sensei
36
+ post_install_message:
37
+ rdoc_options:
38
+ - --main
39
+ - README.rdoc
40
+ require_paths:
41
+ - lib
42
+ required_ruby_version: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: "0"
47
+ version:
48
+ required_rubygems_version: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: "0"
53
+ version:
54
+ requirements: []
55
+
56
+ rubyforge_project:
57
+ rubygems_version: 1.2.0
58
+ signing_key:
59
+ specification_version: 2
60
+ summary: ArgumentSensei is yet another command-line argument parser with a minimal set of features.
61
+ test_files: []
62
+