yap-shell 0.4.0 → 0.4.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 70c4b7a17173c5d2f16dd2d318ad7fca64121c2b
4
- data.tar.gz: f17c759ac10afbb5ed4d9bac3c47cdfc2e8590f8
3
+ metadata.gz: cca1d07b231bb416a8b295be3f63df4d343de0c9
4
+ data.tar.gz: 152bf268dedcba028e1f288a781f47fc818b780f
5
5
  SHA512:
6
- metadata.gz: e998bdac5eccd88ff29438bed0dc970371453a11e300049988b328174940a179c94d3f3ce90e061500ff3c87022517baa7380ae7d88d41304c31c234d092fea9
7
- data.tar.gz: 6c7b6164c54f5dc7a3a1cca6a2a78d15dd7a31af58782cb7a7f4b0ff57cce86d3893aa920d668d3f054602a48e80be5b7ef331ca1720af0ca637d766c2ee3e69
6
+ metadata.gz: 5fcb45c7edd79541be56467df065440dd90b723d8291733407c9bf76a63b896ec46a62278cd4acdb65064bcaf6e2fb97461e97ccdd3669434d5879f866078df9
7
+ data.tar.gz: e600e1906665cf39be024ec45dd1fc88ecc397ff898e81adb129b2e1392a0d837bbd47c716b15eb75be4e394addb769c0ec8bd425a223a05ae1abb5d846f7469
data/.gitignore CHANGED
@@ -20,3 +20,5 @@ tmp
20
20
  *.o
21
21
  *.a
22
22
  mkmf.log
23
+
24
+ wiki/
@@ -22,6 +22,7 @@ class History < Addon
22
22
  end
23
23
 
24
24
  def save
25
+ debug_log "saving history file=#{file.inspect}"
25
26
  File.open(file, "a") do |file|
26
27
  # Don't write the YAML header because we're going to append to the
27
28
  # history file, not overwrite. YAML works fine without it.
@@ -40,6 +41,7 @@ class History < Addon
40
41
  private
41
42
 
42
43
  def load_history
44
+ debug_log "loading history file=#{file.inspect}"
43
45
  at_exit { save }
44
46
 
45
47
  if File.exists?(file) && File.readable?(file)
@@ -1,6 +1,20 @@
1
1
  class KeyboardMacros < Addon
2
2
  require 'keyboard_macros/cycle'
3
3
 
4
+ module PrettyPrintKey
5
+ # ppk means "pretty print key". For example, it returns \C-g if the given
6
+ # byte is 7.
7
+ def ppk(byte)
8
+ if byte && byte.ord <= 26
9
+ '\C-' + ('a'..'z').to_a[byte.ord - 1]
10
+ else
11
+ byte.inspect
12
+ end
13
+ end
14
+ end
15
+
16
+ include PrettyPrintKey
17
+
4
18
  DEFAULT_TRIGGER_KEY = :ctrl_g
5
19
  DEFAULT_CANCEL_KEY = " "
6
20
  DEFAULT_TIMEOUT_IN_MS = 500
@@ -24,7 +38,29 @@ class KeyboardMacros < Addon
24
38
  @cancel_on_unknown_sequences = false
25
39
  end
26
40
 
41
+ def cancel_key=(key)
42
+ debug_log "setting default cancel_key key=#{ppk(key)}"
43
+ @cancel_key = key
44
+ end
45
+
46
+ def cancel_on_unknown_sequences=(true_or_false)
47
+ debug_log "setting default cancel_on_unknown_sequences=#{true_or_false}"
48
+ @cancel_on_unknown_sequences = true_or_false
49
+ end
50
+
51
+ def timeout_in_ms=(milliseconds)
52
+ debug_log "setting default timeout_in_ms milliseconds=#{milliseconds.inspect}"
53
+ @timeout_in_ms = milliseconds
54
+ end
55
+
56
+ def trigger_key=(key)
57
+ debug_log "setting default trigger_key key=#{ppk(key)}"
58
+ @trigger_key = key
59
+ end
60
+
27
61
  def configure(cancel_key: nil, trigger_key: nil, &blk)
