tap 0.18.0 → 0.19.0

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