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