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
data/lib/yap.rb CHANGED
@@ -2,62 +2,11 @@ require 'yap/shell'
2
2
  require 'yap/world'
3
3
 
4
4
  module Yap
5
- module WorldAddons
6
- def self.syntax_ok?(file)
7
- `ruby -c #{file}`
8
- $?.exitstatus == 0
9
- end
10
-
11
- def self.load_from_files(files:[])
12
- files.map do |file|
13
- (puts "Cannot load world addon: #{file} does not exist" and next) unless File.exist?(file)
14
- (puts "Cannot load world addon: #{file} is not readable" and next) unless File.exist?(file)
15
- (puts "Cannot load world addon: #{file} is a directory file" and next) if File.directory?(file)
16
-
17
- addon = file.end_with?("rc") ? load_rcfile(file) : load_addon_file(file)
18
- addon.load
19
- end
20
- end
21
-
22
- class RcFile
23
- def initialize(contents)
24
- @contents = contents
25
- end
26
-
27
- def load
28
- self
29
- end
30
-
31
- def initialize_world(world)
32
- world.instance_eval @contents
33
- end
34
- end
35
-
36
- def self.load_rcfile(file)
37
- RcFile.new IO.read(file)
38
- end
39
-
40
- def self.load_addon_file(file)
41
- name = File.basename(file).sub(/\.[^\.]+$/, "").split(/[_]/).map(&:capitalize).join
42
- klass_name = "Yap::WorldAddons::#{name}"
43
-
44
- load file
45
-
46
- if Yap::WorldAddons.const_defined?(name)
47
- Yap::WorldAddons.const_get(name)
48
- else
49
- raise("Did not find #{klass_name} in #{file}")
50
- end
51
- end
52
- end
53
-
54
5
  def self.run_shell
55
- addon_files = Dir[
56
- "#{ENV['HOME']}/.yaprc",
57
- "#{ENV['HOME']}/.yap-addons/*.rb"
58
- ]
6
+ addons = []
7
+ addons.push World::Addons.load_directories Dir["#{ENV['HOME']}/.yap-addons/*"]
8
+ addons.push World::Addons.load_rcfiles Dir["#{ENV['HOME']}/.yaprc"]
59
9
 
60
- addons = WorldAddons.load_from_files(files:addon_files)
61
- Shell::Impl.new(addons: addons).repl
10
+ Shell::Impl.new(addons: addons.flatten).repl
62
11
  end
63
12
  end
data/lib/yap/shell.rb CHANGED
@@ -2,6 +2,7 @@ require 'readline'
2
2
  require 'yaml'
3
3
  require 'yap/shell/version'
4
4
  require 'yap/shell/builtins'
5
+ require 'fcntl'
5
6
 
6
7
  module Yap
7
8
  module Shell
@@ -22,6 +23,12 @@ module Yap
22
23
 
23
24
  class Impl
24
25
  def initialize(addons:)
26
+ @original_file_descriptor_flags = {
27
+ stdin: $stdin.fcntl(Fcntl::F_GETFL, 0),
28
+ stdout: $stdout.fcntl(Fcntl::F_GETFL, 0),
29
+ stderr: $stderr.fcntl(Fcntl::F_GETFL, 0)
30
+ }
31
+
25
32
  @stdin = $stdin
26
33
  @stdout = $stdout
27
34
  @stderr = $stderr
@@ -29,29 +36,63 @@ module Yap
29
36
  @stdout.sync = true
30
37
  @stderr.sync = true
31
38
 
32
- @addons = addons
39
+ @world = Yap::World.instance(addons:addons)
40
+ end
41
+
42
+ # Yields to the passed in block after restoring the file descriptor
43
+ # flags that Yap started in. This ensures that any changes Yap has
44
+ # made to run the shell don't interfere with child processes.
45
+ def with_original_file_descriptor_flags(&block)
46
+ current_file_descriptor_flags = {
47
+ stdin: $stdin.fcntl(Fcntl::F_GETFL, 0),
48
+ stdout: $stdout.fcntl(Fcntl::F_GETFL, 0),
49
+ stderr: $stderr.fcntl(Fcntl::F_GETFL, 0)
50
+ }
51
+
52
+ $stdin.fcntl(Fcntl::F_SETFL, @original_file_descriptor_flags[:stdin])
53
+ $stdout.fcntl(Fcntl::F_SETFL, @original_file_descriptor_flags[:stdout])
54
+ $stderr.fcntl(Fcntl::F_SETFL, @original_file_descriptor_flags[:stderr])
55
+
56
+ yield
57
+ ensure
58
+ $stdin.fcntl(Fcntl::F_SETFL, current_file_descriptor_flags[:stdin])
59
+ $stdout.fcntl(Fcntl::F_SETFL, current_file_descriptor_flags[:stdout])
60
+ $stderr.fcntl(Fcntl::F_SETFL, current_file_descriptor_flags[:stderr])
33
61
  end
