yap-shell 0.4.0 → 0.4.1

Sign up to get free protection for your applications and to get access to all the features.
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