optix 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,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