34
62
 
35
63
  def repl
36
- @world = Yap::World.new(addons:@addons)
37
64
  context = Yap::Shell::Execution::Context.new(
38
65
  stdin: @stdin,
39
66
  stdout: @stdout,
40
67
  stderr: @stderr
41
68
  )
42
69
 
43
- @repl = Yap::Shell::Repl.new(world:@world)
44
- @repl.loop_on_input do |input|
45
- evaluation = Yap::Shell::Evaluation.new(stdin:@stdin, stdout:@stdout, stderr:@stderr)
46
- evaluation.evaluate(input) do |command, stdin, stdout, stderr|
70
+ @world.repl.on_input do |input|
71
+ evaluation = Yap::Shell::Evaluation.new(stdin:@stdin, stdout:@stdout, stderr:@stderr, world:@world)
72
+ evaluation.evaluate(input) do |command, stdin, stdout, stderr, wait|
47
73
  context.clear_commands
48
- context.add_command_to_run command, stdin:stdin, stdout:stdout, stderr:stderr
49
- context.execute(world:@world)
74
+ context.add_command_to_run command, stdin:stdin, stdout:stdout, stderr:stderr, wait:wait
75
+
76
+ with_original_file_descriptor_flags do
77
+ context.execute(world:@world)
78
+ end
50
79
  end
51
80
  end
52
- end
53
81
 
82
+ begin
83
+ @world.interactive!
84
+ # rescue Errno::EIO => ex
85
+ # # This happens when yap is no longer the foreground process
86
+ # # but it tries to receive input/output from the tty. I believe it
87
+ # # is a race condition when launching a child process.
88
+ rescue Interrupt
89
+ @world.editor.puts "^C"
90
+ retry
91
+ rescue Exception => ex
92
+ require 'pry'
93
+ binding.pry unless ex.is_a?(SystemExit)
94
+ end
95
+ end
54
96
  end
55
-
56
97
  end
57
98
  end
@@ -6,8 +6,8 @@ module Yap::Shell
6
6
  Yap::Shell::BuiltinCommand.add(name, &blk)
7
7
  end
8
8
 
9
- def self.execute_builtin(name, args:, stdin:, stdout:, stderr:)
10
- command = Yap::Shell::BuiltinCommand.new(str:name, args: args)
9
+ def self.execute_builtin(name, world:, args:, stdin:, stdout:, stderr:)
10
+ command = Yap::Shell::BuiltinCommand.new(world:world, str:name, args: args)
11
11
  command.execute(stdin:stdin, stdout:stdout, stderr:stderr)
12
12
  end
13
13
 
@@ -3,7 +3,7 @@ require 'shellwords'
3
3
 
4
4
  module Yap::Shell
5
5
  module Builtins
6
- builtin :alias do |*args|
6
+ builtin :alias do |args:, stdout:, **kwargs|
7
7
  output = []
8
8
  if args.empty?
9
9
  Yap::Shell::Aliases.instance.to_h.each_pair do |name, value|
@@ -19,7 +19,7 @@ module Yap::Shell
19
19
  name, command = name_eq_value.scan(/^(.*?)\s*=\s*(.*)$/).flatten
20
20
  Yap::Shell::Aliases.instance.set_alias name, command
21
21
  end
22
- output.join("\n")
22
+ stdout.puts output.join("\n")
23
23
  end
24
24
  end
25
25
  end
@@ -3,13 +3,12 @@ module Yap::Shell
3
3
  DIRECTORY_HISTORY = []
4
4
  DIRECTORY_FUTURE = []
5
5
 
6
- builtin :cd do |args:, stderr:, **|
7
- path = args.first || ENV['HOME']
8
- cwd = Dir.pwd
6
+ builtin :cd do |world:, args:, stderr:, **|
7
+ path = args.first || world.env['HOME']
9
8
  if Dir.exist?(path)