62
+ debug_log "configure cancel_key=#{ppk(cancel_key)} trigger_key=#{ppk(trigger_key)} block_given?=#{block_given?}"
63
+
28
64
  cancel_key ||= @cancel_key
29
65
  trigger_key ||= @trigger_key
30
66
 
@@ -45,12 +81,18 @@ class KeyboardMacros < Addon
45
81
 
46
82
  world.unbind(trigger_key)
47
83
  world.bind(trigger_key) do
84
+ debug_log "macro triggered key=#{ppk(trigger_key)}"
85
+
48
86
  begin
49
87
  @previous_result = nil
50
88
  @stack << OpenStruct.new(configuration: configuration)
51
89
  configuration.start.call if configuration.start
90
+
91
+ debug_log "taking over keyboard input processing from editor"
52
92
  world.editor.keyboard_input_processors.push(self)
53
- world.editor.input.wait_timeout_in_seconds = 0.1
93
+
94
+ wait_timeout_in_seconds = 0.1
95
+ world.editor.input.wait_timeout_in_seconds = wait_timeout_in_seconds
54
96
  ensure
55
97
  queue_up_remove_input_processor(&configuration.stop)
56
98
  end
@@ -60,6 +102,8 @@ class KeyboardMacros < Addon
60
102
  end
61
103
 
62
104
  def cycle(name, &cycle_thru_blk)
105
+ debug_log "defining cycle name=#{name.inspect}"
106
+
63
107
  @cycles ||= {}
64
108
  if block_given?
