rash-command-shell 0.2.2 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8d2870af889b41fda5f16c62f4f181c09ae843cc61ef4391596078c723c13269
4
- data.tar.gz: 0e4e282af5b0940e63478b62985238e427969f2d95e7fccb6e25afc7f18f7c3b
3
+ metadata.gz: 00dbd88cd56e1de80255ce40b9a061048ae8ec96b70cb0dc5c36a78589e73c68
4
+ data.tar.gz: f31d65ea147f7c3fa5516d51b38e70c28fa3cadb482427393887f9d42ef34321
5
5
  SHA512:
6
- metadata.gz: ca2658254fe315de5c6902a301d1a5d7f210e68630f9ac0677d811af697a2b334e6f185882d4ac8e725e56ee3e8dad777f8b09e7a5db9159914fb2dbecf9a589
7
- data.tar.gz: 2820b7e820e1a28fdf87e898cc284b8c8a9be53193fd72047b61cc6036ba51eb2fa92e2b0dc687e73f258a8af7854a89320230bf349b71cc8d5d3e09ee46ca62
6
+ metadata.gz: e4194630eb9dbe5b1c4ad8afb9eb09e232b801eab51d06560cc1c41404d4202ade769c22a4da4f878f9d95ca213566635b90472e6c507e66578ddd2f465921f0
7
+ data.tar.gz: 5a12911de5b15ed865d14773b34b2a51f0efea3a3159bf29821eda79cf0ba7459429c7e4b1d95290cf03701745b4c914e4d9b8745906985e82a113f7be8351ab
data/bin/rash CHANGED
@@ -4,7 +4,7 @@ if ARGV.empty?
4
4
  exec("irb", "-r", "rash", *ARGV)
5
5
  elsif ARGV[0] =~ /(-)?-v(ersion)?/
6
6
  puts "Rash (c) 2020 Kellen Watt"
7
- puts "Version 0.2.2" # I may forget to update this
7
+ puts "Version 0.4.2" # I may forget to update this
8
8
  elsif File.exists?(ARGV[0]) && !File.directory?(ARGV[0])
9
9
  require "rash"
10
10
  file = ARGV.shift
@@ -12,9 +12,24 @@ class Environment
12
12
  Dir.chdir(dir.nil? ? "~" : dir.to_s)
13
13
  @working_directory = Dir.pwd
14
14
  ENV["OLDPWD"] = old.to_s
15
+ ENV["PWD"] = Dir.pwd
15
16
  Dir.pwd
16
17
  end
17
-
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
+
18
33
  def add_path(path)
19
34
  ENV["PATH"] += File::PATH_SEPARATOR + (path.respond_to?(:path) ? path.path : path.to_s)
20
35
  end
@@ -64,6 +79,10 @@ class Environment
64
79
  end
65
80
  end
66
81
 
82
+ def name?(v)
83
+ v.kind_of?(String) || v.kind_of?(Symbol)
84
+ end
85
+
67
86
  private
68
87
 
69
88
  def common_init
@@ -76,6 +95,8 @@ class Environment
76
95
 
77
96
  @active_pipelines = []
78
97
 
