yap-shell 0.1.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -0
  3. data/Gemfile +5 -0
  4. data/WISHLIST.md +14 -0
  5. data/addons/history/Gemfile +2 -0
  6. data/addons/history/history.rb +101 -0
  7. data/addons/history/lib/history/buffer.rb +204 -0
  8. data/addons/history/lib/history/events.rb +13 -0
  9. data/addons/keyboard_macros/keyboard_macros.rb +295 -0
  10. data/addons/prompt/Gemfile +1 -0
  11. data/addons/prompt/right_prompt.rb +17 -0
  12. data/addons/prompt_updates/prompt_updates.rb +28 -0
  13. data/addons/tab_completion/Gemfile +0 -0
  14. data/addons/tab_completion/lib/tab_completion/completer.rb +62 -0
  15. data/addons/tab_completion/lib/tab_completion/custom_completion.rb +33 -0
  16. data/addons/tab_completion/lib/tab_completion/dsl_methods.rb +7 -0
  17. data/addons/tab_completion/lib/tab_completion/file_completion.rb +75 -0
  18. data/addons/tab_completion/tab_completion.rb +157 -0
  19. data/bin/yap +13 -4
  20. data/lib/tasks/addons.rake +51 -0
  21. data/lib/yap.rb +4 -55
  22. data/lib/yap/shell.rb +51 -10
  23. data/lib/yap/shell/builtins.rb +2 -2
  24. data/lib/yap/shell/builtins/alias.rb +2 -2
  25. data/lib/yap/shell/builtins/cd.rb +9 -11
  26. data/lib/yap/shell/builtins/env.rb +11 -0
  27. data/lib/yap/shell/commands.rb +29 -18
  28. data/lib/yap/shell/evaluation.rb +185 -68
  29. data/lib/yap/shell/evaluation/shell_expansions.rb +85 -0
  30. data/lib/yap/shell/event_emitter.rb +18 -0
  31. data/lib/yap/shell/execution/builtin_command_execution.rb +1 -1
  32. data/lib/yap/shell/execution/command_execution.rb +3 -3
  33. data/lib/yap/shell/execution/context.rb +32 -9
  34. data/lib/yap/shell/execution/file_system_command_execution.rb +12 -7
  35. data/lib/yap/shell/execution/ruby_command_execution.rb +6 -6
  36. data/lib/yap/shell/execution/shell_command_execution.rb +17 -2
  37. data/lib/yap/shell/prompt.rb +21 -0
  38. data/lib/yap/shell/repl.rb +179 -18
  39. data/lib/yap/shell/version.rb +1 -1
  40. data/lib/yap/world.rb +149 -15
  41. data/lib/yap/world/addons.rb +135 -0
  42. data/rcfiles/.yaprc +240 -10
  43. data/test.rb +206 -0
  44. data/update-rawline.sh +6 -0
  45. data/yap-shell.gemspec +11 -3
  46. metadata +101 -10
  47. data/addons/history.rb +0 -171
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: c4e3c7118e864187750ddf674697554b760e3853
4
- data.tar.gz: 8dc3b7e693b4cd35bd48935135cb9b8c37b13bd8
3
+ metadata.gz: 246c3bc588c78cfde392f379b5ca086e6aac79e0
4
+ data.tar.gz: f1c54403abc8e95a96f255582b6b3af4fcb511ba
5
5
  SHA512:
6
- metadata.gz: cfa9336a5d7756b646fe4de4f75830d9162a729351f2f710d4f18a74bde8c3a5708785becb519e32a9513724ebdf5591ffa4f65872dee3a52c02426fd685487d
7
- data.tar.gz: dce637e76f850540d3aaad4352bad5254abe9d8a7c4ec38b4ef819bb1babd39f9304b3d54e06999717c85b33e94cfd4a79e99eeb8932a8508efad0d0ddc4e0ce
6
+ metadata.gz: 61414d9cc1b15a58661bca3aac9911798fe6b8aabc99e4d03732109c29d4c2fa2d9928f2c43d0cadfb736a45f4ca056c41657226d654c3b04d2dc9ce14f1ae35
7
+ data.tar.gz: a3999e8f5fcbcaab03450309ecfc1a7942d3bf2f072a75862e96f203a5b9c3b89ee31ffde11ba3eedd59780a1f75a519107f04b9aa816b48a16cceef6b510eaa
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 2.2.3
data/Gemfile CHANGED
@@ -2,3 +2,8 @@ source 'https://rubygems.org'
2
2
 