10
- DIRECTORY_HISTORY << cwd
9
+ DIRECTORY_HISTORY << Dir.pwd
10
+ world.env["PWD"] = File.expand_path(path)
11
11
  Dir.chdir(path)
12
- ENV["PWD"] = cwd
13
12
  exit_status = 0
14
13
  else
15
14
  stderr.puts "cd: #{path}: No such file or directory"
@@ -17,15 +16,14 @@ module Yap::Shell
17
16
  end
18
17
  end
19
18
 
20
- builtin :popd do |args:, stderr:, **keyword_args|
19
+ builtin :popd do |world:, args:, stderr:, **keyword_args|
21
20
  output = []
22
- cwd = Dir.pwd
23
21
  if DIRECTORY_HISTORY.any?
24
22
  path = DIRECTORY_HISTORY.pop
25
23
  if Dir.exist?(path)
26
- DIRECTORY_FUTURE << cwd
24
+ DIRECTORY_FUTURE << Dir.pwd
27
25
  Dir.chdir(path)
28
- ENV["PWD"] =cwd
26
+ world.env["PWD"] = Dir.pwd
29
27
  exit_status = 0
30
28
  else
31
29
  stderr.puts "popd: #{path}: No such file or directory"
@@ -37,14 +35,14 @@ module Yap::Shell
37
35
  end
38
36
  end
39
37
 
40
- builtin :pushd do |args:, stderr:, **keyword_args|
38
+ builtin :pushd do |world:, args:, stderr:, **keyword_args|
41
39
  output = []
42
40
  if DIRECTORY_FUTURE.any?
43
41
  path = DIRECTORY_FUTURE.pop
44
42
  if Dir.exist?(path)
45
43
  DIRECTORY_HISTORY << Dir.pwd
46
44
  Dir.chdir(path)
47
- ENV["PWD"] = path
45
+ world.env["PWD"] = Dir.pwd
48
46
  exit_status = 0
49
47
  else
50
48
  stderr.puts "pushd: #{path}: No such file or directory"
@@ -0,0 +1,11 @@
1
+ module Yap::Shell
2
+ module Builtins
3
+
4
+ builtin :env do |world:, args:, stdout:, **|
5
+ world.env.keys.sort.each do |key|
6
+ stdout.puts "#{key}=#{world.env[key]}"
7
+ end
8
+ end
9
+
10
+ end
11
+ end
@@ -7,13 +7,13 @@ module Yap::Shell
7
7
  class CommandUnknownError < CommandError ; end
8
8
 
9
9
  class CommandFactory
10
- def self.build_command_for(command:, args:, heredoc:, internally_evaluate:)
11
- return RubyCommand.new(str:command) if internally_evaluate
10
+ def self.build_command_for(world:, command:, args:, heredoc:, internally_evaluate:, line:)
11
+ return RubyCommand.new(world:world, str:command) if internally_evaluate
12
12
 
13
13
  case command
14
- when ShellCommand then ShellCommand.new(str:command, args:args, heredoc:heredoc)
15
- when BuiltinCommand then BuiltinCommand.new(str:command, args:args, heredoc:heredoc)
16
- when FileSystemCommand then FileSystemCommand.new(str:command, args:args, heredoc:heredoc)
14
+ when ShellCommand then ShellCommand.new(world:world, str:command, args:args, heredoc:heredoc, line:line)
15
+ when BuiltinCommand then BuiltinCommand.new(world:world, str:command, args:args, heredoc:heredoc)
16
+ when FileSystemCommand then FileSystemCommand.new(world:world, str:command, args:args, heredoc:heredoc)
17
17
  else
18
18
  raise CommandUnknownError, "Don't know how to execute command: #{command}"
19
19
  end
@@ -21,13 +21,15 @@ module Yap::Shell
21
21
  end
22
22
 
23
23
  class Command
24
- attr_accessor :str, :args
24
+ attr_accessor :world, :str, :args, :line
25
25
  attr_accessor :heredoc
26
26
 
27
- def initialize(str:, args:[], heredoc:nil)
27
+ def initialize(world:, str:, args:[], line:nil, heredoc:nil)
28
+ @world = world
28
29
  @str = str
29
30
  @args = args
30
31
  @heredoc = heredoc