98
+ @directory_stack = []
99
+
79
100
  @prompt = {
80
101
  AUTO_INDENT: true,
81
102
  RETURN: ""
@@ -105,6 +126,7 @@ require_relative "rash/redirection"
105
126
  require_relative "rash/aliasing"
106
127
  require_relative "rash/jobcontrol"
107
128
  require_relative "rash/pipeline"
129
+ require_relative "rash/capturing"
108
130
 
109
131
  $env = Environment.new
110
132
 
@@ -120,6 +142,17 @@ def cd(dir = nil)
120
142
  $env.chdir(d)
121
143
  end
122
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
123
156
 
124
157
  def run(file, *args)
125
158
  filename = file.to_s
@@ -128,7 +161,6 @@ def run(file, *args)
128
161
  raise SystemCallError.new("No such executable file - #{exe}", Errno::ENOENT::Errno)
129
162
  end
130
163
  $env.dispatch(exe, *args, literal: true)
131
- # system(exe, *args.flatten.map{|a| a.to_s}, {out: $stdout, err: $stderr, in: $stdin, umask: $env.umask})
132
164
  end
133
165
 
134
166
  alias cmd __send__
@@ -151,7 +183,7 @@ end
151
183
 
152
184
 
153
185
  def which(command)
154
- cmd = File.expand_path(command)
186
+ cmd = File.expand_path(command.to_s)
155
187
  return cmd if File.executable?(cmd) && !File.directory?(cmd)
156
188
 
157
189
  exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : ['']
@@ -165,6 +197,11 @@ def which(command)
165
197
  nil
166
198
  end
167
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
168
205
 
169
206
  # Note that I defy convention and don't define `respond_to_missing?`. This
170
207
  # is because doing so screws with irb.
@@ -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
@@ -6,30 +6,56 @@ class Environment
6
6
  def initialize
7
7
  common_init
8
8
  @working_directory = Directory.root("/")
9
- traverse_filetree("/", Dir.pwd)
10
9
  end
11
10
 
12
11
  def chdir(dir = nil)
13
12
  old = @working_directory
14
- traverse_filetree(Dir.pwd, (dir.nil? ? "~" : dir.to_s))
13
+ traverse_filetree(@working_directory.to_s, (dir.nil? ? "~" : dir.to_s))
15
14
  ENV["OLDPWD"] = old.to_s
15
+ ENV["PWD"] = Dir.pwd
16
16
  Dir.pwd
17
17
  end
18
18
 
19
- def local_def(name, &block)
20
- @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
21
23
  end
22
24
 
23
25
  def local_undef(name)
24
- @working_directory.clear_local_method(name.to_sym)
26
+ @working_directory.clear_local_method(name)
25
27
  end
26
28
 
27
29
  def local_method?(name)
28
- @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
29
35
  end
30
36
 
31
37
  def local_call(name, *args, &block)
32
- @working_directory.local_methods[name.to_sym].call(*args, &block)
38
+ @working_directory.local_method(name).call(*args, &block)
39
+ end
40
+
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
33
59
  end
34
60
 
35
61
  private
@@ -65,7 +91,6 @@ class Environment
65
91
  end
66
92
 
67
93
  class Directory
68
- attr_reader :local_methods
69
94
  attr_reader :parent, :children
70
95
 
71
96
  def self.root(dir)
@@ -76,9 +101,121 @@ class Environment
76
101
  @path = Dir.new(dir)
77
102
  @parent = parent
78
103
  @children = []
79
- @local_methods = parent&.local_methods.dup || {}
104
+ @local_methods = {}
105
+ @locked_methods = []
106
+ @local_variables = {}
107
+ @locked_variables = []
108
+ end
109
+
110
+ ######################
111
+ # Local method methods
112
+ ######################
113
+
114
+ def local_method(name)
115
+ name = name.to_sym
116
+ @local_methods[name] || @parent&.unlocked_local_method(name)
117
+ end
118
+
119
+ def local_method?(name)
120
+ name = name.to_sym
121
+ @local_methods.key?(name) || !!@parent&.unlocked_local_method?(name)
122
+ end
123
+
124
+ def local_methods
125
+ @local_methods.keys + (@parent&.unlocked_local_methods).to_a
126
+ end
127
+
128
+ def add_local_method(name, &block)
129
+ raise ArgumentError.new "no method body provided" unless block_given?
130
+ @local_methods[name.to_sym] = block # if name already exists, its function is overriden
131
+ name.to_sym
132
+ end
133
+
134
+ def unlocked_local_method(name)
135
+ name = name.to_sym
136
+ @local_methods[name] || @parent&.unlocked_local_method(name)
137
+ end
138
+
139
+ def unlocked_local_method?(name)
140
+ name = name.to_sym
141
+ @local_methods.filter{|k, v| !@locked_methods.include?(k)}.key?(name) ||
142
+ !!@parent&.unlocked_local_method?(name)
143
+ end
144
+
145
+ def unlocked_local_methods
146
+ @local_methods.filter{|k, v| !@locked_methods.include?(k)}.keys + (@parent&.unlocked_local_methods).to_a
147
+ end
148
+
149
+ def lock_method(name)
150
+ n = name.to_sym
151
+ raise NameError.new("#{name} is not a local method", n) unless @local_methods.key?(n)
152
+ @locked_methods << n unless @locked_methods.include?(n)
153
+ n
154
+ end
155
+
156
+ # might not be useful
157
+ def clear_local_method(name)
158
+ @local_methods.delete(name.to_sym)
159
+ name.to_sym
160
+ end
161
+
162
+ ######################
163
+ # Local variable stuff
164
+ ######################
165
+
166
+ def local_variable(name)
167
+ name = name.to_sym
168
+ @local_variables[name] || @parent&.unlocked_local_variable(name)
169
+ end
170
+
171
+ def local_variable?(name)
172
+ @local_variables.include?(name.to_sym) || !!@parent&.unlocked_local_variable?(name.to_sym)
173
+ end
174
+
175
+ def local_variables
176
+ @local_variables.keys + (@parent&.unlocked_local_variables).to_a
177
+ end
178
+
179
+ def set_local_variable(name, value)
180
+ name = name.to_sym
181
+ if !@local_variables.key?(name) && @parent&.unlocked_local_variable?(name)
182
+ @parent&.set_local_variable(name, value)
183
+ else
184
+ @local_variables[name] = value
185
+ end
186
+ end
187
+
188
+ def unlocked_local_variable(name)
189
+ name = name.to_sym
190
+ @local_variables.filter{|k| !@locked_variables.include?(k)}[name] || @parent&.unlocked_local_variable(name)
191
+ end
192
+
193
+ def unlocked_local_variables
194
+ @local_variables.keys.filter{|k| !@locked_variables.include?(k)} + (@parent&.unlocked_local_variables).to_a
80
195
  end
81
196
 
197
+ def unlocked_local_variable?(name)
198
+ name = name.to_sym
199
+ @local_variables.filter{|k,_v| !@locked_variables.include?(k)}.key?(name) ||
200
+ !!@parent&.unlocked_local_variable?(name)
201
+ end
202
+
203
+ def lock_variable(name)
204
+ n = name.to_sym
205
+ raise NameError.new("#{name} is not a local variable", n) unless @local_variables.key?(n)
206
+ @locked_variables << n unless @locked_variables.include?(n)
207
+ n
208
+ end
209
+
210
+ def clear_local_variable(name)
211
+ @local_variables.delete(name.to_sym)
212
+ name.to_sym
213
+ end
214
+
215
+ ###########################
216
+ # Generic traversal methods
217
+ ###########################
218
+
82
219
  def root?
83
220
  parent.nil?
84
221
  end
@@ -101,18 +238,6 @@ class Environment
101
238
  dir
102
239
  end
103
240
 
104
- def add_local_method(name, &block)
105
- raise ArgumentError.new "no method body provided" unless block_given?
106
- @local_methods[name] = block # if name already exists, its function is overriden
107
- name
108
- end
109
-
110
- # might not be useful
111
- def clear_local_method(name)
112
- @local_methods.delete(name)
113
- name
114
- end
115
-
116
241
  def to_s
117
242
  @path.path
118
243
  end
@@ -121,10 +246,9 @@ end
121
246
 
122
247
  # still could absolutely be more cleaned up, but it works
123
248
  def self.method_missing(m, *args, &block)
124
- exe = which(m.to_s)
125
249
  if $env.local_method?(m)
126
250
  $env.local_call(m, *args, &block)
127
- elsif exe || ($env.alias?(m) && !$env.aliasing_disabled)
251
+ elsif which(m.to_s) || ($env.alias?(m) && !$env.aliasing_disabled)
128
252
  $env.dispatch(m, *args)
129
253
  else
130
254
  super
@@ -132,3 +256,4 @@ def self.method_missing(m, *args, &block)
132
256
  end
133
257
 
134
258
  $env = Environment.new
259
+ $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)
@@ -4,6 +4,10 @@ class Environment
4
4
  @in_pipeline
