tap 0.18.0 → 0.19.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,268 @@
1
+ require 'shellwords'
2
+
3
+ module Tap
4
+
5
+ # A parser for workflows defined on the command line.
6
+ class Parser
7
+ class << self
8
+ def parse(argv=ARGV)
9
+ parse!(argv.dup)
10
+ end
11
+
12
+ def parse!(argv=ARGV)
13
+ argv = Shellwords.shellwords(argv) if argv.kind_of?(String)
14
+ sig, obj = argv.shift, nil
15
+
16
+ if sig =~ OBJECT
17
+ obj, sig = $1, $2
18
+ end
19
+
20
+ [obj, sig, argv]
21
+ end
22
+ end
23
+
24
+ # The escape begin argument
25
+ ESCAPE_BEGIN = "-."
26
+
27
+ # The escape end argument
28
+ ESCAPE_END = ".-"
29
+
30
+ # The parser end flag
31
+ END_FLAG = "---"
32
+
33
+ # Matches any breaking arg. Examples:
34
+ #
35
+ # --
36
+ # --:
37
+ # --[1,2][3]
38
+ # --@
39
+ # --/var
40
+ # --.
41
+ #
42
+ # After the match:
43
+ #
44
+ # $1:: The string after the break, or nil
45
+ # (ex: '--' => nil, '--:' => ':', '--[1,2][3,4]' => '[1,2][3,4]')
46
+ #
47
+ BREAK = /\A--(?:\z|([\:\[\/\.@].*?)\z)/
48
+
49
+ # The node modifier.
50
+ NODE_BREAK = nil
51
+
52
+ # The join modifier.
53
+ JOIN_BREAK = "."
54
+
55
+ # Matches a sequence break. After the match:
56
+ #
57
+ # $1:: The modifier string, or nil
58
+ # (ex: ':' => nil, ':i' => 'i')
59
+ #
60
+ SEQUENCE = /\A:(.+)?\z/
61
+
62
+ # Matches a generic join break. After the match:
63
+ #
64
+ # $1:: The inputs string.
65
+ # (ex: '[1,2,3][4,5,6]' => '1,2,3')
66
+ # $2:: The outputs string.
67
+ # (ex: '[1,2,3][4,5,6]' => '4,5,6')
68
+ # $3:: The modifier string, or nil
69
+ # (ex: '[][]is' => 'is')
70
+ #
71
+ JOIN = /\A\[(.*?)\]\[(.*?)\](.+)?\z/
72
+
73
+ # Matches a join modifier. After the match:
74
+ #
75
+ # $1:: The modifier flag string.
76
+ # (ex: 'is.sync' => 'is')
77
+ # $2:: The class string.
78
+ # (ex: 'is.sync' => 'sync')
79
+ #
80
+ JOIN_MODIFIER = /\A([A-z]*)(?:\.(.*))?\z/
81
+
82
+ # Matches an enque modifier. After the match:
83
+ #
84
+ # $1:: The modifier string, or nil
85
+ # (ex: '@var' => 'var')
86
+ #
87
+ ENQUE = /\A@(.+)?\z/
88
+
89
+ # Matches a signal break. After the match:
90
+ #
91
+ # $1:: The object string, or nil
92
+ # (ex: 'obj/sig' => 'obj')
93
+ # $2:: The signal string
94
+ # (ex: 'obj/sig' => 'sig')
95
+ #
96
+ SIGNAL = /\A\/(?:(.*)\/)?(.*)\z/
97
+
98
+ # Splits a signal into an object string and a signal string. If OBJECT
99
+ # doesn't match, then the string can be considered a signal, and the
100
+ # object is nil. After a match:
101
+ #
102
+ # $1:: The object string
103
+ # (ex: 'obj/sig' => 'obj')
104
+ # $2:: The signal string
105
+ # (ex: 'obj/sig' => 'sig')
106
+ #
107
+ OBJECT = /\A(.*)\/(.*)\z/
108
+
109
+ attr_reader :specs
110
+
111
+ def initialize(specs=[])
112
+ @specs = specs
113
+ end
114
+
115
+ def parse(argv)
116
+ argv = argv.dup unless argv.kind_of?(String)
117
+ parse!(argv)
118
+ end
119
+
120
+ # Same as parse, but removes parsed args from argv.
121
+ def parse!(argv)
122
+ argv = Shellwords.shellwords(argv) if argv.kind_of?(String)
123
+ return argv if argv.empty?
124
+
125
+ unless argv[0] =~ BREAK
126
+ argv.unshift("--")
127
+ end
128
+
129
+ @current_type = nil
130
+ @current_index = -1
131
+ @current = nil
132
+ escape = false
133
+
134
+ while !argv.empty?
135
+ arg = argv.shift
136
+
137
+ # if escaping, add escaped arguments
138
+ # until an escape-end argument
139
+ if escape
140
+ if arg == ESCAPE_END
141
+ escape = false
142
+ else
143
+ current << arg
144
+ end
145
+ next
146
+ end
147
+
148
+ # handle breaks and parser flags
149
+ case arg
150
+ when BREAK
151
+ begin
152
+ @current_type = nil
153
+ @current_index += 1
154
+ @current = parse_break($1)
155
+ rescue
156
+ raise "invalid break: #{arg} (#{$!.message})"
157
+ end
158
+ next
159
+
160
+ when ESCAPE_BEGIN
161
+ escape = true
162
+ next
163
+
164
+ when END_FLAG
165
+ break
166
+
167
+ end if arg[0] == ?-
168
+
169
+ # add all remaining args to the current argv
170
+ current << arg
171
+ end
172
+
173
+ @current_type = nil
174
+ @current_index = nil
175
+ @current = nil
176
+
177
+ argv
178
+ end
179
+
180
+ private
181
+
182
+ def spec(*argv) # :nodoc:
183
+ specs << argv
184
+ argv
185
+ end
186
+
187
+ # returns the current argv or a new spec argv for the current type/index
188
+ def current # :nodoc:
189
+ @current ||= spec(@current_type, nil, 'set', @current_index.to_s)
190
+ end
191
+
192
+ # determines the type of break and modifies self appropriately
193
+ def parse_break(one) # :nodoc:
194
+ case one
195
+ when NODE_BREAK
196
+ set_type(:node)
197
+ when JOIN_BREAK
198
+ set_type(:join)
199
+ when SEQUENCE
200
+ parse_sequence($1)
201
+ when JOIN
202
+ parse_join($1, $2, $3)
203
+ when ENQUE
204
+ parse_enque($1)
205
+ when SIGNAL
206
+ parse_signal($1, $2)
207
+ else
208
+ raise "invalid modifier"
209
+ end
210
+ end
211
+
212
+ # sets the type of the next spec
213
+ def set_type(type) # :nodoc:
214
+ @current_type = type
215
+ nil
216
+ end
217
+
218
+ # parses the match of a SEQUENCE regexp
219
+ def parse_sequence(one) # :nodoc:
220
+ unless @current_index > 0
221
+ raise "no prior entry"
222
+ end
223
+
224
+ @current_type = :node
225
+ @current = nil
226
+ argv = current
227
+ parse_join_spec(one, "#{@current_index - 1}", @current_index.to_s)
228
+ argv
229
+ end
230
+
231
+ # parses the match of a JOIN regexp
232
+ def parse_join(one, two, three) # :nodoc:
233
+ parse_join_spec(three, one, two)
234
+ end
235
+
236
+ # parses a join modifier string into an argv.
237
+ def parse_join_spec(modifier, inputs, outputs) # :nodoc:
238
+ argv = [:join, nil, 'set', nil]
239
+
240
+ case
241
+ when modifier.nil?
242
+ argv << 'tap:join'
243
+ argv << inputs
244
+ argv << outputs
245
+ when modifier =~ JOIN_MODIFIER
246
+ argv << ($2 || 'join')
247
+ argv << inputs
248
+ argv << outputs
249
+ $1.split("").each {|char| argv << "-#{char}"}
250
+ else
251
+ raise "invalid join modifier"
252
+ end
253
+
254
+ specs << argv
255
+ argv
256
+ end
257
+
258
+ # parses the match of an ENQUE regexp
259
+ def parse_enque(one) # :nodoc:
260
+ spec(:signal, nil, 'enque', one)
261
+ end
262
+
263
+ # parses the match of a SIGNAL regexp
264
+ def parse_signal(one, two) # :nodoc:
265
+ spec(:signal, one, two)
266
+ end
267
+ end
268
+ end
@@ -0,0 +1,36 @@
1
+ require 'tap/app/api'
2
+ require 'readline'
3
+
4
+ module Tap
5
+
6
+ # :startdoc::prompt
7
+ #
8
+ # A prompt to signal a running app. Any signals that return app (ie /run
9
+ # /stop /terminate) will exit the prompt.
10
+ class Prompt < App::Api
11
+
12
+ def call
13
+ puts "starting prompt (help for help):"
14
+ loop do
15
+ begin
16
+ line = Readline.readline('--/', true).strip
17
+ next if line.empty?
18
+
19
+ args = Shellwords.shellwords(line)
20
+ "/#{args.shift}" =~ Tap::Parser::SIGNAL
21
+
22
+ result = app.call('obj' => $1, 'sig' => $2, 'args' => args)
23
+ if result == app
24
+ break
25
+ else
26
+ puts "=> #{result}"
27
+ end
28
+ rescue
29
+ puts $!.message
30
+ puts $!.backtrace if app.debug?
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+
@@ -54,14 +54,14 @@ module Tap
54
54
  include Utils
55
55
 
56
56
  # The root directory.
57
- config_attr(:root, '.', :writer => false, :set_default => false)
57
+ config_attr(:root, '.', :writer => false, :init => false)
58
58
 
59
59
  # A hash of (alias, relative path) pairs for aliased paths relative
60
60
  # to root.
61
- config_attr(:relative_paths, {}, :writer => false, :set_default => false, :type => :hash)
61
+ config_attr(:relative_paths, {}, :writer => false, :init => false, :type => :hash)
62
62
 
63
63
  # A hash of (alias, relative path) pairs for aliased absolute paths.
64
- config_attr(:absolute_paths, {}, :reader => false, :writer => false, :set_default => false, :type => :hash)
64
+ config_attr(:absolute_paths, {}, :reader => false, :writer => false, :init => false, :type => :hash)
65
65
 
66
66
  # A hash of (alias, expanded path) pairs for expanded relative and
67
67
  # absolute paths.
@@ -0,0 +1,26 @@
1
+ require 'tap/signals/module_methods'
2
+
3
+ module Tap
4
+
5
+ # Signals is a module providing signaling capbilities for objects. Signals
6
+ # are effectively bound to methods with pre-processing that allows inputs
7
+ # from the command line (ie an ARGV) or from interfaces like HTTP that
8
+ # commonly produce a parameters hash.
9
+ #
10
+ module Signals
11
+ def signal(sig, &block)
12
+ sig = sig.to_s
13
+ unless signal = self.class.signals[sig]
14
+ raise "unknown signal: #{sig} (#{self.class})"
15
+ end
16
+
17
+ signal.new(self, &block)
18
+ end
19
+
20
+ def signal?(sig)
21
+ sig = sig.to_s
22
+ self.class.signals.has_key?(sig.to_s)
23
+ end
24
+ end
25
+ end
26
+
@@ -0,0 +1,222 @@
1
+ require 'tap/signals/signal'
2
+
3
+ module Tap
4
+ module Signals
5
+ module ClassMethods
6
+ SIGNALS_CLASS = Configurable::ClassMethods::CONFIGURATIONS_CLASS
7
+
8
+ # A hash of (key, Signal) pairs defining signals available to the class.
9
+ attr_reader :signal_registry
10
+
11
+ def self.initialize(base)
12
+ unless base.instance_variable_defined?(:@signal_registry)
13
+ base.instance_variable_set(:@signal_registry, SIGNALS_CLASS.new)
14
+ end
15
+
16
+ unless base.instance_variable_defined?(:@signals)
17
+ base.instance_variable_set(:@signals, nil)
18
+ end
19
+
20
+ unless base.instance_variable_defined?(:@use_signal_constants)
21
+ base.instance_variable_set(:@use_signal_constants, true)
22
+ end
23
+ end
24
+
25
+ # A hash of (key, Signal) pairs representing all signals defined on this
26
+ # class or inherited from ancestors. The signals hash is generated on
27
+ # each call to ensure it accurately reflects any signals added on
28
+ # ancestors. This slows down signal calls through instance.signal.
29
+ #
30
+ # Call cache_signals after all signals have been declared in order
31
+ # to prevent regeneration of signals and to significantly improve
32
+ # performance.
33
+ def signals
34
+ return @signals if @signals
35
+
36
+ signals = SIGNALS_CLASS.new
37
+ ancestors.reverse.each do |ancestor|
38
+ next unless ancestor.kind_of?(ClassMethods)
39
+ ancestor.signal_registry.each_pair do |key, value|
40
+ if value.nil?
41
+ signals.delete(key)
42
+ else
43
+ signals[key] = value
44
+ end
45
+ end
46
+ end
47
+
48
+ signals
49
+ end
50
+
51
+ # Caches the signals hash so as to improve peformance. Call with on set to
52
+ # false to turn off caching.
53
+ def cache_signals(on=true)
54
+ @signals = nil
55
+ @signals = self.signals if on
56
+ end
57
+
58
+ protected
59
+
60
+ def use_signal_constants(input=true)
61
+ @use_signal_constants = input
62
+ end
63
+
64
+ # Defines a signal to call a method using an argument vector. The argv
65
+ # is sent to the method using a splat, so any method may be signaled.
66
+ # A signature of keys may be specified to automatically generate an argv
67
+ # from a hash; values for the keys are collected in order.
68
+ #
69
+ # A block may also be provided to pre-process the argv before it is sent
70
+ # to the method; the block return is sent to the method (and so should
71
+ # be an argv).
72
+ def signal(sig, opts={}) # :yields: sig, argv
73
+ signature = opts[:signature] || []
74
+ remainder = opts[:remainder] || false
75
+
76
+ signal = define_signal(sig, opts) do |args|
77
+ argv = convert_to_array(args, signature, remainder)
78
+ block_given? ? yield(self, argv) : argv
79
+ end
80
+
81
+ register_signal(sig, signal, opts)
82
+ end
83
+
84
+ # Defines a signal to call a method that receives a single hash as an
85
+ # input. A signature may be specified to automatically generate a
86
+ # hash from an array input.
87
+ #
88
+ # A block may also be provided to pre-process the hash before it is sent
89
+ # to the method; the block return is sent to the method (and so should
90
+ # be a hash).
91
+ def signal_hash(sig, opts={}) # :yields: sig, argh
92
+ signature = opts[:signature] || []
93
+ remainder = opts[:remainder]
94
+
95
+ signal = define_signal(sig, opts) do |args|
96
+ argh = convert_to_hash(args, signature, remainder)
97
+ [block_given? ? yield(self, argh) : argh]
98
+ end
99
+
100
+ register_signal(sig, signal, opts)
101
+ end
102
+
103
+ def signal_class(sig, signal_class=Signal, opts={}, &block) # :yields: sig, argv
104
+ if block_given?
105
+ signal = Class.new(signal_class)
106
+ signal.class_eval(&block)
107
+ else
108
+ signal = signal_class
109
+ end
110
+
111
+ register_signal(sig, signal, opts)
112
+ end
113
+
114
+ # Removes a signal much like remove_method removes a method. The signal
115
+ # constant is likewise removed unless the :remove_const option is set to
116
+ # to true.
117
+ def remove_signal(sig, opts={})
118
+ sig = sig.to_s
119
+ unless signal_registry.has_key?(sig)
120
+ raise NameError.new("#{sig} is not a signal for #{self}")
121
+ end
122
+
123
+ unregister_signal(sig, opts)
124
+ end
125
+
126
+ # Undefines a signal much like undef_method undefines a method. The signal
127
+ # constant is likewise removed unless the :remove_const option is set to
128
+ # to true.
129
+ #
130
+ # ==== Implementation Note
131
+ #
132
+ # Signals are undefined by setting the key to nil in the registry. Deleting
133
+ # the signal is not sufficient because the registry needs to convey to self
134
+ # and subclasses to not inherit the signal from ancestors.
135
+ #
136
+ # This is unlike remove_signal where the signal is simply deleted from
137
+ # the signal_registry.
138
+ #
139
+ def undef_signal(sig, opts={})
140
+ # temporarily cache as an optimization
141
+ signals_cache = signals
142
+ sig = sig.to_s
143
+ unless signals_cache.has_key?(sig)
144
+ raise NameError.new("#{sig} is not a signal for #{self}")
145
+ end
146
+
147
+ unregister_signal(sig, opts)
148
+ signal_registry[sig] = nil
149
+ signals_cache[sig]
150
+ end
151
+
152
+ private
153
+
154
+ def inherited(base) # :nodoc:
155
+ ClassMethods.initialize(base)
156
+
157
+ unless base.instance_variable_defined?(:@use_signal_constants)
158
+ base.instance_variable_set(:@use_signal_constants, true)
159
+ end
160
+
161
+ super
162
+ end
163
+
164
+ def define_signal(sig, opts={}, &block) # :nodoc:
165
+ # generate a subclass of signal
166
+ klass = opts[:class] || Signal
167
+ signal = Class.new(klass)
168
+
169
+ # bind the new signal
170
+ method_name = opts.has_key?(:bind) ? opts[:bind] : sig
171
+ if method_name
172
+ signal.send(:define_method, :call) do |args|
173
+ args = process(args)
174
+ obj.send(method_name, *args, &self.block)
175
+ end
176
+ end
177
+
178
+ if block_given?
179
+ signal.send(:define_method, :process, &block)
180
+ end
181
+
182
+ signal
183
+ end
184
+
185
+ def register_signal(sig, signal, opts={}) # :nodoc:
186
+ if signal.respond_to?(:desc=)
187
+ signal.desc ||= Lazydoc.register_caller(Lazydoc::Trailer, 2)
188
+ end
189
+
190
+ signal_registry[sig.to_s] = signal
191
+ cache_signals(@signals != nil)
192
+
193
+ # set the new constant, if specified
194
+ if @use_signal_constants
195
+ const_name = opts.has_key?(:const_name) ? opts[:const_name] : sig.to_s.capitalize
196
+ const_name = const_name.to_s
197
+
198
+ if const_name =~ /\A[A-Z]\w*\z/ && !const_defined?(const_name)
199
+ const_set(const_name, signal)
200
+ end
201
+ end
202
+
203
+ signal
204
+ end
205
+
206
+ def unregister_signal(sig, opts={}) # :nodoc:
207
+ signal = signal_registry.delete(sig.to_s)
208
+
209
+ remove_const = opts.has_key?(:remove_const) ? opts[:remove_const] : true
210
+ if @use_signal_constants && remove_const
211
+ const_name = signal.to_s.split("::").pop.to_s
212
+ if const_name =~ /\A[A-Z]\w*\z/ && const_defined?(const_name)
213
+ remove_const(const_name)
214
+ end
215
+ end
216
+
217
+ cache_signals(@signals != nil)
218
+ signal
219
+ end
220
+ end
221
+ end
222
+ end