32
+ @line = line
31
33
  end
32
34
 
33
35
  def to_executable_str
@@ -54,7 +56,7 @@ module Yap::Shell
54
56
 
55
57
  def execute(stdin:, stdout:, stderr:)
56
58
  action = self.class.builtins.fetch(str.to_sym){ raise("Missing proc for builtin: '#{builtin}' in #{str.inspect}") }
57
- action.call args:args, stdin:stdin, stdout:stdout, stderr:stderr
59
+ action.call world:world, args:args, stdin:stdin, stdout:stdout, stderr:stderr
58
60
  end
59
61
 
60
62
  def type
@@ -67,6 +69,10 @@ module Yap::Shell
67
69
  end
68
70
 
69
71
  class FileSystemCommand < Command
72
+ def self.world
73
+ ::Yap::World.instance
74
+ end
75
+
70
76
  def self.===(other)
71
77
  command = other.split(/\s+/).detect{ |f| !f.include?("=") }
72
78
 
@@ -74,7 +80,7 @@ module Yap::Shell
74
80
  return true if File.executable?(command)
75
81
 
76
82
  # See if the command exists anywhere on the path
77
- ENV["PATH"].split(":").detect do |path|
83
+ world.env["PATH"].split(":").detect do |path|
78
84
  File.executable?(File.join(path, command))
79
85
  end
80
86
  end
@@ -86,7 +92,7 @@ module Yap::Shell
86
92
  def to_executable_str
87
93
  [
88
94
  str,
89
- args.map(&:shellescape).join(' ')
95
+ args.join(' ')
90
96
  ].join(' ')
91
97
  end
92
98
  end
@@ -96,13 +102,17 @@ module Yap::Shell
96
102
  (@registered_functions ||= {}).freeze
97
103
  end
98
104
 
99
- def self.define_shell_function(name, &blk)
100
- raise ArgumentError, "Must provided block when defining a shell function" unless blk
101
- (@registered_functions ||= {})[name.to_sym] = blk
105
+ def self.define_shell_function(name_or_pattern, &blk)
106
+ raise ArgumentError, "Must provide block when defining a shell function" unless blk
107
+ name_or_pattern = name_or_pattern.to_s if name_or_pattern.is_a?(Symbol)
108
+ name_or_pattern = /^#{Regexp.escape(name_or_pattern)}$/ if name_or_pattern.is_a?(String)
109
+ (@registered_functions ||= {})[name_or_pattern] = blk
102
110
  end
103
111
 
104
- def self.===(other)
105
- registered_functions.include?(other.to_sym)
112
+ def self.===(command)
113
+ registered_functions.detect do |name_or_pattern, *_|
114
+ name_or_pattern.match(command)
115
+ end
106
116
  end
107
117
 
108
118
  def type
@@ -110,9 +120,10 @@ module Yap::Shell
110
120
  end
111
121
 
112
122
  def to_proc
113
- self.class.registered_functions.fetch(str.to_sym){
114
- raise "Shell function #{str} was not found!"
115
- }
123
+ self.class.registered_functions.detect do |name_or_pattern, function_body|
124
+ return function_body if name_or_pattern.match(str)
125
+ end
126
+ raise "Shell function #{str} was not found!"
116
127
  end
117
128
  end
118
129
 
@@ -1,32 +1,38 @@
1
1
  require 'yap/shell/parser'
2
2
  require 'yap/shell/commands'
3
3
  require 'yap/shell/aliases'
4
+ require 'yap/shell/evaluation/shell_expansions'
4
5
 
5
6
  module Yap::Shell
6
7
  class Evaluation
7
- def initialize(stdin:, stdout:, stderr:)
8
+ attr_reader :world
9
+
10
+ def initialize(stdin:, stdout:, stderr:, world:)
8
11
  @stdin, @stdout, @stderr = stdin, stdout, stderr
9
- @last_result = nil
12
+ @world = world
10
13
  end
11
14
 
12
15
  def evaluate(input, &blk)
13
16
  @blk = blk
14
- parser = Yap::Shell::Parser.new
15
- input = recursively_find_and_replace_command_substitutions(parser, input)
16
- ast = Yap::Shell::Parser.new.parse(input)
17
+ @input = recursively_find_and_replace_command_substitutions(input)
18
+ ast = Parser.parse(@input)
17
19
  ast.accept(self)
