yap-shell 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +22 -0
- data/DESIGN.md +87 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +29 -0
- data/Rakefile +3 -0
- data/WISHLIST.md +40 -0
- data/bin/yap +24 -0
- data/lib/tasks/gem.rake +60 -0
- data/lib/yap.rb +30 -0
- data/lib/yap/shell.rb +73 -0
- data/lib/yap/shell/aliases.rb +40 -0
- data/lib/yap/shell/builtins.rb +18 -0
- data/lib/yap/shell/builtins/alias.rb +25 -0
- data/lib/yap/shell/builtins/cd.rb +37 -0
- data/lib/yap/shell/commands.rb +127 -0
- data/lib/yap/shell/evaluation.rb +198 -0
- data/lib/yap/shell/execution.rb +18 -0
- data/lib/yap/shell/execution/builtin_command_execution.rb +15 -0
- data/lib/yap/shell/execution/command_execution.rb +30 -0
- data/lib/yap/shell/execution/context.rb +88 -0
- data/lib/yap/shell/execution/file_system_command_execution.rb +102 -0
- data/lib/yap/shell/execution/result.rb +18 -0
- data/lib/yap/shell/execution/ruby_command_execution.rb +85 -0
- data/lib/yap/shell/execution/shell_command_execution.rb +10 -0
- data/lib/yap/shell/repl.rb +63 -0
- data/lib/yap/shell/version.rb +5 -0
- data/lib/yap/world.rb +43 -0
- data/rcfiles/.yaprc +183 -0
- data/yap-shell.gemspec +27 -0
- metadata +145 -0
@@ -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
|
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
|