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.
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE +25 -0
- data/README.md +431 -0
- data/Rakefile +30 -0
- data/examples/filetool.rb +188 -0
- data/examples/printer.rb +43 -0
- data/lib/optix/trollop.rb +814 -0
- data/lib/optix/version.rb +3 -0
- data/lib/optix.rb +276 -0
- data/optix.gemspec +20 -0
- data/spec/optix_spec.rb +988 -0
- data/spec/spec_helper.rb +30 -0
- metadata +83 -0
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
|