optix 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,3 @@
1
+ module Optix
2
+ VERSION = "1.0.1"
3
+ end
data/lib/optix.rb ADDED
@@ -0,0 +1,276 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'chronic'
4
+ require 'optix/trollop'
5
+
6
+ class Optix
7
+ class HelpNeeded < StandardError; end
8
+
9
+ @@tree = {}
10
+ attr_reader :parser, :node, :filters, :triggers, :command, :subcommands
11
+
12
+ def self.command cmd=nil, scope=:default, &b
13
+ @@tree[scope] ||= {}
14
+ Command.new(@@tree[scope], @@config, cmd, &b)
15
+ end
16
+
17
+ def self.reset!
18
+ @@config = {
19
+ :text_help => 'Show this message', # set to nil to suppress help option
20
+ :text_required => ' (required)',
21
+ :text_header_usage => 'Usage: %0 %command %params',
22
+ :text_header_subcommands => 'Subcommands:',
23
+ :text_header_topcommands => 'Commands:',
24
+ :text_header_options => 'Options:',
25
+ :text_param_subcommand => '<subcommand>'
26
+ }
27
+ @@tree = {}
28
+ end
29
+ reset!
30
+
31
+ def self.configure &b
32
+ Configurator.new(@@config, &b)
33
+ end
34
+
35
+ def initialize(argv=ARGV, scope=:default)
36
+ unless @@tree.include? scope
37
+ raise RuntimeError, "Scope '#{scope}' is not defined"
38
+ end
39
+ o = @@tree[scope]
40
+
41
+ parent_calls = o[:calls] || []
42
+ filters = o[:filters] || []
43
+ triggers = o[:triggers] || {}
44
+ cmdpath = []
45
+ while o.include? argv[0]
46
+ cmdpath << cmd = argv.shift
47
+ o = o[cmd]
48
+ parent_calls += o[:calls] if o.include? :calls
49
+ filters += o[:filters] if o.include? :filters
50
+ triggers.merge! o[:triggers] if o.include? :triggers
51
+ end
52
+
53
+ o[:header] ||= "\n#{@@config[:text_header_usage]}\n"
54
+ o[:params] ||= ''
55
+
56
+ subcmds = o.keys.reject{|x| x.is_a? Symbol}
57
+
58
+ if 0 < subcmds.length
59
+ o[:params] = @@config[:text_param_subcommand]
60
+ end
61
+
62
+ text = o[:header].gsub('%0', $0)
63
+ .gsub('%command', cmdpath.join(' '))
64
+ .gsub('%params', o[:params])
65
+ .gsub(/ +/, ' ')
66
+
67
+ calls = []
68
+ calls << [:banner, [text], nil]
69
+
70
+ calls << [:banner, [' '], nil]
71
+ unless o[:text].nil?
72
+ calls << [:banner, o[:text], nil]
73
+ calls << [:banner, [' '], nil]
74
+ end
75
+
76
+ # sort opts and move non-opt calls to the end
77
+ non_opt = parent_calls.select {|x| x[0] != :opt }
78
+ parent_calls.select! {|x| x[0] == :opt }
79
+ parent_calls.sort! {|a,b| ; a[1][0] <=> b[1][0] }
80
+ parent_calls += non_opt
81
+ parent_calls.unshift([:banner, [@@config[:text_header_options]], nil])
82
+ calls += parent_calls
83
+
84
+ unless @@config[:text_help].nil?
85
+ calls << [:opt, [:help, @@config[:text_help]], nil]
86
+ end
87
+
88
+ if 0 < subcmds.length
89
+ prefix = cmdpath.join(' ')
90
+
91
+ text = ""
92
+ wid = 0
93
+ subcmds.each do |k|
94
+ len = k.length + prefix.length + 1
95
+ wid = len if wid < len
96
+ end
97
+
98
+ #calls << [:no_help_help, [], nil]
99
+
100
+ subcmds.each do |k|
101
+ cmd = "#{prefix} #{k}"
102
+ text += " #{cmd.ljust(wid)}"
103
+ unless o[k][:description].nil?
104
+ text += " #{o[k][:description]}"
105
+ end
106
+ text += "\n"
107
+ end
108
+
109
+ if 0 < cmdpath.length
110
+ calls << [:banner, ["\n#{@@config[:text_header_subcommands]}\n#{text}"], nil]
111
+ else
112
+ calls << [:banner, ["\n#{@@config[:text_header_topcommands]}\n#{text}"], nil]
113
+ end
114
+ end
115
+
116
+ calls << [:banner, [" \n"], nil]
117
+
118
+ parser = Trollop::Parser.new
119
+
120
+ lastmeth = nil
121
+ begin
122
+ calls.each do |e|
123
+ lastmeth = e[0]
124
+ parser.send(e[0], *e[1])
125
+ end
126
+ rescue NoMethodError => e
127
+ raise RuntimeError, "Unknown Optix command: '#{lastmeth}'"
128
+ end
129
+
130
+ # expose our goodies
131
+ @parser = parser
132
+ @node = o
133
+ @filters = filters
134
+ @triggers = triggers
135
+ @command = cmdpath
136
+ @subcommands = subcmds
137
+ end
138
+
139
+ def self.invoke!(argv=ARGV, scope=:default)
140
+ optix = Optix.new(argv, scope)
141
+
142
+ # If you need more flexibility than this block provides
143
+ # then you may want to create your own Optix instance
144
+ # and perform the parsing manually.
145
+ opts = Trollop::with_standard_exception_handling optix.parser do
146
+
147
+ # Process triggers first
148
+ triggers = optix.triggers
149
+ opts = optix.parser.parse argv, triggers
150
+ return if opts.nil?
151
+
152
+ # Always show help-screen if the invoked command has subcommands.
153
+ if 0 < optix.subcommands.length
154
+ raise Trollop::HelpNeeded # show help screen
155
+ end
156
+
157
+ begin
158
+ # Run filter-chain
159
+ optix.filters.each do |filter|
160
+ filter.call(optix.command, opts, argv)
161
+ end
162
+
163
+ # Run exec-block
164
+ if optix.node[:exec].nil?
165
+ raise RuntimeError, "Command '#{optix.command.join(' ')}' has no exec{}-block!"
166
+ end
167
+ optix.node[:exec].call(optix.command, opts, argv)
168
+ rescue HelpNeeded
169
+ raise Trollop::HelpNeeded
170
+ end
171
+ end
172
+ end
173
+
174
+ class Configurator
175
+ def initialize(config, &b)
176
+ @config = config
177
+ cloak(&b).bind(self).call
178
+ end
179
+
180
+ def method_missing(meth, *args, &block)
181
+ unless @config.include? meth
182
+ raise ArgumentError, "Unknown configuration key '#{meth}'"
183
+ end
184
+ @config[meth] = args[0]
185
+ end
186
+
187
+ def cloak &b
188
+ (class << self; self; end).class_eval do
189
+ define_method :cloaker_, &b
190
+ meth = instance_method :cloaker_
191
+ remove_method :cloaker_
192
+ meth
193
+ end
194
+ end
195
+ end
196
+
197
+ class Command
198
+ attr_reader :tree
199
+ def initialize(tree, config, cmd, &b)
200
+ @tree = tree
201
+ @config = config
202
+ @cmd = cmd || ''
203
+ node
204
+ cloak(&b).bind(self).call
205
+ end
206
+
207
+ def node
208
+ path = @cmd.split
209
+ o = @tree
210
+ path.each do |e|
211
+ o = o[e] ||= {}
212
+ end
213
+ o
214
+ end
215
+
216
+ def push_call(meth, args, block)
217
+ node[:calls] ||= []
218
+ node[:calls] << [meth, args, block]
219
+ end
220
+
221
+ def method_missing(meth, *args, &block)
222
+ push_call(meth, args, block)
223
+ end
224
+
225
+ def opt(cmd, desc='', args={})
226
+ if args.fetch(:default, false)
227
+ args.delete :required
228
+ end
229
+ if args.fetch(:required, false)
230
+ desc += @config[:text_required]
231
+ end
232
+ push_call(:opt, [cmd, desc, args], nil)
233
+ end
234
+
235
+ def params(text)
236
+ node[:params] = text
237
+ end
238
+
239
+ def exec(&block)
240
+ node[:exec] = block
241
+ end
242
+
243
+ def filter(&block)
244
+ node[:filters] ||= []
245
+ node[:filters] << block
246
+ end
247
+
248
+ def trigger(opts, &block)
249
+ node[:triggers] ||= {}
250
+ node[:triggers][opts] = block
251
+ end
252
+
253
+
254
+ def desc(text)
255
+ node[:description] = text
256
+ end
257
+
258
+ def text(text)
259
+ node[:text] ||= ''
260
+ if 0 < node[:text].length
261
+ node[:text] += "\n"
262
+ end
263
+ node[:text] += text
264
+ end
265
+
266
+ def cloak &b
267
+ (class << self; self; end).class_eval do
268
+ define_method :cloaker_, &b
269
+ meth = instance_method :cloaker_
270
+ remove_method :cloaker_
271
+ meth
272
+ end
273
+ end
274
+ end
275
+ end
276
+
data/optix.gemspec ADDED
@@ -0,0 +1,20 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/optix/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Moe"]
6
+ gem.email = ["moe@busyloop.net"]
7
+ gem.description = %q{Optix is an unobtrusive, composable command line parser.}
8
+ gem.summary = %q{Optix is an unobtrusive, composable command line parser.}
9
+ gem.homepage = ""
10
+
11
+ gem.files = `git ls-files`.split($\)
12
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
13
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
14
+ gem.name = "optix"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = Optix::VERSION
17
+
18
+ gem.add_dependency "chronic"
19
+ gem.add_development_dependency "simplecov"
20
+ end