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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6769401d090279e0c3f7b3a2e26a52c59af013196c84e9b42ec09e5d9e5b5a42
4
- data.tar.gz: 9a93bc36eb0316ba762cefa301457b9537df4009ee8127ae8c8691bdb18eecaa
3
+ metadata.gz: f799801cf8b57a584f090320cbd63d7b2e54dbc0e7c66fbac9943d7b75e35417
4
+ data.tar.gz: 34b945f7da8b07813f405417c99d091466215b087fb709e71394ad3b551044a0
5
5
  SHA512:
6
- metadata.gz: 1462afc1e5767594c11f45104046618a3c5158cab07ad1315fe2e46b7ed64cea0c2972516338086cbb5e6ae2ba3f99e7e442873bc7213d72830697f36adf3cdd
7
- data.tar.gz: c296445d776d3dcde932617030279866961e1f77126e9551d08a5e31835dc0d583b710ba34a8382b245b6c142b1d51ab02081ef0a744b29556d4aef06a830933
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
@@ -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
- system(exe, *args.flatten.map{|a| a.to_s}, {out: $stdout, err: $stderr, in: $stdin, umask: $env.umask})
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
- if $env.superuser_mode
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
@@ -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.has_key?(f.to_sym)
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
@@ -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(Dir.pwd, (dir.nil? ? "~" : dir.to_s))
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.to_sym, &block)
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.to_sym)
26
+ @working_directory.clear_local_method(name)
24
27
  end
25
28
 
26
29
  def local_method?(name)
27
- @working_directory.local_methods.key?(name.to_sym)
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.local_methods[name.to_sym].call(*args, &block)
38
+ @working_directory.local_method(name).call(*args, &block)
32
39
  end
33
40
 
34
- private
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
- LOCAL_FUNCTIONS_ENABLED = true
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&.local_methods.dup || {}
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
- if $env.superuser_mode
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)
@@ -1,6 +1,6 @@
1
1
  class Environment
2
2
  def jobs
3
- @active_jobs.keep_if {|pid| Process.kill(0, pid) rescue false}
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
-
@@ -4,47 +4,137 @@ class Environment
4
4
  @in_pipeline
5
5
  end
6
6
 
7
- def start_pipeline
8
- @in_pipeline = true
7
+ def synced_pipeline?
8
+ @in_pipeline && @synchronous_pipeline
9
9
  end
10
10
 
11
- def end_pipeline
12
- @in_pipeline = false
13
- if @active_pipelines.size > 0
14
- Process.wait(@active_pipelines.last.pid)
15
- @active_pipelines.last.writer.close # probably redundant, but leaving it for now
16
- IO.copy_stream(@active_pipelines.last.reader, $stdout)
17
- @active_pipelines.pop.close
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 add_pipeline(m, *args)
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
- pid = fork do # might not be necessary, spawn might cover it. Not risking it before testing
29
- system(*$env.resolve_alias(m), *args.flatten.map{|a| a.to_s}, {out: output, in: input, err: error, exception: true, umask: @umask})
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 as_pipe(&block)
38
- input = (@active_pipelines.empty? ? $stdin : @active_pipelines.last.reader)
39
- @active_pipelines << Pipeline.new
40
- output = @active_pipelines.last.writer
41
- error = ($stderr == $stdout ? output : $stderr)
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
- private
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(:PIPE, @pid)
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
- raise IOError.new("pipelining already enabled") if $env.pipelined?
89
- $env.start_pipeline
90
- begin
91
- block.call
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
@@ -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.2.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