yap-shell 0.1.1 → 0.3.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.
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