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,18 @@
|
|
1
|
+
require 'yap/shell/commands'
|
2
|
+
|
3
|
+
module Yap::Shell
|
4
|
+
module Builtins
|
5
|
+
def self.builtin(name, &blk)
|
6
|
+
Yap::Shell::BuiltinCommand.add(name, &blk)
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.execute_builtin(name, *args)
|
10
|
+
builtin = Yap::Shell::BuiltinCommand.builtins.fetch(name){ raise("Builtin #{name} not found") }
|
11
|
+
builtin.call *args
|
12
|
+
end
|
13
|
+
|
14
|
+
Dir[File.dirname(__FILE__) + "/builtins/**/*.rb"].each do |f|
|
15
|
+
require f
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'yap/shell/aliases'
|
2
|
+
require 'shellwords'
|
3
|
+
|
4
|
+
module Yap::Shell
|
5
|
+
module Builtins
|
6
|
+
builtin :alias do |*args|
|
7
|
+
output = []
|
8
|
+
if args.empty?
|
9
|
+
Yap::Shell::Aliases.instance.to_h.each_pair do |name, value|
|
10
|
+
# Escape and wrap single quotes since we're using
|
11
|
+
# single quotes to wrap the aliased command for matching
|
12
|
+
# bash output.
|
13
|
+
escaped_value = value.gsub(/'/){ |a| "'\\#{a}'" }
|
14
|
+
output << "alias #{name.shellescape}='#{escaped_value}'"
|
15
|
+
end
|
16
|
+
output << ""
|
17
|
+
else
|
18
|
+
name_eq_value = args.first
|
19
|
+
name, command = name_eq_value.scan(/^(.*?)\s*=\s*(.*)$/).flatten
|
20
|
+
Yap::Shell::Aliases.instance.set_alias name, command
|
21
|
+
end
|
22
|
+
output.join("\n")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Yap::Shell
|
2
|
+
module Builtins
|
3
|
+
DIRECTORY_HISTORY = []
|
4
|
+
DIRECTORY_FUTURE = []
|
5
|
+
|
6
|
+
builtin :cd do |path=ENV['HOME'], *_|
|
7
|
+
DIRECTORY_HISTORY << Dir.pwd
|
8
|
+
Dir.chdir(path)
|
9
|
+
ENV["PWD"] = Dir.pwd
|
10
|
+
output=""
|
11
|
+
end
|
12
|
+
|
13
|
+
builtin :popd do
|
14
|
+
output = []
|
15
|
+
if DIRECTORY_HISTORY.any?
|
16
|
+
DIRECTORY_FUTURE << Dir.pwd
|
17
|
+
path = DIRECTORY_HISTORY.pop
|
18
|
+
execute_builtin :cd, path
|
19
|
+
else
|
20
|
+
output << "popd: directory stack empty\n"
|
21
|
+
end
|
22
|
+
output.join("\n")
|
23
|
+
end
|
24
|
+
|
25
|
+
builtin :pushd do
|
26
|
+
output = []
|
27
|
+
if DIRECTORY_FUTURE.any?
|
28
|
+
DIRECTORY_HISTORY << Dir.pwd
|
29
|
+
path = DIRECTORY_FUTURE.pop
|
30
|
+
execute_builtin :cd, path
|
31
|
+
else
|
32
|
+
output << "pushd: there are no directories in your future\n"
|
33
|
+
end
|
34
|
+
output.join("\n")
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
require 'shellwords'
|
2
|
+
require 'yap/shell/aliases'
|
3
|
+
|
4
|
+
module Yap::Shell
|
5
|
+
class CommandError < StandardError ; end
|
6
|
+
class CommandUnknownError < CommandError ; end
|
7
|
+
|
8
|
+
class CommandFactory
|
9
|
+
def self.build_command_for(command:, args:, heredoc:, internally_evaluate:)
|
10
|
+
return RubyCommand.new(str:command) if internally_evaluate
|
11
|
+
|
12
|
+
case command
|
13
|
+
when ShellCommand then ShellCommand.new(str:command, args:args, heredoc:heredoc)
|
14
|
+
when BuiltinCommand then BuiltinCommand.new(str:command, args:args, heredoc:heredoc)
|
15
|
+
when FileSystemCommand then FileSystemCommand.new(str:command, args:args, heredoc:heredoc)
|
16
|
+
else
|
17
|
+
raise CommandUnknownError, "Don't know how to execute command: #{command}"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
class Command
|
23
|
+
attr_accessor :str, :args
|
24
|
+
attr_accessor :heredoc
|
25
|
+
|
26
|
+
def initialize(str:, args:[], heredoc:nil)
|
27
|
+
@str = str
|
28
|
+
@args = args
|
29
|
+
@heredoc = heredoc
|
30
|
+
end
|
31
|
+
|
32
|
+
def to_executable_str
|
33
|
+
raise NotImplementedError, ":to_executable_str must be implemented by including object."
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
class BuiltinCommand < Command
|
38
|
+
def self.===(other)
|
39
|
+
self.builtins.keys.include?(other.split(' ').first.to_sym) || super
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.builtins
|
43
|
+
@builtins ||= {
|
44
|
+
builtins: lambda { puts @builtins.keys.sort },
|
45
|
+
exit: lambda { |code = 0| exit(code.to_i) },
|
46
|
+
fg: lambda{ :resume },
|
47
|
+
}
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.add(command, &action)
|
51
|
+
builtins.merge!(command.to_sym => action)
|
52
|
+
end
|
53
|
+
|
54
|
+
def execute
|
55
|
+
action = self.class.builtins.fetch(str.to_sym){ raise("Missing proc for builtin: '#{builtin}' in #{str.inspect}") }
|
56
|
+
action.call *args
|
57
|
+
end
|
58
|
+
|
59
|
+
def type
|
60
|
+
:BuiltinCommand
|
61
|
+
end
|
62
|
+
|
63
|
+
def to_executable_str
|
64
|
+
raise NotImplementedError, "#to_executable_str is not implemented on BuiltInCommand"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
class FileSystemCommand < Command
|
69
|
+
def self.===(other)
|
70
|
+
command = other.split(/\s+/).detect{ |f| !f.include?("=") }
|
71
|
+
|
72
|
+
# Check to see if the user gave us a valid path to execute
|
73
|
+
return true if File.executable?(command)
|
74
|
+
|
75
|
+
# See if the command exists anywhere on the path
|
76
|
+
ENV["PATH"].split(":").detect do |path|
|
77
|
+
File.executable?(File.join(path, command))
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def type
|
82
|
+
:FileSystemCommand
|
83
|
+
end
|
84
|
+
|
85
|
+
def to_executable_str
|
86
|
+
[
|
87
|
+
str,
|
88
|
+
args.map(&:shellescape).join(' ')
|
89
|
+
].join(' ')
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
class ShellCommand < Command
|
94
|
+
def self.registered_functions
|
95
|
+
(@registered_functions ||= {}).freeze
|
96
|
+
end
|
97
|
+
|
98
|
+
def self.define_shell_function(name, &blk)
|
99
|
+
raise ArgumentError, "Must provided block when defining a shell function" unless blk
|
100
|
+
(@registered_functions ||= {})[name.to_sym] = blk
|
101
|
+
end
|
102
|
+
|
103
|
+
def self.===(other)
|
104
|
+
registered_functions.include?(other.to_sym)
|
105
|
+
end
|
106
|
+
|
107
|
+
def type
|
108
|
+
:ShellCommand
|
109
|
+
end
|
110
|
+
|
111
|
+
def to_proc
|
112
|
+
self.class.registered_functions.fetch(str.to_sym){
|
113
|
+
raise "Shell function #{str} was not found!"
|
114
|
+
}
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
class RubyCommand < Command
|
119
|
+
def type
|
120
|
+
:RubyCommand
|
121
|
+
end
|
122
|
+
|
123
|
+
def to_executable_str
|
124
|
+
str
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
@@ -0,0 +1,198 @@
|
|
1
|
+
require 'yap/shell/parser'
|
2
|
+
require 'yap/shell/commands'
|
3
|
+
require 'yap/shell/aliases'
|
4
|
+
|
5
|
+
module Yap::Shell
|
6
|
+
class Evaluation
|
7
|
+
def initialize(stdin:, stdout:, stderr:)
|
8
|
+
@stdin, @stdout, @stderr = stdin, stdout, stderr
|
9
|
+
@last_result = nil
|
10
|
+
end
|
11
|
+
|
12
|
+
def evaluate(input, &blk)
|
13
|
+
@blk = blk
|
14
|
+
ast = Yap::Shell::Parser.new.parse(input)
|
15
|
+
ast.accept(self)
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
######################################################################
|
21
|
+
# #
|
22
|
+
# VISITOR METHODS FOR AST TREE WALKING #
|
23
|
+
# #
|
24
|
+
######################################################################
|
25
|
+
|
26
|
+
def visit_CommandNode(node)
|
27
|
+
@aliases_expanded ||= []
|
28
|
+
with_standard_streams do |stdin, stdout, stderr|
|
29
|
+
if !node.literal? && !@aliases_expanded.include?(node.command) && _alias=Aliases.instance.fetch_alias(node.command)
|
30
|
+
@suppress_events = true
|
31
|
+
ast = Yap::Shell::Parser.new.parse([_alias].concat(node.args).join(" "))
|
32
|
+
@aliases_expanded.push(node.command)
|
33
|
+
ast.accept(self)
|
34
|
+
@aliases_expanded.pop
|
35
|
+
@suppress_events = false
|
36
|
+
else
|
37
|
+
command = CommandFactory.build_command_for(
|
38
|
+
command: node.command,
|
39
|
+
args: shell_expand(node.args),
|
40
|
+
heredoc: node.heredoc,
|
41
|
+
internally_evaluate: node.internally_evaluate?)
|
42
|
+
@stdin, @stdout, @stderr = stream_redirections_for(node)
|
43
|
+
@last_result = @blk.call command, @stdin, @stdout, @stderr
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def visit_StatementsNode(node)
|
49
|
+
env = ENV.to_h
|
50
|
+
Yap::Shell::Execution::Context.fire :before_statements_execute, self unless @suppress_events
|
51
|
+
node.head.accept(self)
|
52
|
+
if node.tail
|
53
|
+
node.tail.accept(self)
|
54
|
+
ENV.clear
|
55
|
+
ENV.replace(env)
|
56
|
+
end
|
57
|
+
Yap::Shell::Execution::Context.fire :after_statements_execute, self unless @suppress_events
|
58
|
+
end
|
59
|
+
|
60
|
+
def visit_EnvWrapperNode(node)
|
61
|
+
env = ENV.to_h
|
62
|
+
node.env.each_pair do |k,v|
|
63
|
+
ENV[k] = v
|
64
|
+
end
|
65
|
+
node.node.accept(self)
|
66
|
+
ENV.clear
|
67
|
+
ENV.replace(env)
|
68
|
+
end
|
69
|
+
|
70
|
+
def visit_EnvNode(node)
|
71
|
+
node.env.each_pair do |key,val|
|
72
|
+
ENV[key] = val
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def visit_ConditionalNode(node)
|
77
|
+
case node.operator
|
78
|
+
when '&&'
|
79
|
+
node.expr1.accept self
|
80
|
+
if @last_result.status_code == 0
|
81
|
+
node.expr2.accept self
|
82
|
+
end
|
83
|
+
when '||'
|
84
|
+
node.expr1.accept self
|
85
|
+
if @last_result.status_code != 0
|
86
|
+
node.expr2.accept self
|
87
|
+
end
|
88
|
+
else
|
89
|
+
raise "Don't know how to visit conditional node: #{node.inspect}"
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def visit_PipelineNode(node, options={})
|
94
|
+
with_standard_streams do |stdin, stdout, stderr|
|
95
|
+
# Modify @stdout and @stderr for the first command
|
96
|
+
stdin, @stdout = IO.pipe
|
97
|
+
@stderr = @stdout
|
98
|
+
|
99
|
+
# Don't modify @stdin for the first command in the pipeline.
|
100
|
+
node.head.accept(self)
|
101
|
+
|
102
|
+
# Modify @stdin starting with the second command to read from the
|
103
|
+
# read portion of our above stdout.
|
104
|
+
@stdin = stdin
|
105
|
+
|
106
|
+
# Modify @stdout,@stderr to go back to the original
|
107
|
+
@stdout, @stderr = stdout, stderr
|
108
|
+
|
109
|
+
node.tail.accept(self)
|
110
|
+
|
111
|
+
# Set our @stdin back to the original
|
112
|
+
@stdin = stdin
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def visit_InternalEvalNode(node)
|
117
|
+
command = CommandFactory.build_command_for(
|
118
|
+
command: node.command,
|
119
|
+
args: node.args,
|
120
|
+
heredoc: node.heredoc,
|
121
|
+
internally_evaluate: node.internally_evaluate?)
|
122
|
+
@last_result = @blk.call command, @stdin, @stdout, @stderr
|
123
|
+
end
|
124
|
+
|
125
|
+
######################################################################
|
126
|
+
# #
|
127
|
+
# HELPER / UTILITY METHODS #
|
128
|
+
# #
|
129
|
+
######################################################################
|
130
|
+
|
131
|
+
def alias_expand(input, aliases:Aliases.instance)
|
132
|
+
head, *tail = input.split(/\s/, 2).first
|
133
|
+
if new_head=aliases.fetch_alias(head)
|
134
|
+
[new_head].concat(tail).join(" ")
|
135
|
+
else
|
136
|
+
input
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def shell_expand(input)
|
141
|
+
[input].flatten.inject([]) do |results,str|
|
142
|
+
# Basic bash-style brace expansion
|
143
|
+
expansions = str.scan(/\{([^\}]+)\}/).flatten.first
|
144
|
+
if expansions
|
145
|
+
expansions.split(",").each do |expansion|
|
146
|
+
results << str.sub(/\{([^\}]+)\}/, expansion)
|
147
|
+
end
|
148
|
+
else
|
149
|
+
results << str
|
150
|
+
end
|
151
|
+
|
152
|
+
results = results.map! do |s|
|
153
|
+
# Basic bash-style tilde expansion
|
154
|
+
s.gsub!(/\A~(.*)/, ENV["HOME"] + '\1')
|
155
|
+
|
156
|
+
# Basic bash-style variable expansion
|
157
|
+
if s =~ /^\$(.*)/
|
158
|
+
s = ENV.fetch($1, "")
|
159
|
+
end
|
160
|
+
|
161
|
+
# Basic bash-style path-name expansion
|
162
|
+
expansions = Dir[s]
|
163
|
+
if expansions.any?
|
164
|
+
expansions
|
165
|
+
else
|
166
|
+
s
|
167
|
+
end
|
168
|
+
end.flatten
|
169
|
+
end.flatten
|
170
|
+
end
|
171
|
+
|
172
|
+
def with_standard_streams(&blk)
|
173
|
+
stdin, stdout, stderr = @stdin, @stdout, @stderr
|
174
|
+
yield stdin, stdout, stderr
|
175
|
+
@stdin, @stdout, @stderr = stdin, stdout, stderr
|
176
|
+
end
|
177
|
+
|
178
|
+
def stream_redirections_for(node)
|
179
|
+
stdin, stdout, stderr = @stdin, @stdout, @stderr
|
180
|
+
node.redirects.each do |redirect|
|
181
|
+
case redirect.kind
|
182
|
+
when "<"
|
183
|
+
stdin = redirect.target
|
184
|
+
when ">", "1>"
|
185
|
+
stdout = redirect.target
|
186
|
+
when "1>&2"
|
187
|
+
stderr = :stdout
|
188
|
+
when "2>"
|
189
|
+
stderr = redirect.target
|
190
|
+
when "2>&1"
|
191
|
+
stdout = :stderr
|
192
|
+
end
|
193
|
+
end
|
194
|
+
[stdin, stdout, stderr]
|
195
|
+
end
|
196
|
+
|
197
|
+
end
|
198
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Yap::Shell
|
2
|
+
module Execution
|
3
|
+
autoload :Context, "yap/shell/execution/context"
|
4
|
+
|
5
|
+
autoload :CommandExecution, "yap/shell/execution/command_execution"
|
6
|
+
autoload :BuiltinCommandExecution, "yap/shell/execution/builtin_command_execution"
|
7
|
+
autoload :FileSystemCommandExecution, "yap/shell/execution/file_system_command_execution"
|
8
|
+
autoload :RubyCommandExecution, "yap/shell/execution/ruby_command_execution"
|
9
|
+
autoload :ShellCommandExecution, "yap/shell/execution/shell_command_execution"
|
10
|
+
|
11
|
+
autoload :Result, "yap/shell/execution/result"
|
12
|
+
|
13
|
+
Context.register BuiltinCommandExecution, command_type: :BuiltinCommand
|
14
|
+
Context.register FileSystemCommandExecution, command_type: :FileSystemCommand
|
15
|
+
Context.register ShellCommandExecution, command_type: :ShellCommand
|
16
|
+
Context.register RubyCommandExecution, command_type: :RubyCommand
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Yap::Shell::Execution
|
2
|
+
class BuiltinCommandExecution < CommandExecution
|
3
|
+
on_execute do |command:, n:, of:|
|
4
|
+
command_output = command.execute
|
5
|
+
if command_output == :resume
|
6
|
+
ResumeExecution.new(status_code:0, directory:Dir.pwd, n:n, of:of)
|
7
|
+
else
|
8
|
+
@stdout.write command_output
|
9
|
+
@stdout.close if @stdout != $stdout && !@stdout.closed?
|
10
|
+
@stderr.close if @stderr != $stderr && !@stderr.closed?
|
11
|
+
Result.new(status_code:0, directory:Dir.pwd, n:n, of:of)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|