5
5
  end
6
6
 
7
+ def synced_pipeline?
8
+ @in_pipeline && @synchronous_pipeline
9
+ end
10
+
7
11
  def make_pipeline(&block)
8
12
  raise IOError.new("pipelining already enabled") if @in_pipeline
9
13
  start_pipeline
@@ -14,9 +18,21 @@ class Environment
14
18
  end
15
19
  nil
16
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
17
32
 
18
33
  def as_pipe_command(&block)
19
34
  raise IOError.new("pipelining not enabled") unless @in_pipeline
35
+ return as_sync_pipe_command(&block) if @synchronous_pipeline
20
36
 
21
37
  input = (@active_pipelines.empty? ? $stdin : @active_pipelines.last.reader)
22
38
  @active_pipelines << Pipeline.new
@@ -38,28 +54,81 @@ class Environment
38
54
  nil
39
55
  end
40
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
+
41
85
  private
42
86
 
43
87
  def start_pipeline
44
88
  @in_pipeline = true
45
89
  end
46
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
+
47
99
  def end_pipeline
48
100
  raise IOError.new("pipelining not enabled") unless @in_pipeline
49
101
  @in_pipeline = false
50
102
  if @active_pipelines.size > 0
51
- Process.wait(@active_pipelines.last.pid)
52
- @active_pipelines.last.writer.close # probably redundant, but leaving it for now
53
- IO.copy_stream(@active_pipelines.last.reader, $stdout)
54
- @active_pipelines.pop.close
55
- @active_pipelines.reverse_each {|pipe| pipe.terminate}
56
- @active_pipelines.clear
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
57
112
  end
58
113
  end
59
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
+
60
127
  # special method to be referenced from Environment#dispatch. Do not use directly
61
128
  def add_pipeline(m, *args)
62
129
  raise IOError.new("pipelining not enabled") unless @in_pipeline
130
+ return add_sync_pipeline(m, *args) if @synchronous_pipeline
131
+
63
132
  input = (@active_pipelines.empty? ? $stdin : @active_pipelines.last.reader)
64
133
  @active_pipelines << Pipeline.new
65
134
  output = @active_pipelines.last.writer
@@ -73,6 +142,23 @@ class Environment
73
142
  @active_pipelines.last.link_process(pid)
74
143
  end
75
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
+
76
162
  class Pipeline
77
163
  attr_reader :writer, :reader, :pid
78
164
 
@@ -92,7 +178,7 @@ class Environment
92
178
 
93
179
  def terminate
94
180
  self.close
95
- Process.kill(:PIPE, @pid)
181
+ Process.kill(:TERM, @pid)
96
182
  Process.wait(@pid)
97
183
  end
98
184
 
@@ -103,6 +189,10 @@ class Environment
103
189
  end
104
190
 
105
191
 
106
- def in_pipeline(&block)
107
- $env.make_pipeline(&block)
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
108
198
  end
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.2
4
+ version: 0.4.2
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