yap-shell 0.0.2

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