yap-shell 0.0.2

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.
@@ -0,0 +1,30 @@
1
+ module Yap::Shell::Execution
2
+ class CommandExecution
3
+ attr_reader :stdin, :stdout, :stderr, :world
4
+
5
+ def self.on_execute(&blk)
6
+ if block_given?
7
+ @on_execute_blk = blk
8
+ else
9
+ @on_execute_blk
10
+ end
11
+ end
12
+
13
+ def initialize(stdin:, stdout:, stderr:,world:)
14
+ @stdin, @stdout, @stderr = stdin, stdout, stderr
15
+ @world = world
16
+ end
17
+
18
+ def execute(command:, n:, of:)
19
+ if self.class.on_execute
20
+ self.instance_exec(command:command, n:n, of:of, &self.class.on_execute)
21
+ else
22
+ raise NotImplementedError, "on_execute block hasn't been implemented!"
23
+ end
24
+ end
25
+
26
+ def suspended?
27
+ @suspended
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,88 @@
1
+ module Yap::Shell::Execution
2
+ class Context
3
+ def self.on(event=nil, &blk)
4
+ @on_callbacks ||= Hash.new{ |h,k| h[k] = [] }
5
+ if event
6
+ @on_callbacks[event.to_sym].push blk
7
+ end
8
+ @on_callbacks
9
+ end
10
+
11
+ def self.fire(event, context, *args)
12
+ on[event.to_sym].each do |block|
13
+ block.call(context, *args)
14
+ end
15
+ end
16
+
17
+ def self.register(context, command_type:)
18
+ raise "context cannot be nil" if context.nil?
19
+ @registrations ||= {}
20
+ @registrations[command_type] = context
21
+ true
22
+ end
23
+
24
+ def self.execution_context_for(command)
25
+ @registrations[command.type] || raise("No execution context found for given #{command.type} command: #{command.inspect}")
26
+ end
27
+
28
+ def initialize(stdin:, stdout:, stderr:)
29
+ @stdin, @stdout, @stderr = stdin, stdout, stderr
30
+ @command_queue = []
31
+ @suspended_execution_contexts = []
32
+ end
33
+
34
+ def add_command_to_run(command, stdin:, stdout:, stderr:)
35
+ @command_queue << [command, stdin, stdout, stderr]
36
+ end
37
+
38
+ def clear_commands
39
+ @command_queue.clear
40
+ end
41
+
42
+ def execute(world:)
43
+ results = []
44
+ @command_queue.each_with_index do |(command, stdin, stdout, stderr), reversed_i|
45
+ of = @command_queue.length
46
+ i = of - reversed_i
47
+ stdin = @stdin if stdin == :stdin
48
+ stdout = @stdout if stdout == :stdout
49
+ stderr = @stderr if stderr == :stderr
50
+
51
+ execution_context_factory = self.class.execution_context_for(command)
52
+ if execution_context_factory
53
+ execution_context = execution_context_factory.new(
54
+ stdin: stdin,
55
+ stdout: stdout,
56
+ stderr: stderr,
57
+ world: world
58
+ )
59
+
60
+ self.class.fire :before_execute, execution_context, command: command
61
+ result = execution_context.execute(command:command, n:i, of:of)
62
+ self.class.fire :after_execute, execution_context, command: command, result: result
63
+
64
+ case result
65
+ when SuspendExecution
66
+ # Ensure echo is turned back on. Some suspended programs
67
+ # may have turned it off.
68
+ `stty echo`
69
+ @suspended_execution_contexts.push execution_context
70
+ when ResumeExecution
71
+ execution_context = @suspended_execution_contexts.pop
72
+ if execution_context
73
+ execution_context.resume
74
+ else
75
+ stderr.puts "fg: No such job"
76
+ end
77
+ end
78
+
79
+ results << result
80
+ end
81
+ end
82
+
83
+ clear_commands
84
+
85
+ results.last
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,102 @@
1
+ module Yap::Shell::Execution
2
+ class FileSystemCommandExecution < CommandExecution
3
+ on_execute do |command:, n:, of:, resume_blk:nil|
4
+ stdin, stdout, stderr, world = @stdin, @stdout, @stderr, @world
5
+ result = nil
6
+ begin
7
+ if resume_blk
8
+ pid = resume_blk.call
9
+ else
10
+ r,w = nil, nil
11
+ if command.heredoc
12
+ r,w = IO.pipe
13
+ stdin = r
14
+ end
15
+
16
+ pid = fork do
17
+ # Start a new process gruop as the session leader. Now we are
18
+ # responsible for sending signals that would have otherwise
19
+ # been propagated to the process, e.g. SIGINT, SIGSTOP, SIGCONT, etc.
20
+ stdin = File.open(stdin, "rb") if stdin.is_a?(String)
21
+ stdout = File.open(stdout, "wb") if stdout.is_a?(String)
22
+ stderr = File.open(stderr, "wb") if stderr.is_a?(String)
23
+
24
+ stdout = stderr if stdout == :stderr
25
+ stderr = stdout if stderr == :stdout
26
+
27
+ $stdin.reopen stdin
28
+ $stdout.reopen stdout
29
+ $stderr.reopen stderr
30
+ Process.setsid
31
+
32
+ Kernel.exec command.to_executable_str
33
+ end
34
+ if command.heredoc
35
+ w.write command.heredoc
36
+ w.close
37
+ end
38
+ end
39
+
40
+ # This prevents the shell from processing sigint and lets the child
41
+ # process handle it. Necessary for interactive shells that do not
42
+ # abort on Ctrl-C such as irb.
43
+ Signal.trap("SIGINT") do
44
+ Process.kill("SIGINT", pid)
45
+ end
46
+
47
+ Process.waitpid(pid) unless of > 1
48
+ Signal.trap("SIGINT", "DEFAULT")
49
+
50
+ # If we're not printing to the terminal than close in/out/err. This
51
+ # is so the next command in the pipeline can complete and don't hang waiting for
52
+ # stdin after the command that's writing to its stdin has completed.
53
+ if stdout != $stdout && stdout.is_a?(IO) && !stdout.closed? then
54
+ stdout.close
55
+ end
56
+ if stderr != $stderr && stderr.is_a?(IO) && !stderr.closed? then
57
+ stderr.close
58
+ end
59
+ # if stdin != $stdin && !stdin.closed? then stdin.close end
60
+
61
+ rescue Interrupt
62
+ Process.kill "SIGINT", pid
63
+
64
+ rescue SuspendSignalError => ex
65
+ Process.kill "SIGSTOP", pid
66
+
67
+ # The Process started above with the PID +pid+ is a child process
68
+ # so it has also received the suspend/SIGTSTP signal.
69
+ suspended(command:command, n:n, of:of, pid: pid)
70
+
71
+ result = SuspendExecution.new(status_code:nil, directory:Dir.pwd, n:n, of:of)
72
+ end
73
+
74
+ # if a signal killed or stopped the process (such as SIGINT or SIGTSTP) $? is nil.
75
+ exitstatus = $? ? $?.exitstatus : nil
76
+ result || Result.new(status_code:exitstatus, directory:Dir.pwd, n:n, of:of)
77
+ end
78
+
79
+ def resume
80
+ args = @suspended
81
+ @suspended = nil
82
+
83
+ puts "Resuming: #{args[:pid]}" if ENV["DEBUG"]
84
+ resume_blk = lambda do
85
+ Process.kill "SIGCONT", args[:pid]
86
+ args[:pid]
87
+ end
88
+
89
+ self.instance_exec command:args[:command], n:args[:n], of:args[:of], resume_blk:resume_blk, &self.class.on_execute
90
+ end
91
+
92
+ def suspended(command:, n:, of:, pid:)
93
+ puts "Suspending: #{pid}" if ENV["DEBUG"]
94
+ @suspended = {
95
+ command: command,
96
+ n: n,
97
+ of: of,
98
+ pid: pid
99
+ }
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,18 @@
1
+ module Yap::Shell::Execution
2
+ class Result
3
+ attr_reader :status_code, :directory, :n, :of
4
+
5
+ def initialize(status_code:, directory:, n:, of:)
6
+ @status_code = status_code
7
+ @directory = directory
8
+ @n = n
9
+ @of = of
10
+ end
11
+ end
12
+
13
+ class SuspendExecution < Result
14
+ end
15
+
16
+ class ResumeExecution < Result
17
+ end
18
+ end
@@ -0,0 +1,85 @@
1
+ module Yap::Shell::Execution
2
+ class RubyCommandExecution < CommandExecution
3
+ on_execute do |command:, n:, of:|
4
+ result = nil
5
+ stdin, stdout, stderr, world = @stdin, @stdout, @stderr, @world
6
+ t = Thread.new {
7
+ exit_code = 0
8
+ first_command = n == 1
9
+
10
+ f = nil
11
+ ruby_result = nil
12
+ begin
13
+ ruby_command = command.to_executable_str
14
+
15
+ contents = if stdin.is_a?(String)
16
+ puts "READ: stdin as a String: #{stdin.inspect}" if ENV["DEBUG"]
17
+ f = File.open stdin
18
+ f.read
19
+ elsif stdin != $stdin
20
+ puts "READ: stdin is not $stdin: #{stdin.inspect}" if ENV["DEBUG"]
21
+ stdin.read
22
+ else
23
+ puts "READ: contents is: #{contents.inspect}" if ENV["DEBUG"]
24
+ end
25
+
26
+ puts "READ: #{contents.length} bytes from #{stdin}" if ENV["DEBUG"] && contents
27
+ world.contents = contents
28
+
29
+ method = ruby_command.scan(/^(\w+(?:[!?]|\s*=)?)/).flatten.first.gsub(/\s/, '')
30
+ puts "method: #{method}" if ENV["DEBUG"]
31
+
32
+ obj = if first_command
33
+ world
34
+ elsif contents.respond_to?(method)
35
+ contents
36
+ else
37
+ world
38
+ end
39
+
40
+ if ruby_command =~ /^[A-Z0-9]|::/
41
+ puts "Evaluating #{ruby_command.inspect} globally" if ENV["DEBUG"]
42
+ ruby_result = eval ruby_command
43
+ else
44
+ ruby_command = "self.#{ruby_command}"
45
+ puts "Evaluating #{ruby_command.inspect} on #{obj.inspect}" if ENV["DEBUG"]
46
+ ruby_result = obj.instance_eval ruby_command
47
+ end
48
+ rescue Exception => ex
49
+ ruby_result = <<-EOT.gsub(/^\s*\S/, '')
50
+ |Failed processing ruby: #{ruby_command}
51
+ |#{ex}
52
+ |#{ex.backtrace.join("\n")}
53
+ EOT
54
+ exit_code = 1
55
+ ensure
56
+ f.close if f && !f.closed?
57
+ end
58
+
59
+ # The next line causes issues sometimes?
60
+ # puts "WRITING #{ruby_result.length} bytes" if ENV["DEBUG"]
61
+ ruby_result = ruby_result.to_s
62
+ ruby_result << "\n" unless ruby_result.end_with?("\n")
63
+
64
+ stdout.write ruby_result
65
+ stdout.flush
66
+ stderr.flush
67
+
68
+ stdout.close if stdout != $stdout && !stdout.closed?
69
+ stderr.close if stderr != $stderr && !stderr.closed?
70
+
71
+ # Pass current execution to give any other threads a chance
72
+ # to be scheduled before we send back our status code. This could
73
+ # probably use a more elaborate signal or message passing scheme,
74
+ # but that's for another day.
75
+ Thread.pass
76
+
77
+ # Make up an exit code
78
+ result = Result.new(status_code:exit_code, directory:Dir.pwd, n:n, of:of)
79
+ }
80
+ t.abort_on_exception = true
81
+ t.join
82
+ result
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,10 @@
1
+ module Yap::Shell::Execution
2
+ class ShellCommandExecution < CommandExecution
3
+ on_execute do |command:, n:, of:|
4
+ func = command.to_proc
5
+ command_result = func.call(args:command.args, stdin:@stdin, stdout:@stdout, stderr:@stderr)
6
+ @stdout.close if @stdout != $stdout && !@stdout.closed?
7
+ @stderr.close if @stderr != $stderr && !@stderr.closed?
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,63 @@
1
+ require 'readline'
2
+
3
+ module Yap::Shell
4
+ class Repl
5
+ def initialize(world:nil)
6
+ @world = world
7
+ end
8
+
9
+ def loop_on_input(&blk)
10
+ @blk = blk
11
+
12
+ loop do
13
+ heredoc = nil
14
+ prompt = ""
15
+
16
+ begin
17
+ prompt = @world ? @world.prompt : "> "
18
+ input = Readline.readline("#{prompt}", true)
19
+
20
+ next if input == ""
21
+
22
+ input = process_heredoc(input)
23
+
24
+ yield input
25
+ rescue ::Yap::Shell::CommandUnknownError => ex
26
+ puts " CommandError: #{ex.message}"
27
+ rescue Interrupt
28
+ puts "^C"
29
+ next
30
+ rescue SuspendSignalError
31
+ # no-op since if we got here we're on the already at the top-level
32
+ # repl and there's nothing to suspend but ourself and we're not
33
+ # about to do that.
34
+ puts "^Z"
35
+ next
36
+ end
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ def process_heredoc(_input)
43
+ if _input =~ /<<-?([A-z0-9\-]+)\s*$/
44
+ input = _input.dup
45
+ marker = $1
46
+ input << "\n"
47
+ else
48
+ return _input
49
+ end
50
+
51
+ puts "Beginning heredoc" if ENV["DEBUG"]
52
+ loop do
53
+ str = Readline.readline("> ", true)
54
+ input << "#{str}\n"
55
+ if str =~ /^#{Regexp.escape(marker)}$/
56
+ puts "Ending heredoc" if ENV["DEBUG"]
57
+ break
58
+ end
59
+ end
60
+ input
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,5 @@
1
+ module Yap
2
+ module Shell
3
+ VERSION = "0.0.2"
4
+ end
5
+ end
data/lib/yap/world.rb ADDED
@@ -0,0 +1,43 @@
1
+ require 'term/ansicolor'
2
+ require 'forwardable'
3
+
4
+ module Yap
5
+ class World
6
+ include Term::ANSIColor
7
+ extend Forwardable
8
+
9
+ attr_accessor :prompt, :contents, :addons
10
+
11
+ def initialize(options)
12
+ (options || {}).each do |k,v|
13
+ self.send "#{k}=", v
14
+ end
15
+
16
+ addons.each do |addon|
17
+ self.instance_eval addon
18
+ end
19
+ end
20
+
21
+ def func(name, &blk)
22
+ Yap::Shell::ShellCommand.define_shell_function(name, &blk)
23
+ end
24
+
25
+ def readline
26
+ ::Readline
27
+ end
28
+
29
+ def prompt
30
+ if @prompt.respond_to? :call
31
+ @prompt.call
32
+ else
33
+ @prompt
34
+ end
35
+ end
36
+
37
+ (String.instance_methods - Object.instance_methods).each do |m|
38
+ next if [:object_id, :__send__, :initialize].include?(m)
39
+ def_delegator :@contents, m
40
+ end
41
+
42
+ end
43
+ end