rash-command-shell 0.1.3 → 0.4.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/bin/rash +3 -0
- data/lib/rash.rb +82 -9
- data/lib/rash/aliasing.rb +26 -19
- data/lib/rash/capturing.rb +45 -0
- data/lib/rash/ext/filesystem.rb +43 -18
- data/lib/rash/jobcontrol.rb +1 -3
- data/lib/rash/pipeline.rb +198 -0
- data/lib/rash/redirection.rb +4 -2
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 45f54bfa6b60f6a2a35a32d087ad1a722fb88f36fcd46933ae7145ee9dfc944d
|
4
|
+
data.tar.gz: 4dd605ed6fcbfb8a5fbacbd4ff11075933e3e6829785f7605442822f4e5fd508
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 55dde4dead963ce469de63b5c7490224d3d29ca406aafb3ef1d1fd0c2813d3433ba258ed433629fc7983cbe9a399560cf7dee912df871e86cf8c491f3ad83a3b
|
7
|
+
data.tar.gz: 5fe62b4dcc77acb7774f8b06360d4a4768b6b7bd63633886c40fbdbc7e0af41bd4240cbb1c867912ee11eec287e61f19f468ab9b354ac842346e5b46f6a9caea
|
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.0" # 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,7 @@
|
|
1
1
|
class Environment
|
2
2
|
|
3
3
|
attr_reader :aliasing_disabled
|
4
|
-
attr_reader :
|
4
|
+
attr_reader :umask
|
5
5
|
|
6
6
|
def initialize
|
7
7
|
common_init
|
@@ -14,7 +14,21 @@ class Environment
|
|
14
14
|
ENV["OLDPWD"] = old.to_s
|
15
15
|
Dir.pwd
|
16
16
|
end
|
17
|
-
|
17
|
+
|
18
|
+
# Note that this works regardless of which version of chdir is used.
|
19
|
+
def push_dir(dir = nil)
|
20
|
+
@directory_stack.push(Dir.pwd)
|
21
|
+
self.chdir(dir)
|
22
|
+
end
|
23
|
+
|
24
|
+
def pop_dir
|
25
|
+
self.chdir(@directory_stack.pop) if @directory_stack.size > 0
|
26
|
+
end
|
27
|
+
|
28
|
+
def dirs
|
29
|
+
@directory_stack
|
30
|
+
end
|
31
|
+
|
18
32
|
def add_path(path)
|
19
33
|
ENV["PATH"] += File::PATH_SEPARATOR + (path.respond_to?(:path) ? path.path : path.to_s)
|
20
34
|
end
|
@@ -28,6 +42,11 @@ class Environment
|
|
28
42
|
super
|
29
43
|
end
|
30
44
|
end
|
45
|
+
|
46
|
+
def umask=(mask)
|
47
|
+
File.umask(mask)
|
48
|
+
@umask = mask
|
49
|
+
end
|
31
50
|
|
32
51
|
def as_superuser(&block)
|
33
52
|
@superuser_mode = true
|
@@ -37,27 +56,72 @@ class Environment
|
|
37
56
|
@superuser_mode = false
|
38
57
|
end
|
39
58
|
end
|
59
|
+
|
60
|
+
def with_limits(limits, &block)
|
61
|
+
if block_given?
|
62
|
+
pid = fork do
|
63
|
+
limits.each {|resource, limit| Process.setrlimit(resource, *limit)}
|
64
|
+
block.call
|
65
|
+
exit!(true)
|
66
|
+
end
|
67
|
+
Process.wait(pid)
|
68
|
+
else
|
69
|
+
limits.each {|resource, limit| Process.setrlimit(resource, *limit)}
|
70
|
+
end
|
71
|
+
end
|
40
72
|
|
73
|
+
def dispatch(m, *args)
|
74
|
+
if @in_pipeline
|
75
|
+
add_pipeline(m, *args)
|
76
|
+
else
|
77
|
+
system_command(m, *args)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
41
81
|
private
|
42
82
|
|
43
83
|
def common_init
|
44
84
|
@working_directory = Dir.pwd
|
85
|
+
@umask = File.umask
|
45
86
|
|
46
87
|
@aliases = {}
|
47
88
|
@aliasing_disabled = false
|
48
89
|
@active_jobs = []
|
49
90
|
|
91
|
+
@active_pipelines = []
|
92
|
+
|
93
|
+
@directory_stack = []
|
94
|
+
|
50
95
|
@prompt = {
|
51
96
|
AUTO_INDENT: true,
|
52
97
|
RETURN: ""
|
53
98
|
}
|
54
99
|
ENV["RASHDIR"] = File.dirname(__FILE__)
|
55
100
|
end
|
101
|
+
|
102
|
+
def resolve_command(m, *args, literal: false)
|
103
|
+
(literal ? [m.to_s] : resolve_alias(m)) + args.flatten.map{|a| a.to_s}
|
104
|
+
end
|
105
|
+
|
106
|
+
def system_command(m, *args, except: false, literal: false, out: nil, input: nil, err: nil)
|
107
|
+
command = resolve_command(m, *args, literal: literal)
|
108
|
+
command.unshift("sudo") if @superuser_mode
|
109
|
+
opts = {out: out || $stdout,
|
110
|
+
err: err || $stderr,
|
111
|
+
in: input || $stdin,
|
112
|
+
exception: except || @superuser_mode,
|
113
|
+
umask: @umask}
|
114
|
+
|
115
|
+
system(*command, opts)
|
116
|
+
end
|
117
|
+
|
56
118
|
end
|
57
119
|
|
58
120
|
require_relative "rash/redirection"
|
59
121
|
require_relative "rash/aliasing"
|
60
122
|
require_relative "rash/jobcontrol"
|
123
|
+
require_relative "rash/pipeline"
|
124
|
+
require_relative "rash/capturing"
|
61
125
|
|
62
126
|
$env = Environment.new
|
63
127
|
|
@@ -73,6 +137,17 @@ def cd(dir = nil)
|
|
73
137
|
$env.chdir(d)
|
74
138
|
end
|
75
139
|
|
140
|
+
def pushd(dir = nil)
|
141
|
+
case dir
|
142
|
+
when File, Dir
|
143
|
+
dir = dir.path if File.directory(dir.path)
|
144
|
+
end
|
145
|
+
$env.push_dir(dir)
|
146
|
+
end
|
147
|
+
|
148
|
+
def popd
|
149
|
+
$env.pop_dir
|
150
|
+
end
|
76
151
|
|
77
152
|
def run(file, *args)
|
78
153
|
filename = file.to_s
|
@@ -80,13 +155,15 @@ def run(file, *args)
|
|
80
155
|
unless File.executable?(exe)
|
81
156
|
raise SystemCallError.new("No such executable file - #{exe}", Errno::ENOENT::Errno)
|
82
157
|
end
|
83
|
-
|
158
|
+
$env.dispatch(exe, *args, literal: true)
|
84
159
|
end
|
85
160
|
|
86
161
|
alias cmd __send__
|
87
162
|
|
88
163
|
# Defines `bash` psuedo-compatibility. Filesystem effects happen like normal
|
89
164
|
# and environmental variable changes are copied
|
165
|
+
#
|
166
|
+
# This is an artifact of an old design and is deprecated until further notice.
|
90
167
|
def sourcesh(file)
|
91
168
|
bash_env = lambda do |cmd = nil|
|
92
169
|
tmpenv = `#{cmd + ';' if cmd} printenv`
|
@@ -101,7 +178,7 @@ end
|
|
101
178
|
|
102
179
|
|
103
180
|
def which(command)
|
104
|
-
cmd = File.expand_path(command)
|
181
|
+
cmd = File.expand_path(command.to_s)
|
105
182
|
return cmd if File.executable?(cmd) && !File.directory?(cmd)
|
106
183
|
|
107
184
|
exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : ['']
|
@@ -121,11 +198,7 @@ end
|
|
121
198
|
def self.method_missing(m, *args, &block)
|
122
199
|
exe = which(m.to_s)
|
123
200
|
if exe || ($env.alias?(m) && !$env.aliasing_disabled)
|
124
|
-
|
125
|
-
system("sudo", *$env.resolve_alias(m), *args.flatten.map{|a| a.to_s}, {out: $stdout, err: $stderr, in: $stdin, exception: true})
|
126
|
-
else
|
127
|
-
system(*$env.resolve_alias(m), *args.flatten.map{|a| a.to_s}, {out: $stdout, err: $stderr, in: $stdin})
|
128
|
-
end
|
201
|
+
$env.dispatch(m, *args)
|
129
202
|
else
|
130
203
|
super
|
131
204
|
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,6 +2,7 @@ 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("/")
|
@@ -15,26 +16,30 @@ class Environment
|
|
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
|
private
|
35
42
|
|
36
|
-
LOCAL_FUNCTIONS_ENABLED = true
|
37
|
-
|
38
43
|
# from and to are strings
|
39
44
|
def traverse_filetree(from, to)
|
40
45
|
abs_from = File.expand_path(from)
|
@@ -66,7 +71,6 @@ class Environment
|
|
66
71
|
end
|
67
72
|
|
68
73
|
class Directory
|
69
|
-
attr_reader :local_methods
|
70
74
|
attr_reader :parent, :children
|
71
75
|
|
72
76
|
def self.root(dir)
|
@@ -77,7 +81,31 @@ class Environment
|
|
77
81
|
@path = Dir.new(dir)
|
78
82
|
@parent = parent
|
79
83
|
@children = []
|
80
|
-
@local_methods = parent&.
|
84
|
+
@local_methods = parent&.unlocked_local_methods || {}
|
85
|
+
@locked_methods = []
|
86
|
+
end
|
87
|
+
|
88
|
+
def local_method(name)
|
89
|
+
@local_methods[name.to_sym]
|
90
|
+
end
|
91
|
+
|
92
|
+
def local_methods
|
93
|
+
@local_methods.keys
|
94
|
+
end
|
95
|
+
|
96
|
+
def unlocked_local_methods
|
97
|
+
@local_methods.filter{|k, v| !@locked_methods.include?(k)}
|
98
|
+
end
|
99
|
+
|
100
|
+
def lock_method(name)
|
101
|
+
n = name.to_sym
|
102
|
+
raise NameError.new("#{name} is not a local method", n) unless @local_methods.key?(n)
|
103
|
+
@locked_methods << n unless @locked_methods.include?(n)
|
104
|
+
n
|
105
|
+
end
|
106
|
+
|
107
|
+
def local_method?(name)
|
108
|
+
@local_methods.key?(name.to_sym)
|
81
109
|
end
|
82
110
|
|
83
111
|
def root?
|
@@ -104,14 +132,14 @@ class Environment
|
|
104
132
|
|
105
133
|
def add_local_method(name, &block)
|
106
134
|
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
|
135
|
+
@local_methods[name.to_sym] = block # if name already exists, its function is overriden
|
136
|
+
name.to_sym
|
109
137
|
end
|
110
138
|
|
111
139
|
# might not be useful
|
112
140
|
def clear_local_method(name)
|
113
|
-
@local_methods.delete(name)
|
114
|
-
name
|
141
|
+
@local_methods.delete(name.to_sym)
|
142
|
+
name.to_sym
|
115
143
|
end
|
116
144
|
|
117
145
|
def to_s
|
@@ -120,16 +148,13 @@ class Environment
|
|
120
148
|
end
|
121
149
|
end
|
122
150
|
|
151
|
+
# still could absolutely be more cleaned up, but it works
|
123
152
|
def self.method_missing(m, *args, &block)
|
124
153
|
exe = which(m.to_s)
|
125
154
|
if $env.local_method?(m)
|
126
155
|
$env.local_call(m, *args, &block)
|
127
156
|
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})
|
130
|
-
else
|
131
|
-
system(*$env.resolve_alias(m), *args.flatten.map{|a| a.to_s}, {out: $stdout, err: $stderr, in: $stdin})
|
132
|
-
end
|
157
|
+
$env.dispatch(m, *args)
|
133
158
|
else
|
134
159
|
super
|
135
160
|
end
|
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
|
-
|
@@ -0,0 +1,198 @@
|
|
1
|
+
class Environment
|
2
|
+
|
3
|
+
def pipelined?
|
4
|
+
@in_pipeline
|
5
|
+
end
|
6
|
+
|
7
|
+
def synced_pipeline?
|
8
|
+
@in_pipeline && @synchronous_pipeline
|
9
|
+
end
|
10
|
+
|
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
|
18
|
+
end
|
19
|
+
nil
|
20
|
+
end
|
21
|
+
|
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
|
+
|
37
|
+
input = (@active_pipelines.empty? ? $stdin : @active_pipelines.last.reader)
|
38
|
+
@active_pipelines << Pipeline.new
|
39
|
+
output = @active_pipelines.last.writer
|
40
|
+
error = ($stderr == $stdout ? output : $stderr)
|
41
|
+
|
42
|
+
pid = fork do
|
43
|
+
@in_pipeline = false
|
44
|
+
$stdin = input
|
45
|
+
$stdout = output
|
46
|
+
$stderr = error
|
47
|
+
block.call
|
48
|
+
output.close
|
49
|
+
exit!(true)
|
50
|
+
end
|
51
|
+
output.close
|
52
|
+
|
53
|
+
@active_pipelines.last.link_process(pid)
|
54
|
+
nil
|
55
|
+
end
|
56
|
+
|
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
|
+
|
70
|
+
pid = fork do
|
71
|
+
@in_pipeline = false
|
72
|
+
@synchronous_pipeline = false
|
73
|
+
$stdin = input
|
74
|
+
$stdout = output
|
75
|
+
$stderr = error
|
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)
|
138
|
+
output.close
|
139
|
+
exit!(true)
|
140
|
+
end
|
141
|
+
output.close
|
142
|
+
@active_pipelines.last.link_process(pid)
|
143
|
+
end
|
144
|
+
|
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
|
161
|
+
|
162
|
+
class Pipeline
|
163
|
+
attr_reader :writer, :reader, :pid
|
164
|
+
|
165
|
+
def initialize
|
166
|
+
@reader, @writer = IO.pipe
|
167
|
+
end
|
168
|
+
|
169
|
+
def link_process(pid)
|
170
|
+
@pid ||= pid
|
171
|
+
self
|
172
|
+
end
|
173
|
+
|
174
|
+
def close
|
175
|
+
@writer.close
|
176
|
+
@reader.close
|
177
|
+
end
|
178
|
+
|
179
|
+
def terminate
|
180
|
+
self.close
|
181
|
+
Process.kill(:TERM, @pid)
|
182
|
+
Process.wait(@pid)
|
183
|
+
end
|
184
|
+
|
185
|
+
def to_s
|
186
|
+
@pid
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
|
192
|
+
def in_pipeline(async: true, &block)
|
193
|
+
if async
|
194
|
+
$env.make_pipeline(&block)
|
195
|
+
else
|
196
|
+
$env.make_sync_pipeline(&block)
|
197
|
+
end
|
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.0
|
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,8 +34,10 @@ 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
|
40
|
+
- lib/rash/pipeline.rb
|
39
41
|
- lib/rash/prompt/irb.rb
|
40
42
|
- lib/rash/redirection.rb
|
41
43
|
homepage: https://github.com/KellenWatt/rash
|