18
20
  end
19
21
 
22
+ def set_last_result(result)
23
+ @world.last_result = result
24
+ end
25
+
20
26
  private
21
27
 
22
28
  # +recursively_find_and_replace_command_substitutions+ is responsible for recursively
23
29
  # finding and expanding command substitutions, in a depth first manner.
24
- def recursively_find_and_replace_command_substitutions(parser, input)
30
+ def recursively_find_and_replace_command_substitutions(input)
25
31
  input = input.dup
26
- parser.each_command_substitution_for(input) do |substitution_result, start_position, end_position|
27
- result = recursively_find_and_replace_command_substitutions(parser, substitution_result.str)
32
+ Parser.each_command_substitution_for(input) do |substitution_result, start_position, end_position|
33
+ result = recursively_find_and_replace_command_substitutions(substitution_result.str)
28
34
  position = substitution_result.position
29
- ast = parser.parse(result)
35
+ ast = Parser.parse(result)
30
36
  with_standard_streams do |stdin, stdout, stderr|
31
37
  r,w = IO.pipe
32
38
  @stdout = w
@@ -46,65 +52,183 @@ module Yap::Shell
46
52
 
47
53
  def visit_CommandNode(node)
48
54
  @aliases_expanded ||= []
55
+ @command_node_args_stack ||= []
49
56
  with_standard_streams do |stdin, stdout, stderr|
50
- args = node.args.map(&:lvalue)
57
+ args = node.args.map(&:lvalue).map do |arg|
58
+ shell_expand(arg, escape_directory_expansions: false)
59
+ end
51
60
  if !node.literal? && !@aliases_expanded.include?(node.command) && _alias=Aliases.instance.fetch_alias(node.command)
52
61
  @suppress_events = true
53
- ast = Yap::Shell::Parser.new.parse([_alias].concat(args).join(" "))
62
+ @command_node_args_stack << args
63
+ ast = Parser.parse(_alias)
54
64
  @aliases_expanded.push(node.command)
55
65
  ast.accept(self)
56
66
  @aliases_expanded.pop
57
67
  @suppress_events = false
58
68
  else
69
+ cmd2execute = variable_expand(node.command)
70
+ final_args = (args + @command_node_args_stack).flatten.shelljoin
71
+ expanded_args = shell_expand(final_args)
59
72
  command = CommandFactory.build_command_for(
60
- command: node.command,
61
- args: shell_expand(args),
62
- heredoc: node.heredoc,
63
- internally_evaluate: node.internally_evaluate?)
73
+ world: world,
74
+ command: cmd2execute,
75
+ args: expanded_args,
76
+ heredoc: (node.heredoc && node.heredoc.value),
77
+ internally_evaluate: node.internally_evaluate?,
78
+ line: @input)
64
79
  @stdin, @stdout, @stderr = stream_redirections_for(node)
