rash-command-shell 0.2.0 → 0.4.1
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/bin/rash +3 -0
- data/lib/rash.rb +69 -12
- data/lib/rash/aliasing.rb +26 -19
- data/lib/rash/capturing.rb +45 -0
- data/lib/rash/ext/filesystem.rb +117 -20
- data/lib/rash/jobcontrol.rb +1 -3
- data/lib/rash/pipeline.rb +131 -28
- data/lib/rash/redirection.rb +4 -2
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f799801cf8b57a584f090320cbd63d7b2e54dbc0e7c66fbac9943d7b75e35417
|
4
|
+
data.tar.gz: 34b945f7da8b07813f405417c99d091466215b087fb709e71394ad3b551044a0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: baa2bc0af9d01199949370a1fbea1b4682962842bab8618cdbc2c0fe4a3f30352daa3c7661da1989a21ee82b589f937ddd702b5a7ab139ecd791827e8192bdd7
|
7
|
+
data.tar.gz: b05cc5985f22f293f5574057217c33641db8b409b50ecfb13861b332653c4d67e617ed19980abc50af0467934f41cb8e05d50ea51aa0ba1f9f806fb028645d6e
|
data/bin/rash
CHANGED
@@ -2,6 +2,9 @@
|
|
2
2
|
|
3
3
|
if ARGV.empty?
|
4
4
|
exec("irb", "-r", "rash", *ARGV)
|
5
|
+
elsif ARGV[0] =~ /(-)?-v(ersion)?/
|
6
|
+
puts "Rash (c) 2020 Kellen Watt"
|
7
|
+
puts "Version 0.4.1" # I may forget to update this
|
5
8
|
elsif File.exists?(ARGV[0]) && !File.directory?(ARGV[0])
|
6
9
|
require "rash"
|
7
10
|
file = ARGV.shift
|
data/lib/rash.rb
CHANGED
@@ -1,7 +1,6 @@
|
|
1
1
|
class Environment
|
2
2
|
|
3
3
|
attr_reader :aliasing_disabled
|
4
|
-
attr_reader :superuser_mode
|
5
4
|
attr_reader :umask
|
6
5
|
|
7
6
|
def initialize
|
@@ -13,9 +12,24 @@ class Environment
|
|
13
12
|
Dir.chdir(dir.nil? ? "~" : dir.to_s)
|
14
13
|
@working_directory = Dir.pwd
|
15
14
|
ENV["OLDPWD"] = old.to_s
|
15
|
+
ENV["PWD"] = Dir.pwd
|
16
16
|
Dir.pwd
|
17
17
|
end
|
18
|
-
|
18
|
+
|
19
|
+
# Note that this works regardless of which version of chdir is used.
|
20
|
+
def push_dir(dir = nil)
|
21
|
+
@directory_stack.push(Dir.pwd)
|
22
|
+
self.chdir(dir)
|
23
|
+
end
|
24
|
+
|
25
|
+
def pop_dir
|
26
|
+
self.chdir(@directory_stack.pop) if @directory_stack.size > 0
|
27
|
+
end
|
28
|
+
|
29
|
+
def dirs
|
30
|
+
@directory_stack
|
31
|
+
end
|
32
|
+
|
19
33
|
def add_path(path)
|
20
34
|
ENV["PATH"] += File::PATH_SEPARATOR + (path.respond_to?(:path) ? path.path : path.to_s)
|
21
35
|
end
|
@@ -56,6 +70,18 @@ class Environment
|
|
56
70
|
limits.each {|resource, limit| Process.setrlimit(resource, *limit)}
|
57
71
|
end
|
58
72
|
end
|
73
|
+
|
74
|
+
def dispatch(m, *args)
|
75
|
+
if @in_pipeline
|
76
|
+
add_pipeline(m, *args)
|
77
|
+
else
|
78
|
+
system_command(m, *args)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def name?(v)
|
83
|
+
v.kind_of?(String) || v.kind_of?(Symbol)
|
84
|
+
end
|
59
85
|
|
60
86
|
private
|
61
87
|
|
@@ -69,18 +95,38 @@ class Environment
|
|
69
95
|
|
70
96
|
@active_pipelines = []
|
71
97
|
|
98
|
+
@directory_stack = []
|
99
|
+
|
72
100
|
@prompt = {
|
73
101
|
AUTO_INDENT: true,
|
74
102
|
RETURN: ""
|
75
103
|
}
|
76
104
|
ENV["RASHDIR"] = File.dirname(__FILE__)
|
77
105
|
end
|
106
|
+
|
107
|
+
def resolve_command(m, *args, literal: false)
|
108
|
+
(literal ? [m.to_s] : resolve_alias(m)) + args.flatten.map{|a| a.to_s}
|
109
|
+
end
|
110
|
+
|
111
|
+
def system_command(m, *args, except: false, literal: false, out: nil, input: nil, err: nil)
|
112
|
+
command = resolve_command(m, *args, literal: literal)
|
113
|
+
command.unshift("sudo") if @superuser_mode
|
114
|
+
opts = {out: out || $stdout,
|
115
|
+
err: err || $stderr,
|
116
|
+
in: input || $stdin,
|
117
|
+
exception: except || @superuser_mode,
|
118
|
+
umask: @umask}
|
119
|
+
|
120
|
+
system(*command, opts)
|
121
|
+
end
|
122
|
+
|
78
123
|
end
|
79
124
|
|
80
125
|
require_relative "rash/redirection"
|
81
126
|
require_relative "rash/aliasing"
|
82
127
|
require_relative "rash/jobcontrol"
|
83
128
|
require_relative "rash/pipeline"
|
129
|
+
require_relative "rash/capturing"
|
84
130
|
|
85
131
|
$env = Environment.new
|
86
132
|
|
@@ -96,6 +142,17 @@ def cd(dir = nil)
|
|
96
142
|
$env.chdir(d)
|
97
143
|
end
|
98
144
|
|
145
|
+
def pushd(dir = nil)
|
146
|
+
case dir
|
147
|
+
when File, Dir
|
148
|
+
dir = dir.path if File.directory(dir.path)
|
149
|
+
end
|
150
|
+
$env.push_dir(dir)
|
151
|
+
end
|
152
|
+
|
153
|
+
def popd
|
154
|
+
$env.pop_dir
|
155
|
+
end
|
99
156
|
|
100
157
|
def run(file, *args)
|
101
158
|
filename = file.to_s
|
@@ -103,13 +160,15 @@ def run(file, *args)
|
|
103
160
|
unless File.executable?(exe)
|
104
161
|
raise SystemCallError.new("No such executable file - #{exe}", Errno::ENOENT::Errno)
|
105
162
|
end
|
106
|
-
|
163
|
+
$env.dispatch(exe, *args, literal: true)
|
107
164
|
end
|
108
165
|
|
109
166
|
alias cmd __send__
|
110
167
|
|
111
168
|
# Defines `bash` psuedo-compatibility. Filesystem effects happen like normal
|
112
169
|
# and environmental variable changes are copied
|
170
|
+
#
|
171
|
+
# This is an artifact of an old design and is deprecated until further notice.
|
113
172
|
def sourcesh(file)
|
114
173
|
bash_env = lambda do |cmd = nil|
|
115
174
|
tmpenv = `#{cmd + ';' if cmd} printenv`
|
@@ -124,7 +183,7 @@ end
|
|
124
183
|
|
125
184
|
|
126
185
|
def which(command)
|
127
|
-
cmd = File.expand_path(command)
|
186
|
+
cmd = File.expand_path(command.to_s)
|
128
187
|
return cmd if File.executable?(cmd) && !File.directory?(cmd)
|
129
188
|
|
130
189
|
exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : ['']
|
@@ -138,20 +197,18 @@ def which(command)
|
|
138
197
|
nil
|
139
198
|
end
|
140
199
|
|
200
|
+
# This breaks some default IRB functionality
|
201
|
+
# def self.respond_to_missing?(m, *args)
|
202
|
+
#
|
203
|
+
# which(m.to_s) || ($env.alias?(m) && !$env.aliasing_disabled) || $env.local_method?(m) || super
|
204
|
+
# end
|
141
205
|
|
142
206
|
# Note that I defy convention and don't define `respond_to_missing?`. This
|
143
207
|
# is because doing so screws with irb.
|
144
|
-
# This code is a nightmarish monstrosity. I need some kind of "dispatch" method on Environment
|
145
208
|
def self.method_missing(m, *args, &block)
|
146
209
|
exe = which(m.to_s)
|
147
210
|
if exe || ($env.alias?(m) && !$env.aliasing_disabled)
|
148
|
-
|
149
|
-
system("sudo", *$env.resolve_alias(m), *args.flatten.map{|a| a.to_s}, {out: $stdout, err: $stderr, in: $stdin, exception: true, umask: $env.umask})
|
150
|
-
elsif $env.pipelined? # implicitly disallowing superuser_mode for now. Need to refactor to allow
|
151
|
-
$env.add_pipeline(m, *args)
|
152
|
-
else
|
153
|
-
system(*$env.resolve_alias(m), *args.flatten.map{|a| a.to_s}, {out: $stdout, err: $stderr, in: $stdin, umask: $env.umask})
|
154
|
-
end
|
211
|
+
$env.dispatch(m, *args)
|
155
212
|
else
|
156
213
|
super
|
157
214
|
end
|
data/lib/rash/aliasing.rb
CHANGED
@@ -7,26 +7,12 @@ class Environment
|
|
7
7
|
@aliases.delete(func.to_sym)
|
8
8
|
end
|
9
9
|
|
10
|
-
# recursive aliases not currently possible. In the works
|
11
|
-
def resolve_alias(f)
|
12
|
-
result = [f.to_s]
|
13
|
-
aliases = @aliases.dup
|
14
|
-
found_alias = true
|
15
|
-
while found_alias
|
16
|
-
found_alias = false
|
17
|
-
if aliases.has_key?(result[0].to_sym)
|
18
|
-
found_alias = true
|
19
|
-
match = result[0].to_sym
|
20
|
-
result[0] = aliases[match]
|
21
|
-
aliases.delete(match)
|
22
|
-
result.flatten!
|
23
|
-
end
|
24
|
-
end
|
25
|
-
result
|
26
|
-
end
|
27
|
-
|
28
10
|
def alias?(f)
|
29
|
-
@aliases.
|
11
|
+
@aliases.key?(f.to_sym)
|
12
|
+
end
|
13
|
+
|
14
|
+
def aliases
|
15
|
+
@aliases.dup
|
30
16
|
end
|
31
17
|
|
32
18
|
def without_aliasing
|
@@ -52,4 +38,25 @@ class Environment
|
|
52
38
|
end
|
53
39
|
end
|
54
40
|
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
# Unless given a compelling reason, this doesn't need to be public. For most
|
45
|
+
# purposes, some combination of `alias?` and `aliases` should be sufficient.
|
46
|
+
def resolve_alias(f)
|
47
|
+
result = [f.to_s]
|
48
|
+
aliases = @aliases.dup
|
49
|
+
found_alias = true
|
50
|
+
while found_alias
|
51
|
+
found_alias = false
|
52
|
+
if aliases.has_key?(result[0].to_sym)
|
53
|
+
found_alias = true
|
54
|
+
match = result[0].to_sym
|
55
|
+
result[0] = aliases[match]
|
56
|
+
aliases.delete(match)
|
57
|
+
result.flatten!
|
58
|
+
end
|
59
|
+
end
|
60
|
+
result
|
61
|
+
end
|
55
62
|
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
class Environment
|
2
|
+
def capture_block(&block)
|
3
|
+
raise ArgumentError.new("no block provided") unless block_given?
|
4
|
+
result = nil
|
5
|
+
old_pipeline = @in_pipeline
|
6
|
+
begin
|
7
|
+
reader, writer = IO.pipe
|
8
|
+
self.stdout = writer
|
9
|
+
@in_pipeline = false
|
10
|
+
block.call
|
11
|
+
ensure
|
12
|
+
@in_pipeline = old_pipeline
|
13
|
+
reset_stdout
|
14
|
+
writer.close
|
15
|
+
result = reader.read
|
16
|
+
reader.close
|
17
|
+
end
|
18
|
+
result
|
19
|
+
end
|
20
|
+
|
21
|
+
def capture_command(m, *args)
|
22
|
+
raise NameError.new("no such command", m) unless which(m) || ($env.alias?(m) && !$env.aliasing_disabled)
|
23
|
+
result = nil
|
24
|
+
begin
|
25
|
+
reader, writer = IO.pipe
|
26
|
+
system_command(m, *args, out: writer)
|
27
|
+
ensure
|
28
|
+
writer.close
|
29
|
+
result = reader.read
|
30
|
+
reader.close
|
31
|
+
end
|
32
|
+
result
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# This explicitly doesn't support pipelining, as the output is ripped out of sequence.
|
37
|
+
def capture(*cmd, &block)
|
38
|
+
if block_given?
|
39
|
+
$env.capture_block(&block)
|
40
|
+
elsif cmd.size > 0 && (which(cmd[0]) || ($env.alias?(m) && !$env.aliasing_disabled))
|
41
|
+
$env.capture_command(cmd[0].to_s, *cmd[1..])
|
42
|
+
else
|
43
|
+
raise ArgumentError.new("nothing to capture from")
|
44
|
+
end
|
45
|
+
end
|
data/lib/rash/ext/filesystem.rb
CHANGED
@@ -2,38 +2,63 @@ class Environment
|
|
2
2
|
|
3
3
|
RASH_LOCAL_FILE = ".rashrc.local"
|
4
4
|
|
5
|
+
# Has to be a better way of adding extensions and dynamically loading them
|
5
6
|
def initialize
|
6
7
|
common_init
|
7
8
|
@working_directory = Directory.root("/")
|
8
|
-
traverse_filetree("/", Dir.pwd)
|
9
9
|
end
|
10
10
|
|
11
11
|
def chdir(dir = nil)
|
12
12
|
old = @working_directory
|
13
|
-
traverse_filetree(
|
13
|
+
traverse_filetree(@working_directory.to_s, (dir.nil? ? "~" : dir.to_s))
|
14
14
|
ENV["OLDPWD"] = old.to_s
|
15
|
+
ENV["PWD"] = Dir.pwd
|
15
16
|
Dir.pwd
|
16
17
|
end
|
17
18
|
|
18
|
-
def local_def(name, &block)
|
19
|
-
@working_directory.add_local_method(name
|
19
|
+
def local_def(name, locked: false, &block)
|
20
|
+
@working_directory.add_local_method(name, &block)
|
21
|
+
@working_directory.lock_method(name) if locked
|
22
|
+
name.to_sym
|
20
23
|
end
|
21
24
|
|
22
25
|
def local_undef(name)
|
23
|
-
@working_directory.clear_local_method(name
|
26
|
+
@working_directory.clear_local_method(name)
|
24
27
|
end
|
25
28
|
|
26
29
|
def local_method?(name)
|
27
|
-
@working_directory.
|
30
|
+
@working_directory.local_method?(name)
|
31
|
+
end
|
32
|
+
|
33
|
+
def local_methods
|
34
|
+
@working_directory.local_methods
|
28
35
|
end
|
29
36
|
|
30
37
|
def local_call(name, *args, &block)
|
31
|
-
@working_directory.
|
38
|
+
@working_directory.local_method(name).call(*args, &block)
|
32
39
|
end
|
33
40
|
|
34
|
-
|
41
|
+
def local_var(name, v = nil, locked: false)
|
42
|
+
res = nil
|
43
|
+
if v.nil?
|
44
|
+
res = @working_directory.local_variable(name)
|
45
|
+
else
|
46
|
+
@working_directory.set_local_variable(name, v)
|
47
|
+
res = v
|
48
|
+
end
|
49
|
+
@working_directory.lock_variable(name) if locked
|
50
|
+
res
|
51
|
+
end
|
52
|
+
|
53
|
+
def local_var?(name)
|
54
|
+
@working_directory.local_variable?(name)
|
55
|
+
end
|
56
|
+
|
57
|
+
def local_vars
|
58
|
+
@working_directory.local_variables
|
59
|
+
end
|
35
60
|
|
36
|
-
|
61
|
+
private
|
37
62
|
|
38
63
|
# from and to are strings
|
39
64
|
def traverse_filetree(from, to)
|
@@ -66,7 +91,6 @@ class Environment
|
|
66
91
|
end
|
67
92
|
|
68
93
|
class Directory
|
69
|
-
attr_reader :local_methods
|
70
94
|
attr_reader :parent, :children
|
71
95
|
|
72
96
|
def self.root(dir)
|
@@ -77,7 +101,82 @@ class Environment
|
|
77
101
|
@path = Dir.new(dir)
|
78
102
|
@parent = parent
|
79
103
|
@children = []
|
80
|
-
@local_methods = parent&.
|
104
|
+
@local_methods = parent&.unlocked_local_methods || {}
|
105
|
+
@locked_methods = []
|
106
|
+
@local_variables = {}
|
107
|
+
@locked_variables = []
|
108
|
+
end
|
109
|
+
|
110
|
+
def local_method(name)
|
111
|
+
@local_methods[name.to_sym]
|
112
|
+
end
|
113
|
+
|
114
|
+
def local_methods
|
115
|
+
@local_methods.keys
|
116
|
+
end
|
117
|
+
|
118
|
+
def unlocked_local_methods
|
119
|
+
@local_methods.filter{|k, v| !@locked_methods.include?(k)}
|
120
|
+
end
|
121
|
+
|
122
|
+
def lock_method(name)
|
123
|
+
n = name.to_sym
|
124
|
+
raise NameError.new("#{name} is not a local method", n) unless @local_methods.key?(n)
|
125
|
+
@locked_methods << n unless @locked_methods.include?(n)
|
126
|
+
n
|
127
|
+
end
|
128
|
+
|
129
|
+
def local_method?(name)
|
130
|
+
@local_methods.key?(name.to_sym)
|
131
|
+
end
|
132
|
+
|
133
|
+
def local_variable(name)
|
134
|
+
name = name.to_sym
|
135
|
+
@local_variables[name] || @parent&.unlocked_local_variable(name)
|
136
|
+
end
|
137
|
+
|
138
|
+
def local_variable?(name)
|
139
|
+
@local_variables.include?(name.to_sym) || !!@parent&.unlocked_local_variable?(name.to_sym)
|
140
|
+
end
|
141
|
+
|
142
|
+
def local_variables
|
143
|
+
@local_variables.keys + (@parent&.unlocked_local_variables).to_a
|
144
|
+
end
|
145
|
+
|
146
|
+
def set_local_variable(name, value)
|
147
|
+
name = name.to_sym
|
148
|
+
if !@local_variables.key?(name) && @parent&.unlocked_local_variable?(name)
|
149
|
+
@parent&.set_local_variable(name, value)
|
150
|
+
else
|
151
|
+
@local_variables[name] = value
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
def unlocked_local_variables
|
156
|
+
@local_variables.keys.filter{|k| !@locked_variables.include?(k)} + (@parent&.unlocked_local_variables).to_a
|
157
|
+
end
|
158
|
+
|
159
|
+
def unlocked_local_variable(name)
|
160
|
+
name = name.to_sym
|
161
|
+
@local_variables.filter{|k| !@locked_variables.include?(k)}[name] || @parent&.unlocked_local_variable(name)
|
162
|
+
end
|
163
|
+
|
164
|
+
def unlocked_local_variable?(name)
|
165
|
+
name = name.to_sym
|
166
|
+
@local_variables.filter{|k,_v| !@locked_variables.include?(k)}.key?(name) ||
|
167
|
+
!!@parent&.unlocked_local_variable?(name)
|
168
|
+
end
|
169
|
+
|
170
|
+
def lock_variable(name)
|
171
|
+
n = name.to_sym
|
172
|
+
raise NameError.new("#{name} is not a local variable", n) unless @local_variables.key?(n)
|
173
|
+
@locked_variables << n unless @locked_variables.include?(n)
|
174
|
+
n
|
175
|
+
end
|
176
|
+
|
177
|
+
def clear_local_variable(name)
|
178
|
+
@local_variables.delete(name.to_sym)
|
179
|
+
name.to_sym
|
81
180
|
end
|
82
181
|
|
83
182
|
def root?
|
@@ -104,14 +203,14 @@ class Environment
|
|
104
203
|
|
105
204
|
def add_local_method(name, &block)
|
106
205
|
raise ArgumentError.new "no method body provided" unless block_given?
|
107
|
-
@local_methods[name] = block # if name already exists, its function is overriden
|
108
|
-
name
|
206
|
+
@local_methods[name.to_sym] = block # if name already exists, its function is overriden
|
207
|
+
name.to_sym
|
109
208
|
end
|
110
209
|
|
111
210
|
# might not be useful
|
112
211
|
def clear_local_method(name)
|
113
|
-
@local_methods.delete(name)
|
114
|
-
name
|
212
|
+
@local_methods.delete(name.to_sym)
|
213
|
+
name.to_sym
|
115
214
|
end
|
116
215
|
|
117
216
|
def to_s
|
@@ -120,19 +219,17 @@ class Environment
|
|
120
219
|
end
|
121
220
|
end
|
122
221
|
|
222
|
+
# still could absolutely be more cleaned up, but it works
|
123
223
|
def self.method_missing(m, *args, &block)
|
124
224
|
exe = which(m.to_s)
|
125
225
|
if $env.local_method?(m)
|
126
226
|
$env.local_call(m, *args, &block)
|
127
227
|
elsif exe || ($env.alias?(m) && !$env.aliasing_disabled)
|
128
|
-
|
129
|
-
system("sudo", *$env.resolve_alias(m), *args.flatten.map{|a| a.to_s}, {out: $stdout, err: $stderr, in: $stdin, exception: true, umask: $env.umask})
|
130
|
-
else
|
131
|
-
system(*$env.resolve_alias(m), *args.flatten.map{|a| a.to_s}, {out: $stdout, err: $stderr, in: $stdin, umask: $env.umask})
|
132
|
-
end
|
228
|
+
$env.dispatch(m, *args)
|
133
229
|
else
|
134
230
|
super
|
135
231
|
end
|
136
232
|
end
|
137
233
|
|
138
234
|
$env = Environment.new
|
235
|
+
$env.chdir(Dir.pwd)
|
data/lib/rash/jobcontrol.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
class Environment
|
2
2
|
def jobs
|
3
|
-
@active_jobs.keep_if
|
3
|
+
@active_jobs.keep_if{|pid| Process.kill(0, pid) rescue false}.dup
|
4
4
|
end
|
5
5
|
|
6
6
|
def async(&block)
|
@@ -17,5 +17,3 @@ end
|
|
17
17
|
def as_background(&block)
|
18
18
|
$env.async(&block)
|
19
19
|
end
|
20
|
-
|
21
|
-
|
data/lib/rash/pipeline.rb
CHANGED
@@ -4,47 +4,137 @@ class Environment
|
|
4
4
|
@in_pipeline
|
5
5
|
end
|
6
6
|
|
7
|
-
def
|
8
|
-
@in_pipeline
|
7
|
+
def synced_pipeline?
|
8
|
+
@in_pipeline && @synchronous_pipeline
|
9
9
|
end
|
10
10
|
|
11
|
-
def
|
12
|
-
@in_pipeline
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
@active_pipelines.reverse_each {|pipe| pipe.terminate}
|
19
|
-
@active_pipelines.clear
|
11
|
+
def make_pipeline(&block)
|
12
|
+
raise IOError.new("pipelining already enabled") if @in_pipeline
|
13
|
+
start_pipeline
|
14
|
+
begin
|
15
|
+
block.call
|
16
|
+
ensure
|
17
|
+
end_pipeline
|
20
18
|
end
|
19
|
+
nil
|
21
20
|
end
|
22
21
|
|
23
|
-
def
|
22
|
+
def make_sync_pipeline(&block)
|
23
|
+
raise IOError.new("pipelining already enabled") if @in_pipeline
|
24
|
+
start_sync_pipeline
|
25
|
+
begin
|
26
|
+
block.call
|
27
|
+
ensure
|
28
|
+
end_sync_pipeline
|
29
|
+
end
|
30
|
+
nil
|
31
|
+
end
|
32
|
+
|
33
|
+
def as_pipe_command(&block)
|
34
|
+
raise IOError.new("pipelining not enabled") unless @in_pipeline
|
35
|
+
return as_sync_pipe_command(&block) if @synchronous_pipeline
|
36
|
+
|
24
37
|
input = (@active_pipelines.empty? ? $stdin : @active_pipelines.last.reader)
|
25
38
|
@active_pipelines << Pipeline.new
|
26
39
|
output = @active_pipelines.last.writer
|
27
40
|
error = ($stderr == $stdout ? output : $stderr)
|
28
|
-
|
29
|
-
|
41
|
+
|
42
|
+
pid = fork do
|
43
|
+
@in_pipeline = false
|
44
|
+
$stdin = input
|
45
|
+
$stdout = output
|
46
|
+
$stderr = error
|
47
|
+
block.call
|
30
48
|
output.close
|
31
49
|
exit!(true)
|
32
50
|
end
|
33
51
|
output.close
|
52
|
+
|
34
53
|
@active_pipelines.last.link_process(pid)
|
54
|
+
nil
|
35
55
|
end
|
36
56
|
|
37
|
-
def
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
57
|
+
def as_sync_pipe_command(&block)
|
58
|
+
raise IOError.new("pipelining not enabled") unless @in_pipeline
|
59
|
+
raise IOError.new("pipeline is not synchronous") unless @synchronous_pipeline
|
60
|
+
|
61
|
+
@next_pipe.close
|
62
|
+
@next_pipe = Pipeline.new # flush the output pipe
|
63
|
+
@prev_pipe.writer.close
|
64
|
+
|
65
|
+
input = (@first_sync_command ? $stdin : @prev_pipe.reader)
|
66
|
+
@first_sync_command = false
|
67
|
+
output = @next_pipe.writer
|
68
|
+
error = ($stderr == $stdout ? @next_pipe.writer : $stdin)
|
69
|
+
|
42
70
|
pid = fork do
|
43
71
|
@in_pipeline = false
|
72
|
+
@synchronous_pipeline = false
|
44
73
|
$stdin = input
|
45
74
|
$stdout = output
|
46
75
|
$stderr = error
|
47
76
|
block.call
|
77
|
+
exit!(true)
|
78
|
+
end
|
79
|
+
|
80
|
+
Process.wait(pid)
|
81
|
+
@prev_pipe, @next_pipe = @next_pipe, @prev_pipe
|
82
|
+
nil
|
83
|
+
end
|
84
|
+
|
85
|
+
private
|
86
|
+
|
87
|
+
def start_pipeline
|
88
|
+
@in_pipeline = true
|
89
|
+
end
|
90
|
+
|
91
|
+
def start_sync_pipeline
|
92
|
+
@in_pipeline = true
|
93
|
+
@synchronous_pipeline = true
|
94
|
+
@first_sync_command = true
|
95
|
+
@prev_pipe = Pipeline.new
|
96
|
+
@next_pipe = Pipeline.new
|
97
|
+
end
|
98
|
+
|
99
|
+
def end_pipeline
|
100
|
+
raise IOError.new("pipelining not enabled") unless @in_pipeline
|
101
|
+
@in_pipeline = false
|
102
|
+
if @active_pipelines.size > 0
|
103
|
+
begin
|
104
|
+
Process.wait(@active_pipelines.last.pid)
|
105
|
+
@active_pipelines.last.writer.close # probably redundant, but leaving it for now
|
106
|
+
IO.copy_stream(@active_pipelines.last.reader, $stdout)
|
107
|
+
@active_pipelines.pop.close
|
108
|
+
@active_pipelines.reverse_each {|pipe| pipe.terminate}
|
109
|
+
ensure
|
110
|
+
@active_pipelines.clear
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def end_sync_pipeline
|
116
|
+
raise IOError.new("pipelining not enabled") unless @in_pipeline
|
117
|
+
raise IOError.new("pipeline is not synchronous") unless @synchronous_pipeline
|
118
|
+
@next_pipe.close
|
119
|
+
@prev_pipe.writer.close
|
120
|
+
IO.copy_stream(@prev_pipe.reader, $stdout)
|
121
|
+
@prev_pipe.close
|
122
|
+
|
123
|
+
@next_pipe = @prev_pipe = @first_sync_command = nil
|
124
|
+
@synchronous_pipeline = @in_pipeline = false
|
125
|
+
end
|
126
|
+
|
127
|
+
# special method to be referenced from Environment#dispatch. Do not use directly
|
128
|
+
def add_pipeline(m, *args)
|
129
|
+
raise IOError.new("pipelining not enabled") unless @in_pipeline
|
130
|
+
return add_sync_pipeline(m, *args) if @synchronous_pipeline
|
131
|
+
|
132
|
+
input = (@active_pipelines.empty? ? $stdin : @active_pipelines.last.reader)
|
133
|
+
@active_pipelines << Pipeline.new
|
134
|
+
output = @active_pipelines.last.writer
|
135
|
+
error = ($stderr == $stdout ? output : $stderr)
|
136
|
+
pid = fork do # might not be necessary, spawn might cover it. Not risking it before testing
|
137
|
+
system_command(m, *args, out: output, input: input, err: error, except: true)
|
48
138
|
output.close
|
49
139
|
exit!(true)
|
50
140
|
end
|
@@ -52,7 +142,22 @@ class Environment
|
|
52
142
|
@active_pipelines.last.link_process(pid)
|
53
143
|
end
|
54
144
|
|
55
|
-
|
145
|
+
def add_sync_pipeline(m, *args)
|
146
|
+
raise IOError.new("pipelining not enabled") unless @in_pipeline
|
147
|
+
raise IOError.new("pipeline is not synchronous") unless @synchronous_pipeline
|
148
|
+
|
149
|
+
# Ensure pipe is empty for writing
|
150
|
+
@next_pipe.close
|
151
|
+
@next_pipe = Pipeline.new
|
152
|
+
@prev_pipe.writer.close
|
153
|
+
|
154
|
+
input = (@first_sync_command ? $stdin : @prev_pipe.reader)
|
155
|
+
@first_sync_command = false
|
156
|
+
error = ($stderr == $stdout ? @next_pipe.writer : $stdin)
|
157
|
+
system_command(m, *args, out: @next_pipe.writer, input: input, err: error, except: true)
|
158
|
+
@prev_pipe, @next_pipe = @next_pipe, @prev_pipe
|
159
|
+
nil
|
160
|
+
end
|
56
161
|
|
57
162
|
class Pipeline
|
58
163
|
attr_reader :writer, :reader, :pid
|
@@ -73,7 +178,7 @@ class Environment
|
|
73
178
|
|
74
179
|
def terminate
|
75
180
|
self.close
|
76
|
-
Process.kill(:
|
181
|
+
Process.kill(:TERM, @pid)
|
77
182
|
Process.wait(@pid)
|
78
183
|
end
|
79
184
|
|
@@ -84,12 +189,10 @@ class Environment
|
|
84
189
|
end
|
85
190
|
|
86
191
|
|
87
|
-
def in_pipeline(&block)
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
block
|
92
|
-
ensure
|
93
|
-
$env.end_pipeline
|
192
|
+
def in_pipeline(async: true, &block)
|
193
|
+
if async
|
194
|
+
$env.make_pipeline(&block)
|
195
|
+
else
|
196
|
+
$env.make_sync_pipeline(&block)
|
94
197
|
end
|
95
198
|
end
|
data/lib/rash/redirection.rb
CHANGED
@@ -1,7 +1,5 @@
|
|
1
1
|
class Environment
|
2
2
|
|
3
|
-
DEFAULT_IO = {in: STDIN, out: STDOUT, err: STDERR}
|
4
|
-
|
5
3
|
def reset_io
|
6
4
|
reset_stdout
|
7
5
|
reset_stderr
|
@@ -78,6 +76,10 @@ class Environment
|
|
78
76
|
def standard_stream?(f)
|
79
77
|
DEFAULT_IO.values.include?(f)
|
80
78
|
end
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
DEFAULT_IO = {in: STDIN, out: STDOUT, err: STDERR}
|
81
83
|
end
|
82
84
|
|
83
85
|
# If you want to append, you need to get the file object yourself.
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rash-command-shell
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.4.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Kellen Watt
|
@@ -24,7 +24,7 @@ dependencies:
|
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '1.2'
|
27
|
-
description: A Ruby-based shell
|
27
|
+
description: A Ruby-based command shell
|
28
28
|
email:
|
29
29
|
executables:
|
30
30
|
- rash
|
@@ -34,6 +34,7 @@ files:
|
|
34
34
|
- bin/rash
|
35
35
|
- lib/rash.rb
|
36
36
|
- lib/rash/aliasing.rb
|
37
|
+
- lib/rash/capturing.rb
|
37
38
|
- lib/rash/ext/filesystem.rb
|
38
39
|
- lib/rash/jobcontrol.rb
|
39
40
|
- lib/rash/pipeline.rb
|