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.
- 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
|