65
- @last_result = @blk.call command, @stdin, @stdout, @stderr
80
+ set_last_result @blk.call command, @stdin, @stdout, @stderr, pipeline_stack.empty?
81
+ @command_node_args_stack.clear
82
+ end
83
+ end
84
+ end
85
+
86
+ def visit_CommentNode(node)
87
+ # no-op, do nothing
88
+ end
89
+
90
+ def visit_RangeNode(node)
91
+ range = node.head.value
92
+ if node.tail
93
+ @current_range_values = range.to_a
94
+ node.tail.accept(self)
95
+ @current_range_values = nil
96
+ else
97
+ @stdout.puts range.to_a.join(' ')
98
+ end
99
+ end
100
+
101
+ def visit_RedirectionNode(node)
102
+ filename = node.target
103
+
104
+ if File.directory?(filename)
105
+ puts <<-ERROR.gsub(/^\s*/m, '').lines.join(' ')
106
+ Whoops, #{filename.inspect} is a directory! Those can't be redirected to.
107
+ ERROR
108
+ set_last_result Yap::Shell::Execution::Result.new(status_code:1, directory:Dir.pwd, n:1, of:1)
109
+ elsif node.kind == ">"
110
+ File.write(filename, "")
111
+ set_last_result Yap::Shell::Execution::Result.new(status_code:0, directory:Dir.pwd, n:1, of:1)
112
+ else
113
+ puts "Sorry, #{node.kind} redirection isn't a thing, but >#{filename} is!"
114
+ set_last_result Yap::Shell::Execution::Result.new(status_code:2, directory:Dir.pwd, n:1, of:1)
115
+ end
116
+ end
117
+
118
+ def visit_BlockNode(node)
119
+ with_standard_streams do |stdin, stdout, stderr|
120
+ # Modify @stdout and @stderr for the first command
121
+ stdin, @stdout = IO.pipe
122
+
123
+ # Don't modify @stdin for the first command in the pipeline.
124
+ values = []
125
+ if node.head
126
+ node.head.accept(self)
127
+ values = stdin.read.split(/\s+/)
128
+ else
129
+ # assume range for now
130
+ values = @current_range_values
131
+ end
132
+
133
+ evaluate_block = lambda {
134
+ with_standard_streams do |stdin2, stdout2, stderr2|
135
+ @stdout = stdout
136
+ node.tail.accept(self)
137
+ end
138
+ }
139
+
140
+ if node.params.any?
141
+ values.each_slice(node.params.length).each do |_slice|
142
+ with_env do
143
+ Hash[ node.params.zip(_slice) ].each_pair do |k,v|
144
+ world.env[k] = v.to_s
145
+ end
146
+ evaluate_block.call
147
+ end
148
+ end
149
+ else
150
+ values.each do
151
+ evaluate_block.call
152
+ end
153
+ end
154
+ end
155
+
156
+ end
157
+
158
+ def visit_NumericalRangeNode(node)
159
+ node.range.each do |n|
160
+ if node.tail
161
+ if node.reference
162
+ with_env do
163
+ world.env[node.reference.value] = n.to_s
164
+ node.tail.accept(self)
165
+ end
166
+ else
167
+ node.tail.accept(self)
168
+ end
66
169
  end
67
170
  end
68
171
  end
69
172
 
70
173
  def visit_StatementsNode(node)
71
- env = ENV.to_h
72
- Yap::Shell::Execution::Context.fire :before_statements_execute, self unless @suppress_events
174
+ Yap::Shell::Execution::Context.fire :before_statements_execute, @world unless @suppress_events
73
175
  node.head.accept(self)
74
176
  if node.tail
75
177
  node.tail.accept(self)
76
- ENV.clear
77
- ENV.replace(env)
78
178
  end
79
- Yap::Shell::Execution::Context.fire :after_statements_execute, self unless @suppress_events
179
+ Yap::Shell::Execution::Context.fire :after_statements_execute, @world unless @suppress_events
80
180
  end
81
181
 
182
+ # Represents a statement that has scoped environment variables being set,
183
+ # e.g.:
184
+ #
185
+ # yap> A=5 ls
186
+ # yap> A=5 B=6 echo
187
+ #
188
+ # These environment variables are reset after the their statement, e.g.:
189
+ #
190
+ # yap> A=5
191
+ # yap> echo $A
192
+ # 5
193
+ # yap> A=b echo $A
194
+ # b
195
+ # yap> echo $
196
+ # 5
197
+ #
82
198
  def visit_EnvWrapperNode(node)
83
- env = ENV.to_h
84
- node.env.each_pair do |k,v|
85
- ENV[k] = v
199
+ with_env do
200
+ node.env.each_pair { |env_var_name,value| world.env[env_var_name] = variable_expand(value) }
201
+ node.node.accept(self)
86
202
  end
87
- node.node.accept(self)
88
- ENV.clear
89
- ENV.replace(env)
90
203
  end
91
204
 
205
+ # Represents a statement that contains nothing but environment
206
+ # variables being set, e.g.:
207
+ #
208
+ # yap> A=5
209
+ # yap> A=5 B=6
210
+ # yap> A=3 && B=6
211
+ #
212
+ # The environment variables persist from statement to statement until
213
+ # they cleared or overridden.
214
+ #
92
215
  def visit_EnvNode(node)
93
216
  node.env.each_pair do |key,val|
94
- ENV[key] = val
217
+ world.env[key] = variable_expand(val)
95
218
  end
219
+ Yap::Shell::Execution::Result.new(status_code:0, directory:Dir.pwd, n:1, of:1)
96
220
  end
97
221
 
98
222
  def visit_ConditionalNode(node)
99
223
  case node.operator
100
224
  when '&&'
101
225
  node.expr1.accept self
102
- if @last_result.status_code == 0
226
+ if @world.last_result.status_code == 0
103
227
  node.expr2.accept self