65
109
  cycle = KeyboardMacros::Cycle.new(
@@ -126,7 +170,6 @@ class KeyboardMacros < Addon
126
170
 
127
171
  def process_result(result)
128
172
  if result.is_a?(String)
129
- $z.puts "WRITING: #{result}"
130
173
  @world.editor.write result, add_to_line_history: false
131
174
  @previous_result = result
132
175
  end
@@ -146,13 +189,17 @@ class KeyboardMacros < Addon
146
189
  end
147
190
 
148
191
  def cancel_processing
192
+ debug_log "cancel_processing"
149
193
  @event_id = nil
150
194
  @stack.reverse.each do |definition|
151
195
  definition.configuration.stop.call if definition.configuration.stop
152
196
  end
153
197
  @stack.clear
154
198
  if world.editor.keyboard_input_processors.last == self
199
+ debug_log "giving keyboard input processing control back"
155
200
  world.editor.keyboard_input_processors.pop
201
+
202
+ debug_log "restoring default editor input timeout"
156
203
  world.editor.input.restore_default_timeout
157
204
  end
158
205
  end
@@ -171,8 +218,14 @@ class KeyboardMacros < Addon
171
218
  end
172
219
 
173
220
  class Configuration
221
+ include PrettyPrintKey
222
+
174
223
  attr_reader :cancellation, :trigger_key, :keymap
175
224
 
225
+ def debug_log(*args)
226
+ KeyboardMacros.debug_log(*args)
227
+ end
228
+
176
229
  def initialize(cancellation: nil, editor:, keymap: {}, trigger_key: nil)
177
230
  @cancellation = cancellation
178
231
  @editor = editor
@@ -183,6 +236,8 @@ class KeyboardMacros < Addon
183
236
  @on_stop_blk = nil
184
237
  @cycles = {}
185
238
 
239
+ debug_log "configuring a macro trigger_key=#{ppk(trigger_key)}"
240
+
186
241
  if @cancellation
187
242
  define @cancellation.cancel_key, -> { @cancellation.call }
188
243
  end
@@ -199,6 +254,8 @@ class KeyboardMacros < Addon
199
254
  end
200
255
 
201
256
  def cycle(name, &cycle_thru_blk)
257
+ debug_log "defining a cycle on macro name=#{name.inspect}"
258
+
202
259
  if block_given?
203
260
  cycle = KeyboardMacros::Cycle.new(
204
261
  cycle_proc: cycle_thru_blk,
@@ -217,6 +274,7 @@ class KeyboardMacros < Addon
217
274
  end
218
275
 
219
276
  def define(sequence, result=nil, fragment: false, &blk)
277
+ debug_log "defining macro sequence=#{sequence.inspect} result=#{result.inspect} fragment=#{fragment.inspect} under macro #{ppk(trigger_key)}"
220
278
  unless result.respond_to?(:call)
221
279
  string_result = result
222
280
  result = -> { string_result }
@@ -26,6 +26,8 @@ class TabCompletion < Addon
26
26
  def to_s
27
27
  @text
28
28
  end
29
+ alias :to_str :to_s
30
+ alias :inspect :to_s
29
31
  end
30
32
 
31
33
  COMPLETIONS = [ BasicCompletion ]
@@ -69,6 +71,8 @@ class TabCompletion < Addon
69
71
  @display_procs = DISPLAY_PROCS.dup
70
72
 
71
73
  editor.on_word_complete do |event|
74
+ debug_log "on_word_complete event: #{event}"
75
+
72
76
  sub_word = event[:payload][:sub_word]
73
77
  word = event[:payload][:word]
74
78
  actual_completion = event[:payload][:completion]
@@ -98,6 +102,8 @@ class TabCompletion < Addon
98
102
  end
99
103
 
100
104
  editor.on_word_complete_no_match do |event|
105
+ debug_log "on_word_complete_no_match event: #{event}"
106
+
101
107
  sub_word = event[:payload][:sub_word]
102
108
  word = event[:payload][:word]
103
109
  editor.content_box.children = []
@@ -105,6 +111,8 @@ class TabCompletion < Addon
105
111
  end
106
112
 
107
113
  editor.on_word_complete_done do |event|
114
+ debug_log "on_word_complete_done event: #{event}"
115
+
108
116
  # TODO: add a better way to clear content
109
117
  editor.content_box.children = []
110
118
  end
@@ -112,15 +120,19 @@ class TabCompletion < Addon
112
120
 
113
121
  def add_completion(name, pattern, &blk)
114
122
  raise ArgumentError, "Must supply block!" unless block_given?
123
+ debug_log "NO-OP add_completion for name=#{name.inspect} pattern=#{pattern.inspect} block?=#{block_given?}"
115
124
  # @completions.push CustomCompletion.new(name:name, pattern:pattern, world:world, &blk)
116
125
  end
117
126
 
118
127
  def set_decoration(type, &blk)
119
128
  raise ArgumentError, "Must supply block!" unless block_given?
129
+ debug_log "set_decoration for type=#{name.inspect}"
120
130
  @style_procs[type] = blk
121
131
  end
122
132
 
123
133
  def complete(word, words, word_index)
134
+ debug_log "complete word=#{word.inspect} words=#{words.inspect} word_index=#{word_index.inspect}"
135
+
124
136
  matches = @completions.sort_by(&:priority).reverse.map do |completion|
125
137
  if completion.respond_to?(:call)
126
138
  completion.call
@@ -135,6 +147,7 @@ class TabCompletion < Addon
135
147
  end
136
148
  end.flatten
137
149
 
150
+ debug_log "complete possible matches are #{matches.inspect}"
138
151
  matches
139
152
  end
140
153
 
data/bin/yap-dev CHANGED
@@ -4,9 +4,5 @@ require 'bundler'
4
4
  Bundler.setup
5
5
  require 'pry'
6
6
 
7
- $z = File.open("/tmp/z.log", "w+")
8
- $z.sync = true
9
-
10
7
  $LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
11
-
12
8
  load File.dirname(__FILE__) + '/yap'
data/lib/tasks/gem.rake CHANGED
@@ -2,7 +2,7 @@ namespace :bump do
2
2
  namespace :version do
3
3
  class ProjectVersion
4
4
  FILE = File.dirname(__FILE__) + '/../yap/shell/version.rb'
5
- PATTERN = /VERSION\s*=\s*"(\d+)\.(\d+)\.(\d+)"/m
5
+ PATTERN = /VERSION\s*=\s*['"](\d+)\.(\d+)\.(\d+)['"]/m
6
6
 
7
7
  def initialize(file=FILE, pattern=PATTERN)
8
8
  @file = file
data/lib/yap.rb CHANGED
@@ -1,9 +1,12 @@
1
+ require 'treefell'
2
+
1
3
  module Yap
2
4
  require 'yap/configuration'
3
5
  require 'yap/shell'
4
6
  require 'yap/world'
5
7
 
6
8
  def self.run_shell
9
+ Treefell['shell'].puts "#{self}.#{__callee__} booting shell"
7
10
  addons = [
8
11
  World::Addons.load_directories(configuration.addon_paths),
9
12
  World::Addons.load_rcfiles(configuration.rcfiles)
data/lib/yap/shell.rb CHANGED
@@ -37,6 +37,8 @@ module Yap
37
37
  @stdout.sync = true
38
38
  @stderr.sync = true
39
39
 
40
+ addons_str = "\n - " + addons.map(&:class).map(&:name).join("\n - ")
41
+ Treefell['shell'].puts "Constructing world instance with addons: #{addons_str}"
40
42
  @world = Yap::World.instance(addons:addons)
41
43
  end
42
44
 
@@ -69,8 +71,16 @@ module Yap
69
71
  )
70
72
 
71
73
  @world.repl.on_input do |input|
74
+ Treefell['shell'].puts "repl received input: #{input.inspect}"
72
75
  evaluation = Yap::Shell::Evaluation.new(stdin:@stdin, stdout:@stdout, stderr:@stderr, world:@world)
73
76
  evaluation.evaluate(input) do |command, stdin, stdout, stderr, wait|
77
+ Treefell['shell'].puts <<-DEBUG.gsub(/^\s*\|/, '')
78
+ |adding #{command} to run in context of:
79
+ | stdin: #{stdin.inspect}
80
+ | stdout: #{stdout.inspect}
81
+ | stderr: #{stderr.inspect}
82
+ | wait for child process to complete? #{wait.inspect}
83
+ DEBUG
74
84
  context.clear_commands
75
85
  context.add_command_to_run command, stdin:stdin, stdout:stdout, stderr:stderr, wait:wait
76
86
 
@@ -81,12 +91,14 @@ module Yap
81
91
  end
82
92
 
83
93
  begin
94
+ Treefell['shell'].puts "enter interactive mode"
84
95
  @world.interactive!
85
96
  # rescue Errno::EIO => ex
86
97
  # # This happens when yap is no longer the foreground process
87
98
  # # but it tries to receive input/output from the tty. I believe it
88
99
  # # is a race condition when launching a child process.
89
100
  rescue Interrupt
101
+ Treefell['shell'].puts "^C"
90
102
  @world.editor.puts "^C"
91
103
  retry
92
104
  rescue Exception => ex
@@ -7,6 +7,7 @@ module Yap::Shell
7
7
  def initialize
8
8
  @file = ENV["HOME"] + "/.yapaliases.yml"
9
9
  @aliases = begin
10
+ Treefell['shell'].puts "reading aliases from disk: #{@file}"
10
11
  YAML.load_file(@file)
11
12
  rescue
12
13
  {}
@@ -18,16 +19,21 @@ module Yap::Shell
18
19
  end
19
20
 
20
21
  def fetch_alias(name)
21
- @aliases[name]
22
+ @aliases[name].tap do |contents|
23
+ Treefell['shell'].puts "alias fetched name=#{name} contents=#{contents.inspect}"
24
+ end
22
25
  end
23
26
 
24
- def set_alias(name, command)
25
- @aliases[name] = command
26
- File.write @file, @aliases.to_yaml
27
+ def set_alias(name, contents)
28
+ @aliases[name] = contents
29
+ Treefell['shell'].puts "alias set name=#{name} to #{contents.inspect}"
30
+ write_to_disk
27
31
  end
28
32
 
29
33
  def unset_alias(name)
30
34
  @aliases.delete(name)
35
+ Treefell['shell'].puts "alias unset name=#{name}"
36
+ write_to_disk
31
37
  end
32
38
 
33
39
  def has_key?(key)
@@ -40,5 +46,13 @@ module Yap::Shell
40
46
  h
41
47
  end
42
48
  end
49
+
50
+ private
51
+
52
+ def write_to_disk
53
+ File.write(@file, @aliases.to_yaml).tap do
54
+ Treefell['shell'].puts "aliases written to disk: #{@file}"
55
+ end
56
+ end
43
57
  end
44
58
  end
@@ -16,7 +16,7 @@ module Yap::Shell
16
16
  end
17
17
  output << ""
18
18
  else
19
- name_eq_value = args.first
19
+ name_eq_value = args.map(&:shellsplit).join(' ')
20
20
  name, command = name_eq_value.scan(/^(.*?)\s*=\s*(.*)$/).flatten
21
21
  Yap::Shell::Aliases.instance.set_alias name, command
22
22
  end
@@ -33,6 +33,12 @@ module Yap::Shell
33
33
  @line = line
34
34
  end
35
35
 
36
+ def to_s
37
+ "#{self.class.name}(#{str.inspect})"
38
+ end
39
+ alias :to_str :to_s
40
+ alias :inspect :to_s
41
+
36
42
  def to_executable_str
37
43
  raise NotImplementedError, ":to_executable_str must be implemented by including object."
38
44
  end
@@ -90,6 +96,12 @@ module Yap::Shell
90
96
  :FileSystemCommand
91
97
  end
92
98
 
99
+ def to_s
100
+ "#{self.class.name}(#{to_executable_str.inspect})"
101
+ end
102
+ alias :to_str :to_s
103
+ alias :inspect :to_s
104
+
93
105
  def to_executable_str
94
106
  [
95
107
  str,
@@ -14,22 +14,39 @@ module Yap::Shell
14
14
 
15
15
  def evaluate(input, &blk)
16
16
  @blk = blk
17
+ debug_log "evaluation begins input=#{input.inspect}"
17
18
  @input = recursively_find_and_replace_command_substitutions(input)
19
+ debug_log "recursive find/replace command substitutions results in input=#{@input.inspect}"
18
20
  ast = Parser.parse(@input)
19
- ast.accept(self)
21
+ debug_log "parsed input into AST: #{ast}"
22
+ debug_log "beginning to walk AST"
23
+ ast.accept(self).tap do
24
+ debug_log "done walking AST"
25
+ end
20
26
  end
21
27
 
22
28
  def set_last_result(result)
29
+ debug_log "evaluation setting last result=#{result.inspect}"
23
30
  @world.last_result = result
24
31
  end
25
32
 
26
33
  private
27
34
 
35
+ def debug_log(message)
36
+ Treefell['shell'].puts message
37
+ end
38
+
39
+ def debug_visit(node, msg='')
40
+ calling_method = caller[0][/`.*'/][1..-2]
41
+ debug_log "evaluation #{calling_method} node=#{node} #{msg}"
42
+ end
43
+
28
44
  # +recursively_find_and_replace_command_substitutions+ is responsible for recursively
29
45
  # finding and expanding command substitutions, in a depth first manner.
30
46
  def recursively_find_and_replace_command_substitutions(input)
31
47
  input = input.dup
32
48
  Parser.each_command_substitution_for(input) do |substitution_result, start_position, end_position|
49
+ debug_log "found command substitution at position=#{(start_position..end_position)} #{substitution_result.inspect}"
33
50
  result = recursively_find_and_replace_command_substitutions(substitution_result.str)
34
51
  position = substitution_result.position
35
52
  ast = Parser.parse(result)
@@ -37,7 +54,19 @@ module Yap::Shell
37
54
  r,w = IO.pipe
38
55
  @stdout = w
39
56
  ast.accept(self)
40
- input[position.min...position.max] = r.read.chomp
57
+
58
+ output = r.read.chomp
59
+
60
+ # Treat consecutive newlines in output as a single space
61
+ output = output.gsub(/\n+/, ' ')
62
+
63
+ # Double quote the output and escape any double quotes already
64
+ # existing
65
+ output = %|"#{output.gsub(/"/, '\\"')}"|
66
+
67
+ # Put thd output back into the original input
68
+ debug_log "replacing command substitution at position=#{(position.min...position.max)} with #{output.inspect}"
69
+ input[position.min...position.max] = output
41
70
  end
42
71
  end
43
72
  input
@@ -51,11 +80,13 @@ module Yap::Shell
51
80
  ######################################################################
52
81
 
53
82
  def visit_CommandNode(node)
83
+ debug_visit(node)
54
84
  @aliases_expanded ||= []
55
85
  @command_node_args_stack ||= []
56
86
  with_standard_streams do |stdin, stdout, stderr|
57
87
  args = process_ArgumentNodes(node.args)
58
- if !node.literal? && !@aliases_expanded.include?(node.command) && _alias=Aliases.instance.fetch_alias(node.command)
88
+ if !node.literal? && !@aliases_expanded.include?(node.command) && Aliases.instance.has_key?(node.command)
89
+ _alias=Aliases.instance.fetch_alias(node.command)
59
90
  @suppress_events = true
60
91
  @command_node_args_stack << args
61
92
  ast = Parser.parse(_alias)
@@ -66,7 +97,7 @@ module Yap::Shell
66
97
  else
67
98
  cmd2execute = variable_expand(node.command)
68
99
  final_args = (args + @command_node_args_stack).flatten.map(&:shellescape)
69
- expanded_args = shell_expand(final_args)
100
+ expanded_args = final_args
70
101
  command = CommandFactory.build_command_for(
71
102
  world: world,
72
103
  command: cmd2execute,
@@ -82,10 +113,12 @@ module Yap::Shell
82
113
  end
83
114
 
84
115
  def visit_CommentNode(node)
116
+ debug_visit(node)
85
117
  # no-op, do nothing
86
118
  end
87
119
 
88
120
  def visit_RangeNode(node)
121
+ debug_visit(node)
89
122
  range = node.head.value
90
123
  if node.tail
91
124
  @current_range_values = range.to_a
@@ -97,6 +130,7 @@ module Yap::Shell
97
130
  end
98
131
 
99
132
  def visit_RedirectionNode(node)
133
+ debug_visit(node)
100
134
  filename = node.target
101
135
 
102
136
  if File.directory?(filename)
@@ -114,6 +148,8 @@ module Yap::Shell
114
148
  end
115
149
 
116
150
  def visit_BlockNode(node)
151
+ debug_visit(node)
152
+
117
153
  with_standard_streams do |stdin, stdout, stderr|
118
154
  # Modify @stdout and @stderr for the first command
119
155
  stdin, @stdout = IO.pipe
@@ -150,10 +186,10 @@ module Yap::Shell
150
186
  end
151
187
  end
152
188
  end
153
-
154
189
  end
155
190
 
156
191
  def visit_NumericalRangeNode(node)
192
+ debug_visit(node)
157
193
  node.range.each do |n|
158
194
  if node.tail
159
195
  if node.reference
@@ -169,6 +205,7 @@ module Yap::Shell
169
205
  end
170
206
 
171
207
  def visit_StatementsNode(node)
208
+ debug_visit(node)
172
209
  Yap::Shell::Execution::Context.fire :before_statements_execute, @world unless @suppress_events
173
210
  node.head.accept(self)
174
211
  if node.tail
@@ -194,9 +231,10 @@ module Yap::Shell
194
231
  # 5
195
232
  #
196
233
  def visit_EnvWrapperNode(node)
234
+ debug_visit(node)
197
235
  with_env do
198
236
  node.env.each_pair do |env_var_name, arg_node|
199
- world.env[env_var_name] = process_ArgumentNode(arg_node)
237
+ world.env[env_var_name] = process_EnvArgumentNode(arg_node)
200
238
  end
201
239
  node.node.accept(self)
202
240
  end
@@ -213,13 +251,15 @@ module Yap::Shell
213
251
  # they cleared or overridden.
214
252
  #
215
253
  def visit_EnvNode(node)
254
+ debug_visit(node)
216
255
  node.env.each_pair do |key, arg_node|
217
- world.env[key] = process_ArgumentNode(arg_node)
256
+ world.env[key] = process_EnvArgumentNode(arg_node)
218
257
  end
219
258
  Yap::Shell::Execution::Result.new(status_code:0, directory:Dir.pwd, n:1, of:1)
220
259
  end
221
260
 
222
261
  def visit_ConditionalNode(node)
262
+ debug_visit(node)
223
263
  case node.operator
224
264
  when '&&'
225
265
  node.expr1.accept self
@@ -237,6 +277,8 @@ module Yap::Shell
237
277
  end
238
278
 
239
279
  def visit_PipelineNode(node, options={})
280
+ debug_visit(node)
281
+
240
282
  with_standard_streams do |stdin, stdout, stderr|
241
283
  # Modify @stdout and @stderr for the first command
242
284
  stdin, @stdout = IO.pipe
@@ -260,6 +302,8 @@ module Yap::Shell
260
302
  end
261
303
 
262
304
  def visit_InternalEvalNode(node)
305
+ debug_visit(node)
306
+
263
307
  command = CommandFactory.build_command_for(
264
308
  world: world,
265
309
  command: node.command,
@@ -281,10 +325,31 @@ module Yap::Shell
281
325
  end
282
326
 
283
327
  def process_ArgumentNode(node)
284
- if node.quoted?
285
- variable_expand(node.lvalue)
328
+ if node.single_quoted?
329
+ debug_visit(node, 'single quoted argument, performing no expansion')
330
+ ["'#{node.lvalue}'"]
331
+ elsif node.double_quoted?
332
+ debug_visit(node, 'double quoted argument, performing variable expansion')
333
+ [variable_expand(node.lvalue)]
286
334
  else
287
- shell_expand(node.lvalue, escape_directory_expansions: false).first
335
+ debug_visit(node, 'unquoted argument, performing shell expansion')
336
+ shell_expand(node.lvalue, escape_directory_expansions: false)
337
+ end
338
+ end
339
+
340
+ # process_EnvArgumentNode is separate from process_ArgumentNode since
341
+ # it doesn't allow full shell expansion. For example, the following
342
+ # will not be expanded to "FOO=a_1 a_2", it will be stored as provided:
343
+ #
344
+ # > FOO=a_{1,2}
345
+ #
346
+ def process_EnvArgumentNode(node)
347
+ if node.single_quoted?
348
+ debug_visit(node, 'single quoted argument, performing no expansion')
349
+ node.lvalue
350
+ else
351
+ debug_visit(node, 'not singled quoted argument, performing variable expansion')
352
+ variable_expand(node.lvalue)
288
353
  end
289
354
  end
290
355
 
@@ -316,6 +381,8 @@ module Yap::Shell
316
381
  end
317
382
 
318
383
  def stream_redirections_for(node)
384
+ debug_visit(node)
385
+
319
386
  stdin, stdout, stderr = @stdin, @stdout, @stderr
320
387
  node.redirects.each do |redirect|
321
388
  case redirect.kind