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