yap-shell 0.0.2 → 0.1.0
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 +4 -4
- data/addons/history.rb +171 -0
- data/bin/yap +10 -14
- data/lib/tasks/gem.rake +4 -2
- data/lib/yap.rb +39 -6
- data/lib/yap/shell.rb +0 -16
- data/lib/yap/shell/builtins.rb +3 -3
- data/lib/yap/shell/builtins/cd.rb +37 -15
- data/lib/yap/shell/commands.rb +6 -5
- data/lib/yap/shell/evaluation.rb +24 -3
- data/lib/yap/shell/execution.rb +8 -10
- data/lib/yap/shell/execution/builtin_command_execution.rb +5 -4
- data/lib/yap/shell/execution/context.rb +24 -16
- data/lib/yap/shell/execution/file_system_command_execution.rb +73 -59
- data/lib/yap/shell/repl.rb +0 -6
- data/lib/yap/shell/version.rb +1 -1
- data/lib/yap/world.rb +2 -1
- data/rcfiles/.yaprc +5 -145
- data/scripts/4 +8 -0
- data/scripts/bg-vim +4 -0
- data/scripts/fail +3 -0
- data/scripts/letters +8 -0
- data/scripts/lots-of-output +6 -0
- data/scripts/pass +3 -0
- data/scripts/simulate-long-running +4 -0
- data/scripts/write-to-stderr.rb +3 -0
- data/scripts/write-to-stdout.rb +3 -0
- data/yap-shell.gemspec +1 -1
- metadata +14 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e4b50596222f441b63d4b4db6a74b34cdcdee1e7
|
4
|
+
data.tar.gz: 2d0095e8845196cb5b6fa0a6c44b5783f8b2849e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 64733efa5a216775d9c26ab3fe99b48270bf1edb7a80ebe5a2f898906085cd8f127a6c1ca141004890be5fcd8a895119f0bad8ed0856f4d9a4dceb43708a663b
|
7
|
+
data.tar.gz: f280c566e9e15cc692d77094a2659983b6dd9cf6433cdd26fe0cf0fe7b4d2d7107845885b080c4548411b82ffe195e4e462f691be165fa8e5271ac2e5a68ff39
|
data/addons/history.rb
ADDED
@@ -0,0 +1,171 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
module Yap
|
3
|
+
module WorldAddons
|
4
|
+
class History
|
5
|
+
def self.load
|
6
|
+
instance
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.instance
|
10
|
+
@history ||= History.new
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize
|
14
|
+
@history = []
|
15
|
+
end
|
16
|
+
|
17
|
+
def initialize_world(world)
|
18
|
+
load_history
|
19
|
+
|
20
|
+
world.func(:howmuch) do |args:, stdin:, stdout:, stderr:|
|
21
|
+
case args.first
|
22
|
+
when "time"
|
23
|
+
if history_item=self.last_executed_item
|
24
|
+
stdout.puts history_item.total_time_s
|
25
|
+
else
|
26
|
+
stdout.puts "Can't report on something you haven't done."
|
27
|
+
end
|
28
|
+
else
|
29
|
+
stdout.puts "How much what?"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def executing(command:, started_at:)
|
35
|
+
raise "Cannot acknowledge execution beginning of a command when no group has been started!" unless @history.last
|
36
|
+
@history.last.executing command:command, started_at:started_at
|
37
|
+
end
|
38
|
+
|
39
|
+
def executed(command:, stopped_at:)
|
40
|
+
raise "Cannot complete execution of a command when no group has been started!" unless @history.last
|
41
|
+
@history.last.executed command:command, stopped_at:stopped_at
|
42
|
+
end
|
43
|
+
|
44
|
+
def last_executed_item
|
45
|
+
@history.reverse.each do |group|
|
46
|
+
last_run = group.last_executed_item
|
47
|
+
break last_run if last_run
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def start_group(started_at)
|
52
|
+
@history.push Group.new(started_at:started_at)
|
53
|
+
end
|
54
|
+
|
55
|
+
def stop_group(stopped_at)
|
56
|
+
@history.last.stopped_at(stopped_at)
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def history_file
|
62
|
+
@history_file ||= File.expand_path('~') + '/.yap-history'
|
63
|
+
end
|
64
|
+
|
65
|
+
def load_history
|
66
|
+
return unless File.exists?(history_file) && File.readable?(history_file)
|
67
|
+
(YAML.load_file(history_file) || []).each do |item|
|
68
|
+
::Readline::HISTORY.push item
|
69
|
+
end
|
70
|
+
|
71
|
+
at_exit do
|
72
|
+
File.write history_file, ::Readline::HISTORY.to_a.to_yaml
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
class Group
|
77
|
+
extend Forwardable
|
78
|
+
|
79
|
+
def initialize(started_at:Time.now)
|
80
|
+
@started_at = started_at
|
81
|
+
@stopped_at = nil
|
82
|
+
@items = []
|
83
|
+
end
|
84
|
+
|
85
|
+
def_delegators :@items, :push, :<<, :pop, :first, :last
|
86
|
+
|
87
|
+
def duration
|
88
|
+
return nil unless @stopped_at
|
89
|
+
@stopped_at - @started_at
|
90
|
+
end
|
91
|
+
|
92
|
+
def executing(command:, started_at:)
|
93
|
+
@items.push Item.new(command:command, started_at:started_at)
|
94
|
+
end
|
95
|
+
|
96
|
+
def executed(command:, stopped_at:)
|
97
|
+
raise "2:Cannot complete execution of a command when no group has been started!" unless @items.last
|
98
|
+
item = @items.reverse.detect do |item|
|
99
|
+
command == item.command && !item.finished?
|
100
|
+
end
|
101
|
+
item.finished!(stopped_at)
|
102
|
+
end
|
103
|
+
|
104
|
+
def last_executed_item
|
105
|
+
@items.reverse.detect{ |item| item.finished? }
|
106
|
+
end
|
107
|
+
|
108
|
+
def stopped_at(time)
|
109
|
+
@stopped_at ||= time
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
class Item
|
114
|
+
attr_reader :command
|
115
|
+
|
116
|
+
def initialize(command:command, started_at:Time.now)
|
117
|
+
@command = command
|
118
|
+
@started_at = started_at
|
119
|
+
@ended_at = nil
|
120
|
+
end
|
121
|
+
|
122
|
+
def finished!(at)
|
123
|
+
@ended_at = at
|
124
|
+
end
|
125
|
+
|
126
|
+
def finished?
|
127
|
+
!!@ended_at
|
128
|
+
end
|
129
|
+
|
130
|
+
def total_time_s
|
131
|
+
humanize(@ended_at - @started_at) if @ended_at && @started_at
|
132
|
+
end
|
133
|
+
|
134
|
+
private
|
135
|
+
|
136
|
+
def humanize secs
|
137
|
+
[[60, :seconds], [60, :minutes], [24, :hours], [1000, :days]].inject([]){ |s, (count, name)|
|
138
|
+
if secs > 0
|
139
|
+
secs, n = secs.divmod(count)
|
140
|
+
s.unshift "#{n} #{name}"
|
141
|
+
end
|
142
|
+
s
|
143
|
+
}.join(' ')
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
Yap::Shell::Execution::Context.on(:before_statements_execute) do |context|
|
148
|
+
puts "Before group: #{context.to_s}" if ENV["DEBUG"]
|
149
|
+
History.instance.start_group(Time.now)
|
150
|
+
end
|
151
|
+
|
152
|
+
Yap::Shell::Execution::Context.on(:after_statements_execute) do |context|
|
153
|
+
History.instance.stop_group(Time.now)
|
154
|
+
puts "After group: #{context.to_s}" if ENV["DEBUG"]
|
155
|
+
end
|
156
|
+
|
157
|
+
Yap::Shell::Execution::Context.on(:after_process_finished) do |context, *args|
|
158
|
+
# puts "After process: #{context.to_s}, args: #{args.inspect}"
|
159
|
+
end
|
160
|
+
|
161
|
+
Yap::Shell::Execution::Context.on(:before_execute) do |context, command:|
|
162
|
+
History.instance.executing command:command.str, started_at:Time.now
|
163
|
+
end
|
164
|
+
|
165
|
+
Yap::Shell::Execution::Context.on(:after_execute) do |context, command:, result:|
|
166
|
+
History.instance.executed command:command.str, stopped_at:Time.now
|
167
|
+
end
|
168
|
+
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
data/bin/yap
CHANGED
@@ -1,24 +1,20 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
|
+
|
2
3
|
file = __FILE__
|
3
4
|
if File.symlink?(file)
|
4
5
|
file = File.readlink(file)
|
5
6
|
end
|
6
7
|
|
7
|
-
|
8
|
-
|
8
|
+
trap "SIGTSTP", "IGNORE"
|
9
|
+
trap "SIGINT", "IGNORE"
|
10
|
+
trap "SIGTTIN", "IGNORE"
|
11
|
+
trap "SIGTTOU", "IGNORE"
|
9
12
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
+
ENV["PATH"] = "/Applications/Postgres.app/Contents/MacOS/bin:/usr/local/share/npm/bin/:/usr/local/heroku/bin:/Users/zdennis/.bin:/Users/zdennis/.rvm/gems/ruby-2.1.5/bin:/Users/zdennis/.rvm/gems/ruby-2.1.5@global/bin:/Users/zdennis/.rvm/rubies/ruby-2.1.5/bin:/usr/local/bin:/usr/local/sbin:/Users/zdennis/bin:/bin:/opt/local/bin:/opt/local/sbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/opt/X11/bin:/usr/local/CrossPack-AVR/bin:/private/tmp/.tidbits/bin:/Users/zdennis/source/playground/AdobeAir/AdobeAIRSDK/bin:/Users/zdennis/.rvm/bin:/Users/zdennis/Downloads/adt-bundle-mac-x86_64-20130219/sdk/tools/:/Users/zdennis/.rvm/bin"
|
14
|
+
ENV["GEM_HOME"] = "/Users/zdennis/.rvm/gems/ruby-2.1.5:/Users/zdennis/.rvm/gems/ruby-2.1.5@global"
|
15
|
+
ENV["GEM_PATH"] = "/Users/zdennis/.rvm/gems/ruby-2.1.5"
|
13
16
|
|
14
17
|
$LOAD_PATH << File.dirname(file) + '/../lib'
|
15
18
|
|
16
|
-
|
17
|
-
|
18
|
-
begin
|
19
|
-
Yap.run_shell
|
20
|
-
rescue StandardError => ex
|
21
|
-
puts ex.message
|
22
|
-
puts ex.backtrace
|
23
|
-
end
|
24
|
-
# end
|
19
|
+
require "yap"
|
20
|
+
Yap.run_shell
|
data/lib/tasks/gem.rake
CHANGED
@@ -15,11 +15,13 @@ namespace :bump do
|
|
15
15
|
_major = major.call($1) if major
|
16
16
|
_minor = minor.call($2) if minor
|
17
17
|
_patch = patch.call($3) if patch
|
18
|
-
version =
|
18
|
+
version = "#{_major}.#{_minor}.#{_patch}"
|
19
|
+
results = %|VERSION = "#{version}"|
|
19
20
|
end
|
20
21
|
File.write(@file, contents)
|
21
22
|
system "bundle"
|
22
|
-
system "git add #{ProjectVersion::FILE}
|
23
|
+
system "git add #{ProjectVersion::FILE} && git commit -m 'Bumping version to #{version}'"
|
24
|
+
system "git tag v#{version}"
|
23
25
|
end
|
24
26
|
|
25
27
|
private
|
data/lib/yap.rb
CHANGED
@@ -14,17 +14,50 @@ module Yap
|
|
14
14
|
(puts "Cannot load world addon: #{file} is not readable" and next) unless File.exist?(file)
|
15
15
|
(puts "Cannot load world addon: #{file} is a directory file" and next) if File.directory?(file)
|
16
16
|
|
17
|
-
|
18
|
-
|
17
|
+
addon = file.end_with?("rc") ? load_rcfile(file) : load_addon_file(file)
|
18
|
+
addon.load
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
class RcFile
|
23
|
+
def initialize(contents)
|
24
|
+
@contents = contents
|
25
|
+
end
|
26
|
+
|
27
|
+
def load
|
28
|
+
self
|
29
|
+
end
|
30
|
+
|
31
|
+
def initialize_world(world)
|
32
|
+
world.instance_eval @contents
|
19
33
|
end
|
20
34
|
end
|
21
|
-
end
|
22
35
|
|
36
|
+
def self.load_rcfile(file)
|
37
|
+
RcFile.new IO.read(file)
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.load_addon_file(file)
|
41
|
+
name = File.basename(file).sub(/\.[^\.]+$/, "").split(/[_]/).map(&:capitalize).join
|
42
|
+
klass_name = "Yap::WorldAddons::#{name}"
|
43
|
+
|
44
|
+
load file
|
45
|
+
|
46
|
+
if Yap::WorldAddons.const_defined?(name)
|
47
|
+
Yap::WorldAddons.const_get(name)
|
48
|
+
else
|
49
|
+
raise("Did not find #{klass_name} in #{file}")
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
23
53
|
|
24
54
|
def self.run_shell
|
25
|
-
|
26
|
-
"#{ENV['HOME']}/.yaprc"
|
27
|
-
|
55
|
+
addon_files = Dir[
|
56
|
+
"#{ENV['HOME']}/.yaprc",
|
57
|
+
"#{ENV['HOME']}/.yap-addons/*.rb"
|
58
|
+
]
|
59
|
+
|
60
|
+
addons = WorldAddons.load_from_files(files:addon_files)
|
28
61
|
Shell::Impl.new(addons: addons).repl
|
29
62
|
end
|
30
63
|
end
|
data/lib/yap/shell.rb
CHANGED
@@ -51,22 +51,6 @@ module Yap
|
|
51
51
|
end
|
52
52
|
end
|
53
53
|
|
54
|
-
private
|
55
|
-
|
56
|
-
def history_file
|
57
|
-
File.expand_path('~') + '/.yap-history'
|
58
|
-
end
|
59
|
-
|
60
|
-
def load_history
|
61
|
-
return unless File.exists?(history_file) && File.readable?(history_file)
|
62
|
-
(YAML.load_file(history_file) || []).each do |item|
|
63
|
-
::Readline::HISTORY.push item
|
64
|
-
end
|
65
|
-
|
66
|
-
at_exit do
|
67
|
-
File.write history_file, ::Readline::HISTORY.to_a.to_yaml
|
68
|
-
end
|
69
|
-
end
|
70
54
|
end
|
71
55
|
|
72
56
|
end
|
data/lib/yap/shell/builtins.rb
CHANGED
@@ -6,9 +6,9 @@ module Yap::Shell
|
|
6
6
|
Yap::Shell::BuiltinCommand.add(name, &blk)
|
7
7
|
end
|
8
8
|
|
9
|
-
def self.execute_builtin(name,
|
10
|
-
|
11
|
-
|
9
|
+
def self.execute_builtin(name, args:, stdin:, stdout:, stderr:)
|
10
|
+
command = Yap::Shell::BuiltinCommand.new(str:name, args: args)
|
11
|
+
command.execute(stdin:stdin, stdout:stdout, stderr:stderr)
|
12
12
|
end
|
13
13
|
|
14
14
|
Dir[File.dirname(__FILE__) + "/builtins/**/*.rb"].each do |f|
|
@@ -3,35 +3,57 @@ module Yap::Shell
|
|
3
3
|
DIRECTORY_HISTORY = []
|
4
4
|
DIRECTORY_FUTURE = []
|
5
5
|
|
6
|
-
builtin :cd do |
|
7
|
-
|
8
|
-
Dir.
|
9
|
-
|
10
|
-
|
6
|
+
builtin :cd do |args:, stderr:, **|
|
7
|
+
path = args.first || ENV['HOME']
|
8
|
+
cwd = Dir.pwd
|
9
|
+
if Dir.exist?(path)
|
10
|
+
DIRECTORY_HISTORY << cwd
|
11
|
+
Dir.chdir(path)
|
12
|
+
ENV["PWD"] = cwd
|
13
|
+
exit_status = 0
|
14
|
+
else
|
15
|
+
stderr.puts "cd: #{path}: No such file or directory"
|
16
|
+
exit_status = 1
|
17
|
+
end
|
11
18
|
end
|
12
19
|
|
13
|
-
builtin :popd do
|
20
|
+
builtin :popd do |args:, stderr:, **keyword_args|
|
14
21
|
output = []
|
22
|
+
cwd = Dir.pwd
|
15
23
|
if DIRECTORY_HISTORY.any?
|
16
|
-
DIRECTORY_FUTURE << Dir.pwd
|
17
24
|
path = DIRECTORY_HISTORY.pop
|
18
|
-
|
25
|
+
if Dir.exist?(path)
|
26
|
+
DIRECTORY_FUTURE << cwd
|
27
|
+
Dir.chdir(path)
|
28
|
+
ENV["PWD"] =cwd
|
29
|
+
exit_status = 0
|
30
|
+
else
|
31
|
+
stderr.puts "popd: #{path}: No such file or directory"
|
32
|
+
exit_status = 1
|
33
|
+
end
|
19
34
|
else
|
20
|
-
|
35
|
+
stderr.puts "popd: directory stack empty"
|
36
|
+
exit_status = 1
|
21
37
|
end
|
22
|
-
output.join("\n")
|
23
38
|
end
|
24
39
|
|
25
|
-
builtin :pushd do
|
40
|
+
builtin :pushd do |args:, stderr:, **keyword_args|
|
26
41
|
output = []
|
27
42
|
if DIRECTORY_FUTURE.any?
|
28
|
-
DIRECTORY_HISTORY << Dir.pwd
|
29
43
|
path = DIRECTORY_FUTURE.pop
|
30
|
-
|
44
|
+
if Dir.exist?(path)
|
45
|
+
DIRECTORY_HISTORY << Dir.pwd
|
46
|
+
Dir.chdir(path)
|
47
|
+
ENV["PWD"] = path
|
48
|
+
exit_status = 0
|
49
|
+
else
|
50
|
+
stderr.puts "pushd: #{path}: No such file or directory"
|
51
|
+
exit_status = 1
|
52
|
+
end
|
31
53
|
else
|
32
|
-
|
54
|
+
stderr.puts "pushd: there are no directories in your future"
|
55
|
+
exit_status = 1
|
33
56
|
end
|
34
|
-
output.join("\n")
|
35
57
|
end
|
36
58
|
end
|
37
59
|
end
|
data/lib/yap/shell/commands.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
require 'shellwords'
|
2
2
|
require 'yap/shell/aliases'
|
3
|
+
require 'yap/shell/execution/result'
|
3
4
|
|
4
5
|
module Yap::Shell
|
5
6
|
class CommandError < StandardError ; end
|
@@ -41,9 +42,9 @@ module Yap::Shell
|
|
41
42
|
|
42
43
|
def self.builtins
|
43
44
|
@builtins ||= {
|
44
|
-
builtins: lambda { puts @builtins.keys.sort },
|
45
|
-
exit: lambda { |code = 0
|
46
|
-
fg: lambda{ :resume },
|
45
|
+
builtins: lambda { |stdout:, **| stdout.puts @builtins.keys.sort },
|
46
|
+
exit: lambda { |code = 0, **| exit(code.to_i) },
|
47
|
+
fg: lambda{ |**| :resume },
|
47
48
|
}
|
48
49
|
end
|
49
50
|
|
@@ -51,9 +52,9 @@ module Yap::Shell
|
|
51
52
|
builtins.merge!(command.to_sym => action)
|
52
53
|
end
|
53
54
|
|
54
|
-
def execute
|
55
|
+
def execute(stdin:, stdout:, stderr:)
|
55
56
|
action = self.class.builtins.fetch(str.to_sym){ raise("Missing proc for builtin: '#{builtin}' in #{str.inspect}") }
|
56
|
-
action.call
|
57
|
+
action.call args:args, stdin:stdin, stdout:stdout, stderr:stderr
|
57
58
|
end
|
58
59
|
|
59
60
|
def type
|
data/lib/yap/shell/evaluation.rb
CHANGED
@@ -11,12 +11,33 @@ module Yap::Shell
|
|
11
11
|
|
12
12
|
def evaluate(input, &blk)
|
13
13
|
@blk = blk
|
14
|
+
parser = Yap::Shell::Parser.new
|
15
|
+
input = recursively_find_and_replace_command_substitutions(parser, input)
|
14
16
|
ast = Yap::Shell::Parser.new.parse(input)
|
15
17
|
ast.accept(self)
|
16
18
|
end
|
17
19
|
|
18
20
|
private
|
19
21
|
|
22
|
+
# +recursively_find_and_replace_command_substitutions+ is responsible for recursively
|
23
|
+
# finding and expanding command substitutions, in a depth first manner.
|
24
|
+
def recursively_find_and_replace_command_substitutions(parser, input)
|
25
|
+
input = input.dup
|
26
|
+
parser.each_command_substitution_for(input) do |substitution_result, start_position, end_position|
|
27
|
+
result = recursively_find_and_replace_command_substitutions(parser, substitution_result.str)
|
28
|
+
position = substitution_result.position
|
29
|
+
ast = parser.parse(result)
|
30
|
+
with_standard_streams do |stdin, stdout, stderr|
|
31
|
+
r,w = IO.pipe
|
32
|
+
@stdout = w
|
33
|
+
ast.accept(self)
|
34
|
+
input[position.min...position.max] = r.read.chomp
|
35
|
+
end
|
36
|
+
end
|
37
|
+
input
|
38
|
+
end
|
39
|
+
|
40
|
+
|
20
41
|
######################################################################
|
21
42
|
# #
|
22
43
|
# VISITOR METHODS FOR AST TREE WALKING #
|
@@ -26,9 +47,10 @@ module Yap::Shell
|
|
26
47
|
def visit_CommandNode(node)
|
27
48
|
@aliases_expanded ||= []
|
28
49
|
with_standard_streams do |stdin, stdout, stderr|
|
50
|
+
args = node.args.map(&:lvalue)
|
29
51
|
if !node.literal? && !@aliases_expanded.include?(node.command) && _alias=Aliases.instance.fetch_alias(node.command)
|
30
52
|
@suppress_events = true
|
31
|
-
ast = Yap::Shell::Parser.new.parse([_alias].concat(
|
53
|
+
ast = Yap::Shell::Parser.new.parse([_alias].concat(args).join(" "))
|
32
54
|
@aliases_expanded.push(node.command)
|
33
55
|
ast.accept(self)
|
34
56
|
@aliases_expanded.pop
|
@@ -36,7 +58,7 @@ module Yap::Shell
|
|
36
58
|
else
|
37
59
|
command = CommandFactory.build_command_for(
|
38
60
|
command: node.command,
|
39
|
-
args: shell_expand(
|
61
|
+
args: shell_expand(args),
|
40
62
|
heredoc: node.heredoc,
|
41
63
|
internally_evaluate: node.internally_evaluate?)
|
42
64
|
@stdin, @stdout, @stderr = stream_redirections_for(node)
|
@@ -94,7 +116,6 @@ module Yap::Shell
|
|
94
116
|
with_standard_streams do |stdin, stdout, stderr|
|
95
117
|
# Modify @stdout and @stderr for the first command
|
96
118
|
stdin, @stdout = IO.pipe
|
97
|
-
@stderr = @stdout
|
98
119
|
|
99
120
|
# Don't modify @stdin for the first command in the pipeline.
|
100
121
|
node.head.accept(self)
|
data/lib/yap/shell/execution.rb
CHANGED
@@ -1,15 +1,13 @@
|
|
1
|
+
require "yap/shell/execution/context"
|
2
|
+
require "yap/shell/execution/command_execution"
|
3
|
+
require "yap/shell/execution/builtin_command_execution"
|
4
|
+
require "yap/shell/execution/file_system_command_execution"
|
5
|
+
require "yap/shell/execution/ruby_command_execution"
|
6
|
+
require "yap/shell/execution/shell_command_execution"
|
7
|
+
require "yap/shell/execution/result"
|
8
|
+
|
1
9
|
module Yap::Shell
|
2
10
|
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
11
|
Context.register BuiltinCommandExecution, command_type: :BuiltinCommand
|
14
12
|
Context.register FileSystemCommandExecution, command_type: :FileSystemCommand
|
15
13
|
Context.register ShellCommandExecution, command_type: :ShellCommand
|
@@ -1,14 +1,15 @@
|
|
1
|
+
require 'yap/shell/execution/result'
|
2
|
+
|
1
3
|
module Yap::Shell::Execution
|
2
4
|
class BuiltinCommandExecution < CommandExecution
|
3
5
|
on_execute do |command:, n:, of:|
|
4
|
-
|
5
|
-
if
|
6
|
+
status_code = command.execute(stdin:@stdin, stdout:@stdout, stderr:@stderr)
|
7
|
+
if status_code == :resume
|
6
8
|
ResumeExecution.new(status_code:0, directory:Dir.pwd, n:n, of:of)
|
7
9
|
else
|
8
|
-
@stdout.write command_output
|
9
10
|
@stdout.close if @stdout != $stdout && !@stdout.closed?
|
10
11
|
@stderr.close if @stderr != $stderr && !@stderr.closed?
|
11
|
-
Result.new(status_code:
|
12
|
+
Result.new(status_code:status_code, directory:Dir.pwd, n:n, of:of)
|
12
13
|
end
|
13
14
|
end
|
14
15
|
end
|
@@ -57,26 +57,13 @@ module Yap::Shell::Execution
|
|
57
57
|
world: world
|
58
58
|
)
|
59
59
|
|
60
|
+
@saved_tty_attrs = Termios.tcgetattr(STDIN)
|
60
61
|
self.class.fire :before_execute, execution_context, command: command
|
61
62
|
result = execution_context.execute(command:command, n:i, of:of)
|
62
63
|
self.class.fire :after_execute, execution_context, command: command, result: result
|
63
64
|
|
64
|
-
|
65
|
-
|
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
|
65
|
+
results << process_execution_result(execution_context:execution_context, result: result)
|
66
|
+
Termios.tcsetattr(STDIN, Termios::TCSANOW, @saved_tty_attrs)
|
80
67
|
end
|
81
68
|
end
|
82
69
|
|
@@ -84,5 +71,26 @@ module Yap::Shell::Execution
|
|
84
71
|
|
85
72
|
results.last
|
86
73
|
end
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
def process_execution_result(execution_context:, result:)
|
78
|
+
case result
|
79
|
+
when SuspendExecution
|
80
|
+
@suspended_execution_contexts.push execution_context
|
81
|
+
return result
|
82
|
+
|
83
|
+
when ResumeExecution
|
84
|
+
execution_context = @suspended_execution_contexts.pop
|
85
|
+
if execution_context
|
86
|
+
nresult = execution_context.resume
|
87
|
+
return process_execution_result execution_context: execution_context, result: nresult
|
88
|
+
else
|
89
|
+
stderr.puts "fg: No such job"
|
90
|
+
end
|
91
|
+
else
|
92
|
+
return result
|
93
|
+
end
|
94
|
+
end
|
87
95
|
end
|
88
96
|
end
|
@@ -1,79 +1,93 @@
|
|
1
|
+
require 'yap/shell/execution/result'
|
2
|
+
require 'termios'
|
3
|
+
|
1
4
|
module Yap::Shell::Execution
|
2
5
|
class FileSystemCommandExecution < CommandExecution
|
3
6
|
on_execute do |command:, n:, of:, resume_blk:nil|
|
4
7
|
stdin, stdout, stderr, world = @stdin, @stdout, @stderr, @world
|
5
8
|
result = nil
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
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
|
9
|
+
if resume_blk
|
10
|
+
pid = resume_blk.call
|
11
|
+
else
|
12
|
+
r,w = nil, nil
|
13
|
+
if command.heredoc
|
14
|
+
r,w = IO.pipe
|
15
|
+
stdin = r
|
38
16
|
end
|
39
17
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
18
|
+
pid = fork do
|
19
|
+
# reset signals in case any were ignored
|
20
|
+
Signal.trap("SIGINT", "DEFAULT")
|
21
|
+
Signal.trap("SIGQUIT", "DEFAULT")
|
22
|
+
Signal.trap("SIGTSTP", "DEFAULT")
|
23
|
+
Signal.trap("SIGTTIN", "DEFAULT")
|
24
|
+
Signal.trap("SIGTTOU", "DEFAULT")
|
25
|
+
|
26
|
+
# Set the process group of the forked to child to that of the
|
27
|
+
Process.setpgrp
|
28
|
+
|
29
|
+
# Start a new process group as the session leader. Now we are
|
30
|
+
# responsible for sending signals that would have otherwise
|
31
|
+
# been propagated to the process, e.g. SIGINT, SIGSTOP, SIGCONT, etc.
|
32
|
+
stdin = File.open(stdin, "rb") if stdin.is_a?(String)
|
33
|
+
stdout = File.open(stdout, "wb") if stdout.is_a?(String)
|
34
|
+
stderr = File.open(stderr, "wb") if stderr.is_a?(String)
|
46
35
|
|
47
|
-
|
48
|
-
|
36
|
+
stdout = stderr if stdout == :stderr
|
37
|
+
stderr = stdout if stderr == :stdout
|
49
38
|
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
39
|
+
$stdin.reopen stdin
|
40
|
+
$stdout.reopen stdout
|
41
|
+
$stderr.reopen stderr
|
42
|
+
|
43
|
+
Kernel.exec command.to_executable_str
|
55
44
|
end
|
56
|
-
|
57
|
-
|
45
|
+
|
46
|
+
# Put the child process into a process group of its own
|
47
|
+
Process.setpgid pid, pid
|
48
|
+
|
49
|
+
if command.heredoc
|
50
|
+
w.write command.heredoc
|
51
|
+
w.close
|
58
52
|
end
|
59
|
-
|
53
|
+
end
|
60
54
|
|
61
|
-
|
62
|
-
|
55
|
+
# Set terminal's process group to that of the child process
|
56
|
+
Termios.tcsetpgrp STDIN, pid
|
57
|
+
pid, status = Process.wait2(-1, Process::WUNTRACED) unless of > 1
|
58
|
+
puts "Process (#{pid}) stopped: #{status.inspect}" if ENV["DEBUG"]
|
63
59
|
|
64
|
-
|
65
|
-
|
60
|
+
# If we're not printing to the terminal than close in/out/err. This
|
61
|
+
# is so the next command in the pipeline can complete and don't hang waiting for
|
62
|
+
# stdin after the command that's writing to its stdin has completed.
|
63
|
+
if stdout != $stdout && stdout.is_a?(IO) && !stdout.closed? then
|
64
|
+
stdout.close
|
65
|
+
end
|
66
|
+
if stderr != $stderr && stderr.is_a?(IO) && !stderr.closed? then
|
67
|
+
stderr.close
|
68
|
+
end
|
69
|
+
# if stdin != $stdin && !stdin.closed? then stdin.close end
|
66
70
|
|
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
|
|
71
|
-
|
72
|
+
# if the pid that just stopped was the process group owner then
|
73
|
+
# give it back to the us so we can become the foreground process
|
74
|
+
# in the terminal
|
75
|
+
if pid == Termios.tcgetpgrp(STDIN)
|
76
|
+
Process.setpgid Process.pid, Process.pid
|
77
|
+
Termios.tcsetpgrp STDIN, Process.pid
|
72
78
|
end
|
73
79
|
|
74
|
-
# if
|
75
|
-
|
76
|
-
|
80
|
+
# if the reason we stopped is from being suspended
|
81
|
+
if status.stopsig == Signal.list["TSTP"]
|
82
|
+
puts "Process (#{pid}) suspended: #{status.stopsig}" if ENV["DEBUG"]
|
83
|
+
suspended(command:command, n:n, of:of, pid: pid)
|
84
|
+
result = Yap::Shell::Execution::SuspendExecution.new(status_code:nil, directory:Dir.pwd, n:n, of:of)
|
85
|
+
else
|
86
|
+
puts "Process (#{pid}) not suspended? #{status.stopsig}" if ENV["DEBUG"]
|
87
|
+
# if a signal killed or stopped the process (such as SIGINT or SIGTSTP) $? is nil.
|
88
|
+
exitstatus = $? ? $?.exitstatus : nil
|
89
|
+
result = Yap::Shell::Execution::Result.new(status_code:exitstatus, directory:Dir.pwd, n:n, of:of)
|
90
|
+
end
|
77
91
|
end
|
78
92
|
|
79
93
|
def resume
|
data/lib/yap/shell/repl.rb
CHANGED
@@ -27,12 +27,6 @@ module Yap::Shell
|
|
27
27
|
rescue Interrupt
|
28
28
|
puts "^C"
|
29
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
30
|
end
|
37
31
|
end
|
38
32
|
end
|
data/lib/yap/shell/version.rb
CHANGED
data/lib/yap/world.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
require 'term/ansicolor'
|
2
2
|
require 'forwardable'
|
3
|
+
require 'yap/shell/execution'
|
3
4
|
|
4
5
|
module Yap
|
5
6
|
class World
|
@@ -14,7 +15,7 @@ module Yap
|
|
14
15
|
end
|
15
16
|
|
16
17
|
addons.each do |addon|
|
17
|
-
self
|
18
|
+
addon.initialize_world(self)
|
18
19
|
end
|
19
20
|
end
|
20
21
|
|
data/rcfiles/.yaprc
CHANGED
@@ -1,7 +1,10 @@
|
|
1
1
|
#!/usr/bin/ruby
|
2
2
|
|
3
|
-
|
4
|
-
|
3
|
+
ENV["PATH"] = "/Applications/Postgres.app/Contents/MacOS/bin:/usr/local/share/npm/bin/:/usr/local/heroku/bin:/Users/zdennis/.bin:/Users/zdennis/.rvm/gems/ruby-2.1.5/bin:/Users/zdennis/.rvm/gems/ruby-2.1.5@global/bin:/Users/zdennis/.rvm/rubies/ruby-2.1.5/bin:/usr/local/bin:/usr/local/sbin:/Users/zdennis/bin:/bin:/opt/local/bin:/opt/local/sbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/opt/X11/bin:/usr/local/CrossPack-AVR/bin:/private/tmp/.tidbits/bin:/Users/zdennis/source/playground/AdobeAir/AdobeAIRSDK/bin:/Users/zdennis/.rvm/bin:/Users/zdennis/Downloads/adt-bundle-mac-x86_64-20130219/sdk/tools/:/Users/zdennis/.rvm/bin"
|
4
|
+
ENV["GEM_HOME"] = "/Users/zdennis/.rvm/gems/ruby-2.1.5:/Users/zdennis/.rvm/gems/ruby-2.1.5@global"
|
5
|
+
|
6
|
+
# require 'chronic'
|
7
|
+
# require 'term/ansicolor'
|
5
8
|
|
6
9
|
#
|
7
10
|
# Configuring your prompt. This can be set to a static value or to a
|
@@ -33,151 +36,8 @@ self.prompt = -> do
|
|
33
36
|
"#{dark(green('£'))} #{yellow(pwd)} #{git_branch}#{red(arrow)} "
|
34
37
|
end
|
35
38
|
|
36
|
-
func :howmuch do |args:, stdin:, stdout:, stderr:|
|
37
|
-
case args.first
|
38
|
-
when "time"
|
39
|
-
if history_item=CommandHistory.last_run_command
|
40
|
-
stdout.puts history_item.total_time_s
|
41
|
-
else
|
42
|
-
stdout.puts "Can't report on something you haven't done."
|
43
|
-
end
|
44
|
-
else
|
45
|
-
stdout.puts "How much what?"
|
46
|
-
end
|
47
|
-
end
|
48
39
|
|
49
40
|
func :upcase do |args:, stdin:, stdout:, stderr:|
|
50
41
|
str = stdin.read
|
51
42
|
stdout.puts str.upcase
|
52
43
|
end
|
53
|
-
|
54
|
-
class CommandHistoryImplementation
|
55
|
-
def initialize
|
56
|
-
@history = []
|
57
|
-
end
|
58
|
-
|
59
|
-
def start_group(time)
|
60
|
-
@group = Group.new(started_at:time)
|
61
|
-
@history.push @group
|
62
|
-
end
|
63
|
-
|
64
|
-
def stop_group(time)
|
65
|
-
@group.stopped_at(time)
|
66
|
-
end
|
67
|
-
|
68
|
-
def push(item)
|
69
|
-
@group.add_item item
|
70
|
-
end
|
71
|
-
|
72
|
-
def last_group
|
73
|
-
@history.last
|
74
|
-
end
|
75
|
-
|
76
|
-
def last_command
|
77
|
-
return @history.last.last_item if @history.last
|
78
|
-
nil
|
79
|
-
end
|
80
|
-
|
81
|
-
def last_run_command
|
82
|
-
@history.reverse.each do |group|
|
83
|
-
last_run = group.items.reverse.detect{ |item| item.finished? }
|
84
|
-
break last_run if last_run
|
85
|
-
end
|
86
|
-
end
|
87
|
-
|
88
|
-
class Group
|
89
|
-
def initialize(started_at:Time.now)
|
90
|
-
@started_at = started_at
|
91
|
-
@items = []
|
92
|
-
end
|
93
|
-
|
94
|
-
def add_item(item)
|
95
|
-
@items.push item
|
96
|
-
end
|
97
|
-
|
98
|
-
def items
|
99
|
-
@items
|
100
|
-
end
|
101
|
-
|
102
|
-
def last_item
|
103
|
-
@items.last
|
104
|
-
end
|
105
|
-
|
106
|
-
def stopped_at(time)
|
107
|
-
@stopped_at ||= time
|
108
|
-
end
|
109
|
-
|
110
|
-
def duration
|
111
|
-
return nil unless @stopped_at
|
112
|
-
@stopped_at - @started_at
|
113
|
-
end
|
114
|
-
end
|
115
|
-
|
116
|
-
class Item
|
117
|
-
def initialize(command_str:command_str, started_at:Time.now)
|
118
|
-
@command_str = command_str
|
119
|
-
@started_at = started_at
|
120
|
-
@ended_at = nil
|
121
|
-
end
|
122
|
-
|
123
|
-
def finished!
|
124
|
-
@ended_at = Time.now
|
125
|
-
end
|
126
|
-
|
127
|
-
def finished?
|
128
|
-
!!@ended_at
|
129
|
-
end
|
130
|
-
|
131
|
-
def total_time_s
|
132
|
-
humanize(@ended_at - @started_at) if @ended_at && @started_at
|
133
|
-
end
|
134
|
-
|
135
|
-
private
|
136
|
-
|
137
|
-
def humanize secs
|
138
|
-
[[60, :seconds], [60, :minutes], [24, :hours], [1000, :days]].inject([]){ |s, (count, name)|
|
139
|
-
if secs > 0
|
140
|
-
secs, n = secs.divmod(count)
|
141
|
-
s.unshift "#{n} #{name}"
|
142
|
-
end
|
143
|
-
s
|
144
|
-
}.join(' ')
|
145
|
-
end
|
146
|
-
end
|
147
|
-
|
148
|
-
end
|
149
|
-
|
150
|
-
CommandHistory = CommandHistoryImplementation.new
|
151
|
-
|
152
|
-
Yap::Shell::Execution::Context.on(:before_statements_execute) do |context|
|
153
|
-
puts "Before group: #{context.to_s}" if ENV["DEBUG"]
|
154
|
-
CommandHistory.start_group(Time.now)
|
155
|
-
end
|
156
|
-
|
157
|
-
Yap::Shell::Execution::Context.on(:after_statements_execute) do |context|
|
158
|
-
CommandHistory.stop_group(Time.now)
|
159
|
-
puts "After group: #{context.to_s}" if ENV["DEBUG"]
|
160
|
-
end
|
161
|
-
|
162
|
-
Yap::Shell::Execution::Context.on(:after_process_finished) do |context, *args|
|
163
|
-
# puts "After process: #{context.to_s}, args: #{args.inspect}"
|
164
|
-
end
|
165
|
-
|
166
|
-
Yap::Shell::Execution::Context.on(:before_execute) do |context, command:|
|
167
|
-
CommandHistory.push CommandHistoryImplementation::Item.new(command_str: command.str, started_at: Time.now)
|
168
|
-
end
|
169
|
-
|
170
|
-
Yap::Shell::Execution::Context.on(:after_execute) do |context, command:, result:|
|
171
|
-
CommandHistory.last_command.finished!
|
172
|
-
# if result.status_code == 0
|
173
|
-
# # t = TermInfo.new("xterm-color", STDOUT)
|
174
|
-
# # h, w = t.screen_size
|
175
|
-
# # t.control "cub", w
|
176
|
-
# # # msg =
|
177
|
-
# # t.control "cuf"
|
178
|
-
# # # t.control "home"
|
179
|
-
# #
|
180
|
-
# # # t.write "hi"
|
181
|
-
# # # t.control "rc"
|
182
|
-
# end
|
183
|
-
end
|
data/scripts/4
ADDED
data/scripts/bg-vim
ADDED
data/scripts/fail
ADDED
data/scripts/letters
ADDED
data/scripts/pass
ADDED
data/yap-shell.gemspec
CHANGED
@@ -18,7 +18,7 @@ Gem::Specification.new do |spec|
|
|
18
18
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
19
|
spec.require_paths = ["lib"]
|
20
20
|
|
21
|
-
spec.add_dependency "yap-shell-parser", "~> 0.0
|
21
|
+
spec.add_dependency "yap-shell-parser", "~> 0.1.0"
|
22
22
|
spec.add_dependency "term-ansicolor", "~> 1.3"
|
23
23
|
spec.add_dependency "chronic", "~> 0.10"
|
24
24
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: yap-shell
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Zach Dennis
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-03-
|
11
|
+
date: 2015-03-14 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: yap-shell-parser
|
@@ -16,14 +16,14 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: 0.0
|
19
|
+
version: 0.1.0
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: 0.0
|
26
|
+
version: 0.1.0
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: term-ansicolor
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -95,6 +95,7 @@ files:
|
|
95
95
|
- README.md
|
96
96
|
- Rakefile
|
97
97
|
- WISHLIST.md
|
98
|
+
- addons/history.rb
|
98
99
|
- bin/yap
|
99
100
|
- lib/tasks/gem.rake
|
100
101
|
- lib/yap.rb
|
@@ -117,6 +118,15 @@ files:
|
|
117
118
|
- lib/yap/shell/version.rb
|
118
119
|
- lib/yap/world.rb
|
119
120
|
- rcfiles/.yaprc
|
121
|
+
- scripts/4
|
122
|
+
- scripts/bg-vim
|
123
|
+
- scripts/fail
|
124
|
+
- scripts/letters
|
125
|
+
- scripts/lots-of-output
|
126
|
+
- scripts/pass
|
127
|
+
- scripts/simulate-long-running
|
128
|
+
- scripts/write-to-stderr.rb
|
129
|
+
- scripts/write-to-stdout.rb
|
120
130
|
- yap-shell.gemspec
|
121
131
|
homepage: https://github.com/zdennis/yap-shell
|
122
132
|
licenses:
|