104
228
  end
105
229
  when '||'
106
230
  node.expr1.accept self
107
- if @last_result.status_code != 0
231
+ if @world.last_result.status_code != 0
108
232
  node.expr2.accept self
109
233
  end
110
234
  else
@@ -116,7 +240,7 @@ module Yap::Shell
116
240
  with_standard_streams do |stdin, stdout, stderr|
117
241
  # Modify @stdout and @stderr for the first command
118
242
  stdin, @stdout = IO.pipe
119
-
243
+ pipeline_stack.push true
120
244
  # Don't modify @stdin for the first command in the pipeline.
121
245
  node.head.accept(self)
122
246
 
@@ -127,6 +251,7 @@ module Yap::Shell
127
251
  # Modify @stdout,@stderr to go back to the original
128
252
  @stdout, @stderr = stdout, stderr
129
253
 
254
+ pipeline_stack.pop
130
255
  node.tail.accept(self)
131
256
 
132
257
  # Set our @stdin back to the original
@@ -136,11 +261,13 @@ module Yap::Shell
136
261
 
137
262
  def visit_InternalEvalNode(node)
138
263
  command = CommandFactory.build_command_for(
264
+ world: world,
139
265
  command: node.command,
140
266
  args: node.args,
141
267
  heredoc: node.heredoc,
142
- internally_evaluate: node.internally_evaluate?)
143
- @last_result = @blk.call command, @stdin, @stdout, @stderr
268
+ internally_evaluate: node.internally_evaluate?,
269
+ line: @input)
270
+ set_last_result @blk.call command, @stdin, @stdout, @stderr, pipeline_stack.empty?
144
271
  end
145
272
 
146
273
  ######################################################################
@@ -149,45 +276,26 @@ module Yap::Shell
149
276
  # #
150
277
  ######################################################################
151
278
 
152
- def alias_expand(input, aliases:Aliases.instance)
153
- head, *tail = input.split(/\s/, 2).first
154
- if new_head=aliases.fetch_alias(head)
155
- [new_head].concat(tail).join(" ")
156
- else
157
- input
158
- end
159
- end
160
279
 
161
- def shell_expand(input)
162
- [input].flatten.inject([]) do |results,str|
163
- # Basic bash-style brace expansion
164
- expansions = str.scan(/\{([^\}]+)\}/).flatten.first
165
- if expansions
166
- expansions.split(",").each do |expansion|
167
- results << str.sub(/\{([^\}]+)\}/, expansion)
168
- end
169
- else
170
- results << str
171
- end
280
+ # +pipeline_stack+ is used to determine if we are about go inside of a
281
+ # pipeline. It will be empty when we are coming out of a pipeline node.
282
+ def pipeline_stack
283
+ @pipeline_stack ||= []
284
+ end
172
285
 
173
- results = results.map! do |s|
174
- # Basic bash-style tilde expansion
175
- s.gsub!(/\A~(.*)/, ENV["HOME"] + '\1')
286
+ def alias_expand(input)
287
+ ShellExpansions.new(world: world).expand_aliases_in(input)
288
+ end
176
289
 
177
- # Basic bash-style variable expansion
178
- if s =~ /^\$(.*)/
179
- s = ENV.fetch($1, "")
180
- end
290
+ def variable_expand(input)
291
+ ShellExpansions.new(world: world).expand_variables_in(input)
292
+ end
181
293
 
182
- # Basic bash-style path-name expansion
183
- expansions = Dir[s]
184
- if expansions.any?
185
- expansions
186
- else
187
- s
188
- end
189
- end.flatten
190
- end.flatten
294
+ def shell_expand(input, escape_directory_expansions: true)
295
+ ShellExpansions.new(world: world).expand_words_in(
296
+ input,
297
+ escape_directory_expansions: escape_directory_expansions
298
+ )
191
299
  end
192
300
 
193
301
  def with_standard_streams(&blk)
@@ -215,5 +323,14 @@ module Yap::Shell
215
323
  [stdin, stdout, stderr]
216
324
  end
217
325
 
326
+ def with_env(&blk)
327
+ env = world.env.dup
328
+ begin
329
+ yield if block_given?
330
+ ensure
331
+ world.env.clear
332
+ world.env.replace(env)
333
+ end
334
+ end
218
335
  end
219
336
  end