yap-shell 0.4.0 → 0.4.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +2 -0
- data/addons/history/history.rb +2 -0
- data/addons/keyboard_macros/keyboard_macros.rb +60 -2
- data/addons/tab_completion/tab_completion.rb +13 -0
- data/bin/yap-dev +0 -4
- data/lib/tasks/gem.rake +1 -1
- data/lib/yap.rb +3 -0
- data/lib/yap/shell.rb +12 -0
- data/lib/yap/shell/aliases.rb +18 -4
- data/lib/yap/shell/builtins/alias.rb +1 -1
- data/lib/yap/shell/commands.rb +12 -0
- data/lib/yap/shell/evaluation.rb +77 -10
- data/lib/yap/shell/evaluation/shell_expansions.rb +22 -8
- data/lib/yap/shell/execution/builtin_command_execution.rb +5 -1
- data/lib/yap/shell/execution/context.rb +8 -0
- data/lib/yap/shell/execution/file_system_command_execution.rb +20 -7
- data/lib/yap/shell/execution/ruby_command_execution.rb +60 -65
- data/lib/yap/shell/execution/shell_command_execution.rb +3 -0
- data/lib/yap/shell/repl.rb +14 -5
- data/lib/yap/shell/version.rb +1 -1
- data/lib/yap/world.rb +4 -0
- data/lib/yap/world/addons.rb +56 -13
- data/rcfiles/.yaprc +7 -1
- data/yap-shell.gemspec +2 -1
- metadata +18 -4
@@ -9,24 +9,30 @@ module Yap::Shell
|
|
9
9
|
end
|
10
10
|
|
11
11
|
def expand_aliases_in(input)
|
12
|
+
Treefell['shell'].puts "shell-expansions expand aliases in: #{input.inspect}"
|
12
13
|
head, *tail = input.split(/\s/, 2).first
|
13
|
-
if
|
14
|
+
expanded = if aliases.has_key?(head)
|
15
|
+
new_head=aliases.fetch_alias(head)
|
14
16
|
[new_head].concat(tail).join(" ")
|
15
17
|
else
|
16
18
|
input
|
17
19
|
end
|
20
|
+
expanded
|
18
21
|
end
|
19
22
|
|
20
23
|
def expand_words_in(input, escape_directory_expansions: true)
|
21
|
-
[
|
24
|
+
Treefell['shell'].puts "shell-expansions expand words in: #{input.inspect}"
|
25
|
+
expanded = [input].flatten.inject([]) do |results,str|
|
22
26
|
results << process_expansions(
|
23
27
|
word_expand(str),
|
24
28
|
escape_directory_expansions: escape_directory_expansions
|
25
29
|
)
|
26
30
|
end.flatten
|
31
|
+
expanded
|
27
32
|
end
|
28
33
|
|
29
34
|
def expand_variables_in(input)
|
35
|
+
Treefell['shell'].puts "shell-expansions expand variables in: #{input.inspect}"
|
30
36
|
env_expand(input)
|
31
37
|
end
|
32
38
|
|
@@ -35,11 +41,16 @@ module Yap::Shell
|
|
35
41
|
def env_expand(str)
|
36
42
|
str.gsub(/\$(\S+)/) do |match,*args|
|
37
43
|
var_name = match[1..-1]
|
38
|
-
|
39
|
-
|
40
|
-
|
44
|
+
if var_name == '?'
|
45
|
+
(world.last_result ? world.last_result.status_code.to_s : '0').tap do |expanded|
|
46
|
+
Treefell['shell'].puts "shell-expansions expanding env var #{match} to #{expanded}"
|
47
|
+
end
|
48
|
+
elsif world.env.has_key?(var_name)
|
49
|
+
world.env.fetch(var_name).tap do |expanded|
|
50
|
+
Treefell['shell'].puts "shell-expansions expanding env var #{match} to #{expanded}"
|
51
|
+
end
|
41
52
|
else
|
42
|
-
|
53
|
+
match
|
43
54
|
end
|
44
55
|
end
|
45
56
|
end
|
@@ -53,10 +64,13 @@ module Yap::Shell
|
|
53
64
|
# at least one comma listed. E.g. "a_{1,2}" => "a_1 a_2" whereas
|
54
65
|
# "a_{1}" => "a_{1}"
|
55
66
|
if expansions.length > 1
|
56
|
-
|
67
|
+
expanded = expansions.map { |expansion| str.sub(/\{([^\}]+)\}/, expansion) }.tap do |expanded|
|
68
|
+
Treefell['shell'].puts "shell-expansions expanding words in #{str} to #{expanded}"
|
69
|
+
end
|
70
|
+
return expanded
|
57
71
|
end
|
58
72
|
end
|
59
|
-
|
73
|
+
[str]
|
60
74
|
end
|
61
75
|
|
62
76
|
def process_expansions(expansions, escape_directory_expansions: true)
|
@@ -3,13 +3,17 @@ module Yap::Shell::Execution
|
|
3
3
|
|
4
4
|
class BuiltinCommandExecution < CommandExecution
|
5
5
|
on_execute do |command:, n:, of:, wait:|
|
6
|
+
Treefell['shell'].puts "builtin command executing: #{command}"
|
6
7
|
status_code = command.execute(stdin:@stdin, stdout:@stdout, stderr:@stderr)
|
7
8
|
if status_code == :resume
|
9
|
+
Treefell['shell'].puts "builtin command execution resumed: #{command}"
|
8
10
|
ResumeExecution.new(status_code:0, directory:Dir.pwd, n:n, of:of)
|
9
11
|
else
|
10
12
|
@stdout.close if @stdout != $stdout && !@stdout.closed?
|
11
13
|
@stderr.close if @stderr != $stderr && !@stderr.closed?
|
12
|
-
Result.new(status_code:status_code, directory:Dir.pwd, n:n, of:of)
|
14
|
+
Result.new(status_code:status_code, directory:Dir.pwd, n:n, of:of).tap do |result|
|
15
|
+
Treefell['shell'].puts "builtin command execution done with result=#{result.inspect}"
|
16
|
+
end
|
13
17
|
end
|
14
18
|
end
|
15
19
|
end
|
@@ -60,12 +60,15 @@ module Yap::Shell::Execution
|
|
60
60
|
)
|
61
61
|
|
62
62
|
@saved_tty_attrs = Termios.tcgetattr(STDIN)
|
63
|
+
Treefell['shell'].puts "firing :before_execute for #{command}"
|
63
64
|
self.class.fire :before_execute, world, command: command
|
64
65
|
|
65
66
|
begin
|
66
67
|
result = execution_context.execute(command:command, n:i, of:of, wait:wait)
|
67
68
|
rescue Exception => ex
|
68
69
|
raise(ex) if ex.is_a?(SystemExit)
|
70
|
+
|
71
|
+
Treefell['shell'].puts "rescued unexpected error=#{ex} with message=#{ex.message.inspect}"
|
69
72
|
puts <<-ERROR.gsub(/^\s*\|/, '')
|
70
73
|
|******************************
|
71
74
|
|\e[31mWhoops! An unexpected error has occurred\e[0m
|
@@ -83,6 +86,7 @@ module Yap::Shell::Execution
|
|
83
86
|
ERROR
|
84
87
|
end
|
85
88
|
|
89
|
+
Treefell['shell'].puts "firing :after_execute for #{command}"
|
86
90
|
self.class.fire :after_execute, world, command: command, result: result
|
87
91
|
|
88
92
|
results << process_execution_result(execution_context:execution_context, result:result)
|
@@ -100,15 +104,19 @@ module Yap::Shell::Execution
|
|
100
104
|
def process_execution_result(execution_context:, result:)
|
101
105
|
case result
|
102
106
|
when SuspendExecution
|
107
|
+
Treefell['shell'].puts "suspending execution context"
|
103
108
|
@suspended_execution_contexts.push execution_context
|
104
109
|
return result
|
105
110
|
|
106
111
|
when ResumeExecution
|
112
|
+
Treefell['shell'].puts "resuming suspended execution context"
|
107
113
|
execution_context = @suspended_execution_contexts.pop
|
108
114
|
if execution_context
|
109
115
|
nresult = execution_context.resume
|
116
|
+
Treefell['shell'].puts "resuming suspended execution context success"
|
110
117
|
return process_execution_result execution_context: execution_context, result: nresult
|
111
118
|
else
|
119
|
+
Treefell['shell'].puts "error: cannot resume execution when there is nothing suspended"
|
112
120
|
@stderr.puts "fg: No such job"
|
113
121
|
end
|
114
122
|
else
|
@@ -49,11 +49,13 @@ module Yap::Shell::Execution
|
|
49
49
|
ENV.replace(before)
|
50
50
|
end
|
51
51
|
end
|
52
|
+
Treefell['shell'].puts "forked child process pid=#{pid} to execute #{command}"
|
52
53
|
|
53
54
|
# Put the child process into a process group of its own
|
54
55
|
Process.setpgid pid, pid
|
55
56
|
|
56
57
|
if command.heredoc
|
58
|
+
Treefell['shell'].puts "command has heredoc, wriing to stdin"
|
57
59
|
w.write command.heredoc
|
58
60
|
w.close
|
59
61
|
end
|
@@ -67,9 +69,11 @@ module Yap::Shell::Execution
|
|
67
69
|
# is so the next command in the pipeline can complete and don't hang waiting for
|
68
70
|
# stdin after the command that's writing to its stdin has completed.
|
69
71
|
if stdout != $stdout && stdout.is_a?(IO) && !stdout.closed? then
|
72
|
+
Treefell['shell'].puts "closing stdout for child process with pid=#{pid}"
|
70
73
|
stdout.close
|
71
74
|
end
|
72
75
|
if stderr != $stderr && stderr.is_a?(IO) && !stderr.closed? then
|
76
|
+
Treefell['shell'].puts "closing stderr for child process with pid=#{pid}"
|
73
77
|
stderr.close
|
74
78
|
end
|
75
79
|
# if stdin != $stdin && !stdin.closed? then stdin.close end
|
@@ -79,17 +83,24 @@ module Yap::Shell::Execution
|
|
79
83
|
# give it back to the us so we can become the foreground process
|
80
84
|
# in the terminal
|
81
85
|
if pid == Termios.tcgetpgrp(STDIN)
|
86
|
+
Treefell['shell'].puts <<-DEBUG.gsub(/^\s*\|/, '')
|
87
|
+
|restoring process group for STDIN to yap process with pid=#{Process.pid}
|
88
|
+
DEBUG
|
82
89
|
Process.setpgid Process.pid, Process.pid
|
83
90
|
Termios.tcsetpgrp STDIN, Process.pid
|
84
91
|
end
|
85
92
|
|
86
93
|
# if the reason we stopped is from being suspended
|
87
|
-
|
88
|
-
|
94
|
+
sigtstp = Signal.list["TSTP"]
|
95
|
+
if status && status.stopsig == sigtstp
|
96
|
+
Treefell['shell'].puts "process pid=#{pid} suspended by signal=#{status.stopsig.inspect}"
|
97
|
+
Treefell['shell'].puts "$?: #{$?.inspect}"
|
89
98
|
suspended(command:command, n:n, of:of, pid: pid)
|
90
99
|
result = Yap::Shell::Execution::SuspendExecution.new(status_code:nil, directory:Dir.pwd, n:n, of:of)
|
91
100
|
else
|
92
|
-
|
101
|
+
caused_by_signal = status ? (status.termsig || status.stopsig) : nil
|
102
|
+
Treefell['shell'].puts "process pid=#{pid} stopped by signal=#{caused_by_signal.inspect}"
|
103
|
+
Treefell['shell'].puts "$?: #{$?.inspect}"
|
93
104
|
# if a signal killed or stopped the process (such as SIGINT or SIGTSTP) $? is nil.
|
94
105
|
exitstatus = $? ? $?.exitstatus : nil
|
95
106
|
result = Yap::Shell::Execution::Result.new(status_code:exitstatus, directory:Dir.pwd, n:n, of:of)
|
@@ -99,18 +110,20 @@ module Yap::Shell::Execution
|
|
99
110
|
def resume
|
100
111
|
args = @suspended
|
101
112
|
@suspended = nil
|
113
|
+
pid = args[:pid]
|
114
|
+
sigcont = Signal.list["CONT"]
|
102
115
|
|
103
|
-
puts "
|
116
|
+
Treefell['shell'].puts "resuming suspended process pid=#{pid} by sending it signal=#{sigcont}"
|
104
117
|
resume_blk = lambda do
|
105
|
-
Process.kill
|
106
|
-
|
118
|
+
Process.kill sigcont, pid
|
119
|
+
pid
|
107
120
|
end
|
108
121
|
|
109
122
|
self.instance_exec command:args[:command], n:args[:n], of:args[:of], resume_blk:resume_blk, wait:true, &self.class.on_execute
|
110
123
|
end
|
111
124
|
|
112
125
|
def suspended(command:, n:, of:, pid:)
|
113
|
-
puts "
|
126
|
+
Treefell['shell'].puts "process pid=#{pid} suspended"
|
114
127
|
@suspended = {
|
115
128
|
command: command,
|
116
129
|
n: n,
|
@@ -3,83 +3,78 @@ module Yap::Shell::Execution
|
|
3
3
|
on_execute do |command:, n:, of:, wait:|
|
4
4
|
result = nil
|
5
5
|
stdin, stdout, stderr, world = @stdin, @stdout, @stderr, @world
|
6
|
-
# t = Thread.new {
|
7
|
-
exit_code = 0
|
8
|
-
first_command = n == 1
|
9
6
|
|
10
|
-
|
11
|
-
|
12
|
-
begin
|
13
|
-
ruby_command = command.to_executable_str
|
7
|
+
exit_code = 0
|
8
|
+
first_command = n == 1
|
14
9
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
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
|
10
|
+
f = nil
|
11
|
+
ruby_result = nil
|
12
|
+
begin
|
13
|
+
ruby_command = command.to_executable_str
|
25
14
|
|
26
|
-
|
27
|
-
|
15
|
+
Treefell['shell'].puts "ruby execution: reading stdin from #{stdin.inspect}"
|
16
|
+
contents = if stdin.is_a?(String)
|
17
|
+
f = File.open stdin
|
18
|
+
f.read
|
19
|
+
elsif stdin != $stdin
|
20
|
+
stdin.read
|
21
|
+
end
|
22
|
+
|
23
|
+
Treefell['shell'].puts "ruby execution: contents=#{contents.inspect}, setting to world.content"
|
24
|
+
world.contents = contents
|
28
25
|
|
29
|
-
|
30
|
-
|
26
|
+
method = ruby_command.scan(/^(\w+(?:[!?]|\s*=)?)/).flatten.first.gsub(/\s/, '')
|
27
|
+
Treefell['shell'].puts "ruby execution: method=#{method.inspect}"
|
31
28
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
29
|
+
obj = if first_command
|
30
|
+
world
|
31
|
+
elsif contents.respond_to?(method)
|
32
|
+
contents
|
33
|
+
else
|
34
|
+
world
|
35
|
+
end
|
39
36
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
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?
|
37
|
+
if ruby_command =~ /^[A-Z0-9]|::/
|
38
|
+
Treefell['shell'].puts "ruby executing: eval(#{ruby_command.inspect})"
|
39
|
+
ruby_result = eval ruby_command
|
40
|
+
else
|
41
|
+
ruby_command = "self.#{ruby_command}"
|
42
|
+
Treefell['shell'].puts "ruby executing: #{obj.class.name} instance instance_eval(#{ruby_command.inspect})"
|
43
|
+
ruby_result = obj.instance_eval ruby_command
|
57
44
|
end
|
45
|
+
rescue Exception => ex
|
46
|
+
ruby_result = <<-EOT.gsub(/^\s*\S/, '')
|
47
|
+
|Failed processing ruby: #{ruby_command}
|
48
|
+
|#{ex}
|
49
|
+
|#{ex.backtrace.join("\n")}
|
50
|
+
EOT
|
51
|
+
exit_code = 1
|
52
|
+
ensure
|
53
|
+
f.close if f && !f.closed?
|
54
|
+
end
|
58
55
|
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
56
|
+
# The next line causes issues sometimes?
|
57
|
+
# puts "WRITING #{ruby_result.length} bytes" if ENV["DEBUG"]
|
58
|
+
ruby_result = ruby_result.to_s
|
59
|
+
ruby_result << "\n" unless ruby_result.end_with?("\n")
|
63
60
|
|
64
|
-
|
65
|
-
|
66
|
-
|
61
|
+
stdout.write ruby_result
|
62
|
+
stdout.flush
|
63
|
+
stderr.flush
|
67
64
|
|
68
|
-
|
69
|
-
|
65
|
+
stdout.close if stdout != $stdout && !stdout.closed?
|
66
|
+
stderr.close if stderr != $stderr && !stderr.closed?
|
70
67
|
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
68
|
+
# Pass current execution to give any other threads a chance
|
69
|
+
# to be scheduled before we send back our status code. This could
|
70
|
+
# probably use a more elaborate signal or message passing scheme,
|
71
|
+
# but that's for another day.
|
72
|
+
# Thread.pass
|
76
73
|
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
# t.join
|
82
|
-
result
|
74
|
+
# Make up an exit code
|
75
|
+
Result.new(status_code:exit_code, directory:Dir.pwd, n:n, of:of).tap do |result|
|
76
|
+
Treefell['shell'].puts "ruby execution done with result=#{result.inspect}"
|
77
|
+
end
|
83
78
|
end
|
84
79
|
end
|
85
80
|
end
|
@@ -1,6 +1,8 @@
|
|
1
1
|
module Yap::Shell::Execution
|
2
2
|
class ShellCommandExecution < CommandExecution
|
3
3
|
on_execute do |command:, n:, of:, wait:|
|
4
|
+
Treefell['shell'].puts "shell command execution: #{command}"
|
5
|
+
|
4
6
|
possible_parameters = {
|
5
7
|
command: command.str,
|
6
8
|
args: command.args,
|
@@ -17,6 +19,7 @@ module Yap::Shell::Execution
|
|
17
19
|
h
|
18
20
|
end
|
19
21
|
|
22
|
+
Treefell['shell'].puts "shell command executing with params: #{params.inspect}"
|
20
23
|
command_result = func.call(**params)
|
21
24
|
@stdout.close if @stdout != $stdout && !@stdout.closed?
|
22
25
|
@stderr.close if @stderr != $stderr && !@stderr.closed?
|
data/lib/yap/shell/repl.rb
CHANGED
@@ -12,7 +12,11 @@ module Yap::Shell
|
|
12
12
|
def initialize(world:nil)
|
13
13
|
@world = world
|
14
14
|
@editor= world.editor
|
15
|
+
|
16
|
+
Treefell['shell'].puts "installing default keybindings"
|
15
17
|
install_default_keybindings
|
18
|
+
|
19
|
+
Treefell['shell'].puts "installing default tab completion"
|
16
20
|
install_default_tab_completion_proc
|
17
21
|
end
|
18
22
|
|
@@ -20,17 +24,23 @@ module Yap::Shell
|
|
20
24
|
@blk = blk
|
21
25
|
|
22
26
|
@world.editor.on_read_line do |event|
|
27
|
+
line_read = event[:payload][:line]
|
28
|
+
Treefell['shell'].puts "editor line read: #{line_read.inspect}"
|
23
29
|
# editor.history = true?
|
24
|
-
line =
|
30
|
+
line = line_read << "\n"
|
25
31
|
begin
|
26
32
|
@blk.call(line)
|
27
33
|
@world.editor.redraw_prompt
|
28
|
-
rescue Yap::Shell::Parser::Lexer::NonterminatedString,
|
34
|
+
rescue Yap::Shell::Parser::Lexer::NonterminatedString,
|
35
|
+
Yap::Shell::Parser::Lexer::LineContinuationFound => ex
|
36
|
+
Treefell['shell'].puts "rescued #{ex}, asking user for more input"
|
29
37
|
line << read_another_line_of_input
|
30
38
|
retry
|
31
39
|
rescue ::Yap::Shell::CommandUnknownError => ex
|
40
|
+
Treefell['shell'].puts "rescued #{ex}, telling user"
|
32
41
|
puts " CommandError: #{ex.message}"
|
33
42
|
rescue ::Yap::Shell::Parser::ParseError => ex
|
43
|
+
Treefell['shell'].puts "rescued #{ex}, telling user"
|
34
44
|
puts " Parse error: #{ex.message}"
|
35
45
|
ensure
|
36
46
|
@world.editor.reset_line
|
@@ -213,13 +223,12 @@ module Yap::Shell
|
|
213
223
|
return _input
|
214
224
|
end
|
215
225
|
|
216
|
-
puts "
|
217
|
-
|
226
|
+
Treefell['shell'].puts "asking for heredoc input with @world.secondary_prompt"
|
218
227
|
loop do
|
219
228
|
str = editor.read(@world.secondary_prompt.update.text, false)
|
220
229
|
input << "#{str}\n"
|
221
230
|
if str =~ /^#{Regexp.escape(marker)}$/
|
222
|
-
puts "
|
231
|
+
Treefell['shell'].puts "done asking for heredoc input"
|
223
232
|
break
|
224
233
|
end
|
225
234
|
end
|
data/lib/yap/shell/version.rb
CHANGED
data/lib/yap/world.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'term/ansicolor'
|
2
|
+
require 'fileutils'
|
2
3
|
require 'forwardable'
|
3
4
|
require 'rawline'
|
4
5
|
require 'termios'
|
@@ -30,6 +31,9 @@ module Yap
|
|
30
31
|
@env = ENV.to_h.dup
|
31
32
|
dom = build_editor_dom
|
32
33
|
|
34
|
+
# ensure yap directory exists
|
35
|
+
FileUtils.mkdir_p configuration.yap_path
|
36
|
+
|
33
37
|
@editor = RawLine::Editor.create(dom: dom)
|
34
38
|
|
35
39
|
self.prompt = Yap::Shell::Prompt.new(text: DEFAULTS[:primary_prompt_text])
|