3
3
  # Specify your gem's dependencies in yap.gemspec
4
4
  gemspec
5
+ # gem 'yap-shell-parser', path: "../yap-shell-parser"
6
+ # gem 'yap-shell-parser', git: 'git@github.com:zdennis/yap-shell-parser.git'
7
+ # gem "rawline", git: 'git@github.com:zdennis/rawline.git', branch: 'issues/input-issues'
8
+ # gem "rawline", path: '/Users/zdennis/source/opensource_projects/rawline'
9
+ # gem "pry-byebug"
data/WISHLIST.md CHANGED
@@ -1,3 +1,7 @@
1
+ CLI
2
+
3
+ * yap install right-prompt
4
+
1
5
 
2
6
  * need to add custom tab completion for aliases, builtins, shell commands, and file system commands
3
7
  * support user-specified gems to install (so they're available for rc files)
@@ -38,3 +42,13 @@ Others requests.
38
42
  * @dylanized: themeable
39
43
  * @dylanized: browser-based
40
44
  * @dylanized: bookmarks
45
+
46
+
47
+ ### Tab completion
48
+ * sort completions for printing
49
+ * print completion in groups?
50
+ * match on command, command with arguments, arguments, env var args
51
+ * supply the completion text
52
+ * supply the descriptive text
53
+ * alias one completion for another
54
+ * cache completions? invalidate cached completions
@@ -0,0 +1,2 @@
1
+ gem "chronic", "~> 0.10.2"
2
+ gem "term-ansicolor", "~> 1.3.2"
@@ -0,0 +1,101 @@
1
+ require 'forwardable'
2
+ require 'term/ansicolor'
3
+ require 'ostruct'
4
+
5
+ class History < Addon
6
+ require 'history/buffer'
7
+ require 'history/events'
8
+
9
+ Color = Object.extend Term::ANSIColor
10
+
11
+ class << self
12
+ attr_accessor :history_item_formatter, :ignore_history_item
13
+ end
14
+
15
+ self.ignore_history_item = ->(item:) do
16
+ item.command == "exit"
17
+ end
18
+
19
+ self.history_item_formatter = ->(item:, options:{}) do
20
+ if item.duration
21
+ sprintf(
22
+ "%#{options[:max_position_width]}d %-s %s",
23
+ item.position,
24
+ item.command,
25
+ Color.negative(Color.intense_black(item.duration))
26
+ )
27
+ else
28
+ sprintf("%#{options[:max_position_width]}d %-s", item.position, item.command)
29
+ end
30
+ end
31
+
32
+ def initialize_world(world)
33
+ @world = world
34
+ load_history
35
+
36
+ world.editor.bind(:ctrl_h) { show_history(@world.editor) }
37
+
38
+ world.func(:history) do |args:, stdin:, stdout:, stderr:|
39
+ first_arg = args.first
40
+ case first_arg
41
+ when String
42
+ if first_arg.start_with?("/")
43
+ regex = first_arg.gsub(/(^\|\/$)/, '')
44
+ ignore_history_item = ->(item:, options:{}) do
45
+ item.command !~ /#{regex}/
46
+ end
47
+ else
48
+ ignore_history_item = ->(item:, options:{}) do
49
+ item.command !~ /#{first_arg}/
50
+ end
51
+ end
52
+ show_history(world.editor, redraw_prompt:false, ignore_history_item:ignore_history_item)
53
+ else
54
+ show_history(world.editor, redraw_prompt:false)
55
+ end
56
+ end
57
+ end
58
+
59
+ def show_history(editor, redraw_prompt:true, ignore_history_item:nil, history_item_formatter:nil)
60
+ editor.puts @history
61
+ end
62
+
63
+ def executing(command:, started_at:)
64
+ # raise "Cannot acknowledge execution beginning of a command when no group has been started!" unless history.last
65
+ end
66
+
67
+ def executed(command:, stopped_at:)
68
+ # raise "Cannot complete execution of a command when no command has been started!" unless history.last
69
+ end
70
+
71
+ def save
72
+ File.open(history_file, "a") do |file|
73
+ # Don't write the YAML header because we're going to append to the
74
+ # history file, not overwrite. YAML works fine without it.
75
+ file.write @world.editor.history.to_yaml(@history_start_position..-1).gsub(/^---.*/, '')
76
+ end
77
+ end
78
+
79
+ private
80
+
81
+ def history
82
+ @history
83
+ end
84
+
85
+ def history_file
86
+ @history_file ||= File.expand_path('~') + '/.yap-history'
87
+ end
88
+
89
+ def load_history
90
+ @world.editor.history = @history = History::Buffer.new(Float::INFINITY)
91
+ @history_start_position = 0
92
+
93
+ at_exit { save }
94
+
95
+ return unless File.exists?(history_file) && File.readable?(history_file)
96
+
97
+ history_elements = YAML.load_file(history_file) || []
98
+ @history_start_position = history_elements.length
99
+ @world.editor.history.replace(history_elements)
100
+ end
101
+ end
@@ -0,0 +1,204 @@
1
+ class History
2
+ class Buffer < Array
3
+ attr_reader :position, :size
4
+ attr_accessor :duplicates, :exclude, :cycle
5
+
6
+ #
7
+ # Create an instance of History::Buffer.
8
+ # This method takes an optional block used to override the
9
+ # following instance attributes:
10
+ # * <tt>@duplicates</tt> - whether or not duplicate items will be stored in the buffer.
11
+ # * <tt>@exclude</tt> - a Proc object defining exclusion rules to prevent items from being added to the buffer.
12
+ # * <tt>@cycle</tt> - Whether or not the buffer is cyclic.
13
+ #
14
+ def initialize(size)
15
+ @duplicates = true
16
+ @exclude = lambda{|a|}
17
+ @cycle = false
18
+ yield self if block_given?
19
+ @size = size
20
+ @position = nil
21
+ end
22
+
23
+ def supports_partial_text_matching?
24
+ true
25
+ end
26
+
27
+ def supports_matching_text?
28
+ true
29
+ end
30
+
31
+ #
32
+ # Clears the current position on the history object. Useful when deciding
33
+ # to cancel/reset history navigation.
34
+ #
35
+ def clear_position
36
+ @position = nil
37
+ end
38
+
39
+ def searching?
40
+ !!@position
41
+ end
42
+
43
+ #
44
+ # Resize the buffer, resetting <tt>@position</tt> to nil.
45
+ #
46
+ def resize(new_size)
47
+ if new_size < @size
48
+ @size-new_size.times { pop }
49
+ end
50
+ @size = new_size
51
+ @position = nil
52
+ end
53
+
54
+ #
55
+ # Clear the content of the buffer and reset <tt>@position</tt> to nil.
56
+ #
57
+ def empty
58
+ @position = nil
59
+ clear
60
+ end
61
+
62
+ #
63
+ # Retrieve a copy of the element at <tt>@position</tt>.
64
+ #
65
+ def get
66
+ return nil unless length > 0
67
+ return nil unless @position
68
+ at(@position).dup
69
+ end
70
+
71
+ #
72
+ # Return true if <tt>@position</tt> is at the end of the buffer.
73
+ #
74
+ def end?
75
+ @position == length-1
76
+ end
77
+
78
+ #
79
+ # Return true if <tt>@position</tt> is at the start of the buffer.
80
+ #
81
+ def start?
82
+ @position == 0
83
+ end
84
+
85
+ #
86
+ # Decrement <tt>@position</tt>. By default the history will become
87
+ # positioned at the previous item.
88
+ #
89
+ def back(matching_text: nil)
90
+ return nil unless length > 0
91
+ @position = search_back(matching_text: matching_text) || @position
92
+ end
93
+
94
+ #
95
+ # Increment <tt>@position</tt>. By default the history will become
96
+ # positioned at the next item.
97
+ #
98
+ def forward(matching_text: nil)
99
+ return nil unless length > 0
100
+ @position = search_forward(matching_text: matching_text) || @position
101
+ end
102
+
103
+ #
104
+ # Add a new item to the buffer.
105
+ #
106
+ def push(item)
107
+ item = item.without_ansi if item.respond_to?(:without_ansi)
108
+
109
+ if !@duplicates && self[-1] == item
110
+ # skip adding this line
111
+ return
112
+ end
113
+
114
+ unless @exclude.call(item)
115
+ # Remove the oldest element if size is exceeded
116
+ if @size <= length
117
+ reverse!.pop
118
+ reverse!
119
+ end
120
+ # Add the new item and reset the position
121
+ super(item)
122
+ @position = nil
123
+ end
124
+ end
125
+
126
+ alias << push
127
+
128
+ def to_yaml(range=(0..-1))
129
+ self[range].map do |element|
130
+ element.kind_of?(String) ? element : element.to_h
131
+ end.to_yaml
132
+ end
133
+
134
+ private
135
+
136
+ def search_back(matching_text:)
137
+ if !matching_text
138
+ matching_text = ""
139
+ elsif matching_text.respond_to?(:without_ansi)
140
+ matching_text = matching_text.without_ansi
141
+ end
142
+
143
+ command_history = self
144
+ upto_index = (position || length) - 1
145
+ current = get
146
+
147
+ # $z.puts
148
+ # $z.puts <<-EOS.gsub(/^\s*\|/, '')
149
+ # |Search backward:
150
+ # | current:#{current.inspect}
151
+ # | history: #{command_history.inspect}
152
+ # | history position: #{position}
153
+ # | matching_text: #{matching_text.inspect}
154
+ # | upto_index: #{upto_index}
155
+ # | snapshot: #{command_history[0..upto_index].reverse.inspect}
156
+ # EOS
157
+
158
+ return position unless upto_index >= 0
159
+
160
+ snapshot = command_history[0..upto_index].reverse
161
+ no_match = nil
162
+
163
+ position = snapshot.each_with_index.reduce(no_match) do |no_match, (text, i)|
164
+ # $z.print " - matching #{text.inspect} =~ /^#{matching_text.to_s}/ && #{current} != #{text} : "
165
+ if text =~ /^#{Regexp.escape(matching_text)}/ && current != text
166
+ # $z.puts " match #{i}, returning position #{snapshot.length - (i + 1)}"
167
+
168
+ # convert to non-reversed indexing
169
+ position = snapshot.length - (i + 1)
170
+ break position
171
+ else
172
+ # $z.puts " no match."
173
+ no_match
174
+ end
175
+ end
176
+ end
177
+
178
+ def search_forward(matching_text:)
179
+ if !matching_text
180
+ matching_text = ""
181
+ elsif matching_text.respond_to?(:without_ansi)
182
+ matching_text = matching_text.without_ansi
183
+ end
184
+
185
+ command_history = self
186
+ return nil unless position
187
+
188
+ start_index = position + 1
189
+ snapshot = command_history[start_index..-1].dup
190
+ no_match = nil
191
+ current = get
192
+
193
+ position = snapshot.each_with_index.reduce(no_match) do |no_match, (text, i)|
194
+ if text =~ /^#{Regexp.escape(matching_text.to_s)}/ && current != text
195
+ position = start_index + i
196
+ break position
197
+ else
198
+ no_match
199
+ end
200
+ end
201
+ end
202
+ end
203
+
204
+ end
@@ -0,0 +1,13 @@
1
+ Yap::Shell::Execution::Context.on(:before_statements_execute) do |world|
2
+ end
3
+
4
+ Yap::Shell::Execution::Context.on(:after_statements_execute) do |world|
5
+ end
6
+
7
+ Yap::Shell::Execution::Context.on(:before_execute) do |world, command:|
8
+ # world[:history].executing command:command.str, started_at:Time.now
9
+ end
10
+
11
+ Yap::Shell::Execution::Context.on(:after_execute) do |world, command:, result:|
12
+ # world[:history].executed command:command.str, stopped_at:Time.now
13
+ end
@@ -0,0 +1,295 @@
1
+ class KeyboardMacros < Addon
2
+ DEFAULT_TRIGGER_KEY = :ctrl_g
3
+ DEFAULT_CANCEL_KEY = " "
4
+ DEFAULT_TIMEOUT_IN_MS = 500
5
+
6
+ def self.load_addon
7
+ @instance ||= new
8
+ end
9
+
10
+ attr_reader :world
11
+ attr_accessor :timeout_in_ms
12
+ attr_accessor :cancel_key, :trigger_key
13
+ attr_accessor :cancel_on_unknown_sequences
14
+
15
+ def initialize_world(world)
16
+ @world = world
17
+ @configurations = []
18
+ @stack = []
19
+ @timeout_in_ms = DEFAULT_TIMEOUT_IN_MS
20
+ @cancel_key = DEFAULT_CANCEL_KEY
21
+ @trigger_key = DEFAULT_TRIGGER_KEY
22
+ @cancel_on_unknown_sequences = false
23
+ end
24
+
25
+ def configure(cancel_key: nil, trigger_key: nil, &blk)
26
+ cancel_key ||= @cancel_key
27
+ trigger_key ||= @trigger_key
28
+
29
+ cancel_blk = lambda do
30
+ world.editor.event_loop.clear @event_id
31
+ cancel_processing
32
+ nil
33
+ end
34
+
35
+ configuration = Configuration.new(
36
+ keymap: world.editor.terminal.keys,
37
+ trigger_key: trigger_key,
38
+ cancellation: Cancellation.new(cancel_key: cancel_key, &cancel_blk)
39
+ )
40
+
41
+ blk.call configuration if blk
42
+
43
+ world.unbind(trigger_key)
44
+ world.bind(trigger_key) do
45
+ begin
46
+ @stack << configuration
47
+ configuration.start.call if configuration.start
48
+ world.editor.keyboard_input_processors.push(self)
49
+ world.editor.input.wait_timeout_in_seconds = 0.1
50
+ ensure
51
+ queue_up_remove_input_processor(&configuration.stop)
52
+ end
53
+ end
54
+
55
+ @configurations << configuration
56
+ end
57
+
58
+ #
59
+ # InputProcessor Methods
60
+ #
61
+
62
+ def read_bytes(bytes)
63
+ configuration = @stack.last
64
+ bytes.each do |byte|
65
+ definition = configuration[byte]
66
+ if !definition
67
+ cancel_processing if cancel_on_unknown_sequences
68
+ break
69
+ end
70
+ configuration = definition.configuration
71
+ if configuration
72
+ configuration.start.call if configuration.start
73
+ @stack << configuration
74
+ end
75
+
76
+ result = definition.process
77
+
78
+ if result =~ /\n$/
79
+ world.editor.write result.chomp
80
+ world.editor.event_loop.clear @event_id if @event_id
81
+ cancel_processing
82
+ world.editor.newline # add_to_history
83
+ world.editor.process_line
84
+ break
85
+ end
86
+
87
+ @stack.pop if definition.fragment?
88
+
89
+ if @event_id
90
+ world.editor.event_loop.clear @event_id
91
+ @event_id = queue_up_remove_input_processor
92
+ end
93
+ world.editor.write result if result.is_a?(String)
94
+ end
95
+ end
96
+
97
+ private
98
+
99
+ def queue_up_remove_input_processor(&blk)
100
+ return unless @timeout_in_ms
101
+
102
+ event_args = {
103
+ name: 'remove_input_processor',
104
+ source: self,
105
+ interval_in_ms: @timeout_in_ms,
106
+ }
107
+ @event_id = world.editor.event_loop.once(event_args) do
108
+ cancel_processing
109
+ end
110
+ end
111
+
112
+ def cancel_processing
113
+ @event_id = nil
114
+ @stack.reverse.each do |configuration|
115
+ configuration.stop.call if configuration.stop
116
+ end
117
+ @stack.clear
118
+ if world.editor.keyboard_input_processors.last == self
119
+ world.editor.keyboard_input_processors.pop
120
+ world.editor.input.restore_default_timeout
121
+ end
122
+ end
123
+
124
+ class Cancellation
125
+ attr_reader :cancel_key
126
+
127
+ def initialize(cancel_key: , &blk)
128
+ @cancel_key = cancel_key
129
+ @blk = blk
130
+ end
131
+
132
+ def call
133
+ @blk.call
134
+ end
135
+ end
136
+
137
+ class Configuration
138
+ attr_reader :cancellation, :trigger_key, :keymap
139
+
140
+ def initialize(cancellation: nil, keymap: {}, trigger_key: nil)
141
+ @cancellation = cancellation
142
+ @keymap = keymap
143
+ @trigger_key = trigger_key
144
+ @storage = {}
145
+ @on_start_blk = nil
146
+ @on_stop_blk = nil
147
+
148
+ if @cancellation
149
+ define @cancellation.cancel_key, -> { @cancellation.call }
150
+ end
151
+ end
152
+
153
+ def start(&blk)
154
+ @on_start_blk = blk if blk
155
+ @on_start_blk
156
+ end
157
+
158
+ def stop(&blk)
159
+ @on_stop_blk = blk if blk
160
+ @on_stop_blk
161
+ end
162
+
163
+ def fragment(sequence, result)
164
+ define(sequence, result).tap do |definition|
165
+ definition.fragment!
166
+ end
167
+ end
168
+
169
+ def define(sequence, result, &blk)
170
+ unless result.respond_to?(:call)
171
+ string_result = result
172
+ result = -> { string_result }
173
+ end
174
+
175
+ case sequence
176
+ when String
177
+ recursively_define_sequence_for_bytes(
178
+ self,
179
+ sequence.bytes,
180
+ result,
181
+ &blk
182
+ )
183
+ when Symbol
184
+ recursively_define_sequence_for_bytes(
185
+ self,
186
+ @keymap.fetch(sequence){
187
+ fail "Cannot bind unknown sequence #{sequence.inspect}"
188
+ },
189
+ result,
190
+ &blk
191
+ )
192
+ when Regexp
193
+ define_sequence_for_regex(sequence, result, &blk)
194
+ else
195
+ raise NotImplementedError, <<-EOT.gsub(/^\s*/, '')
196
+ Don't know how to define macro for sequence: #{sequence.inspect}
197
+ EOT
198
+ end
199
+ end
200
+
201
+ def [](byte)
202
+ @storage.values.detect { |definition| definition.matches?(byte) }
203
+ end
204
+
205
+ def []=(key, definition)
206
+ @storage[key] = definition
207
+ end
208
+
209
+ private
210
+
211
+ def define_sequence_for_regex(regex, result, &blk)
212
+ @storage[regex] = Definition.new(
213
+ configuration: Configuration.new(
214
+ cancellation: @cancellation,
215
+ keymap: @keymap
216
+ ),
217
+ sequence: regex,
218
+ result: result,
219
+ &blk
220
+ )
221
+ end
222
+
223
+ def recursively_define_sequence_for_bytes(configuration, bytes, result, &blk)
224
+ byte, rest = bytes[0], bytes[1..-1]
225
+ if rest.any?
226
+ definition = Definition.new(
227
+ configuration: Configuration.new(
228
+ cancellation: @cancellation,
229
+ keymap: @keymap
230
+ ),
231
+ sequence: byte,
232
+ result: nil,
233
+ &blk
234
+ )
235
+ configuration[byte] = definition
236
+ recursively_define_sequence_for_bytes(
237
+ definition.configuration,
238
+ rest,
239
+ result,
240
+ &blk
241
+ )
242
+ else
243
+ configuration[byte] = Definition.new(
244
+ configuration: Configuration.new(keymap: @keymap),
245
+ sequence: byte,
246
+ result: result,
247
+ &blk
248
+ )
249
+ end
250
+ end
251
+ end
252
+
253
+ class Definition
254
+ attr_reader :bytes, :configuration, :result, :sequence
255
+
256
+ def initialize(configuration: nil, sequence:, result: nil, &blk)
257
+ @fragment = false
258
+ @configuration = configuration
259
+ @sequence = sequence
260
+ @result = result
261
+ blk.call(@configuration) if blk
262
+ end
263
+
264
+ def fragment?
265
+ @fragment
266
+ end
267
+
268
+ def fragment!
269
+ @fragment = true
270
+ end
271
+
272
+ def matches?(byte)
273
+ if @sequence.is_a?(Regexp)
274
+ @match_data = @sequence.match(byte.chr)
275
+ else
276
+ @sequence == byte
277
+ end
278
+ end
279
+
280
+ def process
281
+ if @result
282
+ if @match_data
283
+ if @match_data.captures.empty?
284
+ @result.call(@match_data[0])
285
+ else
286
+ @result.call(*@match_data.captures)
287
+ end
288
+ else
289
+ @result.call
290
+ end
291
+ end
292
+ end
293
+ end
294
+
295
+ end