rash-command-shell 0.1.0 → 0.3.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 +9 -3
- data/lib/rash.rb +61 -9
- data/lib/rash/aliasing.rb +26 -19
- data/lib/rash/capturing.rb +42 -0
- data/lib/rash/ext/filesystem.rb +5 -9
- data/lib/rash/jobcontrol.rb +0 -2
- data/lib/rash/pipeline.rb +108 -0
- data/lib/rash/redirection.rb +4 -2
- metadata +4 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f1656e8fb0ce00b1053a2fb71e551e4cec9108f31b41e6bff096a48f2a39a45a
|
4
|
+
data.tar.gz: fc0fc83b298541d1b8593a484660b9ba3e1f9470f226e0db2062316650557234
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 275fa36281ff54847e61457ee9f1c4dc8cf0284766d80826b1cce90ae01e0c23384c3ed5f532121566a709318546ebd62749944b3ecf52951cd43dcbdcca0231
|
7
|
+
data.tar.gz: 2b11e91d955014d576ab6a90ff3650458631592f86f5a3d2508d5800d174470365e79959e11f721f516b693957bb72ae2c12eb4c66270464cf9eb45d2d70b546
|
data/bin/rash
CHANGED
@@ -1,9 +1,15 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
|
-
if ARGV.
|
4
|
-
exec("irb", "-r", "rash")
|
5
|
-
|
3
|
+
if ARGV.empty?
|
4
|
+
exec("irb", "-r", "rash", *ARGV)
|
5
|
+
elsif ARGV[0] =~ /(-)?-v(ersion)?/
|
6
|
+
puts "Rash (c) 2020 Kellen Watt"
|
7
|
+
puts "Version 0.2.2" # I may forget to update this
|
8
|
+
elsif File.exists?(ARGV[0]) && !File.directory?(ARGV[0])
|
6
9
|
require "rash"
|
7
10
|
file = ARGV.shift
|
8
11
|
load file
|
12
|
+
else
|
13
|
+
$stderr.puts "#{File.basename($0)}: #{ARGV[0]}: No such file or directory"
|
14
|
+
exit 1
|
9
15
|
end
|
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
|
@@ -28,6 +28,11 @@ class Environment
|
|
28
28
|
super
|
29
29
|
end
|
30
30
|
end
|
31
|
+
|
32
|
+
def umask=(mask)
|
33
|
+
File.umask(mask)
|
34
|
+
@umask = mask
|
35
|
+
end
|
31
36
|
|
32
37
|
def as_superuser(&block)
|
33
38
|
@superuser_mode = true
|
@@ -37,27 +42,70 @@ class Environment
|
|
37
42
|
@superuser_mode = false
|
38
43
|
end
|
39
44
|
end
|
45
|
+
|
46
|
+
def with_limits(limits, &block)
|
47
|
+
if block_given?
|
48
|
+
pid = fork do
|
49
|
+
limits.each {|resource, limit| Process.setrlimit(resource, *limit)}
|
50
|
+
block.call
|
51
|
+
exit!(true)
|
52
|
+
end
|
53
|
+
Process.wait(pid)
|
54
|
+
else
|
55
|
+
limits.each {|resource, limit| Process.setrlimit(resource, *limit)}
|
56
|
+
end
|
57
|
+
end
|
40
58
|
|
59
|
+
def dispatch(m, *args)
|
60
|
+
if @in_pipeline
|
61
|
+
add_pipeline(m, *args)
|
62
|
+
else
|
63
|
+
system_command(m, *args)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
41
67
|
private
|
42
68
|
|
43
69
|
def common_init
|
44
70
|
@working_directory = Dir.pwd
|
71
|
+
@umask = File.umask
|
45
72
|
|
46
73
|
@aliases = {}
|
47
74
|
@aliasing_disabled = false
|
48
75
|
@active_jobs = []
|
49
76
|
|
77
|
+
@active_pipelines = []
|
78
|
+
|
50
79
|
@prompt = {
|
51
80
|
AUTO_INDENT: true,
|
52
81
|
RETURN: ""
|
53
82
|
}
|
54
83
|
ENV["RASHDIR"] = File.dirname(__FILE__)
|
55
84
|
end
|
85
|
+
|
86
|
+
def resolve_command(m, *args, literal: false)
|
87
|
+
(literal ? [m.to_s] : resolve_alias(m)) + args.flatten.map{|a| a.to_s}
|
88
|
+
end
|
89
|
+
|
90
|
+
def system_command(m, *args, except: false, literal: false, out: nil, input: nil, err: nil)
|
91
|
+
command = resolve_command(m, *args, literal: literal)
|
92
|
+
command.unshift("sudo") if @superuser_mode
|
93
|
+
opts = {out: out || $stdout,
|
94
|
+
err: err || $stderr,
|
95
|
+
in: input || $stdin,
|
96
|
+
exception: except || @superuser_mode,
|
97
|
+
umask: @umask}
|
98
|
+
|
99
|
+
system(*command, opts)
|
100
|
+
end
|
101
|
+
|
56
102
|
end
|
57
103
|
|
58
104
|
require_relative "rash/redirection"
|
59
105
|
require_relative "rash/aliasing"
|
60
106
|
require_relative "rash/jobcontrol"
|
107
|
+
require_relative "rash/pipeline"
|
108
|
+
require_relative "rash/capturing"
|
61
109
|
|
62
110
|
$env = Environment.new
|
63
111
|
|
@@ -65,7 +113,12 @@ $env = Environment.new
|
|
65
113
|
# note for later documentation: any aliases of cd must be functions, not
|
66
114
|
# environmental aliases. Limitation of implementation.
|
67
115
|
def cd(dir = nil)
|
68
|
-
|
116
|
+
d = dir
|
117
|
+
case d
|
118
|
+
when File, Dir
|
119
|
+
d = d.path if File.directory?(d.path)
|
120
|
+
end
|
121
|
+
$env.chdir(d)
|
69
122
|
end
|
70
123
|
|
71
124
|
|
@@ -75,12 +128,15 @@ def run(file, *args)
|
|
75
128
|
unless File.executable?(exe)
|
76
129
|
raise SystemCallError.new("No such executable file - #{exe}", Errno::ENOENT::Errno)
|
77
130
|
end
|
78
|
-
|
131
|
+
$env.dispatch(exe, *args, literal: true)
|
79
132
|
end
|
80
133
|
|
134
|
+
alias cmd __send__
|
81
135
|
|
82
136
|
# Defines `bash` psuedo-compatibility. Filesystem effects happen like normal
|
83
137
|
# and environmental variable changes are copied
|
138
|
+
#
|
139
|
+
# This is an artifact of an old design and is deprecated until further notice.
|
84
140
|
def sourcesh(file)
|
85
141
|
bash_env = lambda do |cmd = nil|
|
86
142
|
tmpenv = `#{cmd + ';' if cmd} printenv`
|
@@ -95,7 +151,7 @@ end
|
|
95
151
|
|
96
152
|
|
97
153
|
def which(command)
|
98
|
-
cmd = File.expand_path(command)
|
154
|
+
cmd = File.expand_path(command.to_s)
|
99
155
|
return cmd if File.executable?(cmd) && !File.directory?(cmd)
|
100
156
|
|
101
157
|
exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : ['']
|
@@ -115,11 +171,7 @@ end
|
|
115
171
|
def self.method_missing(m, *args, &block)
|
116
172
|
exe = which(m.to_s)
|
117
173
|
if exe || ($env.alias?(m) && !$env.aliasing_disabled)
|
118
|
-
|
119
|
-
system("sudo", *$env.resolve_alias(m), *args.flatten.map{|a| a.to_s}, {out: $stdout, err: $stderr, in: $stdin})
|
120
|
-
else
|
121
|
-
system(*$env.resolve_alias(m), *args.flatten.map{|a| a.to_s}, {out: $stdout, err: $stderr, in: $stdin})
|
122
|
-
end
|
174
|
+
$env.dispatch(m, *args)
|
123
175
|
else
|
124
176
|
super
|
125
177
|
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,42 @@
|
|
1
|
+
class Environment
|
2
|
+
def capture_block(&block)
|
3
|
+
raise ArgumentError.new("no block provided") unless block_given?
|
4
|
+
result = nil
|
5
|
+
begin
|
6
|
+
reader, writer = IO.pipe
|
7
|
+
self.stdout = writer
|
8
|
+
block.call
|
9
|
+
ensure
|
10
|
+
reset_stdout
|
11
|
+
writer.close
|
12
|
+
result = reader.read
|
13
|
+
reader.close
|
14
|
+
end
|
15
|
+
result
|
16
|
+
end
|
17
|
+
|
18
|
+
def capture_command(m, *args)
|
19
|
+
raise NameError.new("no such command", m) unless which(m) || ($env.alias?(m) && !$env.aliasing_disabled)
|
20
|
+
result = nil
|
21
|
+
begin
|
22
|
+
reader, writer = IO.pipe
|
23
|
+
system_command(m, *args, out: writer)
|
24
|
+
ensure
|
25
|
+
writer.close
|
26
|
+
result = reader.read
|
27
|
+
reader.close
|
28
|
+
end
|
29
|
+
result
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# This explicitly doesn't support pipelining, as the output is ripped out of sequence.
|
34
|
+
def capture(*cmd, &block)
|
35
|
+
if block_given?
|
36
|
+
$env.capture_block(&block)
|
37
|
+
elsif cmd.size > 0 && (which(cmd[0]) || ($env.alias?(m) && !$env.aliasing_disabled))
|
38
|
+
$env.capture_command(cmd[0].to_s, *cmd[1..])
|
39
|
+
else
|
40
|
+
raise ArgumentError.new("nothing to capture from")
|
41
|
+
end
|
42
|
+
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("/")
|
@@ -33,14 +34,12 @@ class Environment
|
|
33
34
|
|
34
35
|
private
|
35
36
|
|
36
|
-
LOCAL_FUNCTIONS_ENABLED = true
|
37
|
-
|
38
37
|
# from and to are strings
|
39
38
|
def traverse_filetree(from, to)
|
40
39
|
abs_from = File.expand_path(from)
|
41
40
|
abs_to = File.expand_path(to)
|
42
|
-
raise SystemCallError(from, Errno::ENOENT::Errno) unless Dir.exists?(abs_from)
|
43
|
-
raise SystemCallError(to, Errno::ENOENT::Errno) unless Dir.exists?(abs_to)
|
41
|
+
raise SystemCallError.new(from, Errno::ENOENT::Errno) unless Dir.exists?(abs_from)
|
42
|
+
raise SystemCallError.new(to, Errno::ENOENT::Errno) unless Dir.exists?(abs_to)
|
44
43
|
|
45
44
|
from_parts = (abs_from == "/" ? [""] : abs_from.split(File::SEPARATOR))
|
46
45
|
to_parts = (abs_to == "/" ? [""] : abs_to.split(File::SEPARATOR))
|
@@ -120,16 +119,13 @@ class Environment
|
|
120
119
|
end
|
121
120
|
end
|
122
121
|
|
122
|
+
# still could absolutely be more cleaned up, but it works
|
123
123
|
def self.method_missing(m, *args, &block)
|
124
124
|
exe = which(m.to_s)
|
125
125
|
if $env.local_method?(m)
|
126
126
|
$env.local_call(m, *args, &block)
|
127
127
|
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
|
128
|
+
$env.dispatch(m, *args)
|
133
129
|
else
|
134
130
|
super
|
135
131
|
end
|
data/lib/rash/jobcontrol.rb
CHANGED
@@ -0,0 +1,108 @@
|
|
1
|
+
class Environment
|
2
|
+
|
3
|
+
def pipelined?
|
4
|
+
@in_pipeline
|
5
|
+
end
|
6
|
+
|
7
|
+
def make_pipeline(&block)
|
8
|
+
raise IOError.new("pipelining already enabled") if @in_pipeline
|
9
|
+
start_pipeline
|
10
|
+
begin
|
11
|
+
block.call
|
12
|
+
ensure
|
13
|
+
end_pipeline
|
14
|
+
end
|
15
|
+
nil
|
16
|
+
end
|
17
|
+
|
18
|
+
def as_pipe_command(&block)
|
19
|
+
raise IOError.new("pipelining not enabled") unless @in_pipeline
|
20
|
+
|
21
|
+
input = (@active_pipelines.empty? ? $stdin : @active_pipelines.last.reader)
|
22
|
+
@active_pipelines << Pipeline.new
|
23
|
+
output = @active_pipelines.last.writer
|
24
|
+
error = ($stderr == $stdout ? output : $stderr)
|
25
|
+
|
26
|
+
pid = fork do
|
27
|
+
@in_pipeline = false
|
28
|
+
$stdin = input
|
29
|
+
$stdout = output
|
30
|
+
$stderr = error
|
31
|
+
block.call
|
32
|
+
output.close
|
33
|
+
exit!(true)
|
34
|
+
end
|
35
|
+
output.close
|
36
|
+
|
37
|
+
@active_pipelines.last.link_process(pid)
|
38
|
+
nil
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def start_pipeline
|
44
|
+
@in_pipeline = true
|
45
|
+
end
|
46
|
+
|
47
|
+
def end_pipeline
|
48
|
+
raise IOError.new("pipelining not enabled") unless @in_pipeline
|
49
|
+
@in_pipeline = false
|
50
|
+
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
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# special method to be referenced from Environment#dispatch. Do not use directly
|
61
|
+
def add_pipeline(m, *args)
|
62
|
+
raise IOError.new("pipelining not enabled") unless @in_pipeline
|
63
|
+
input = (@active_pipelines.empty? ? $stdin : @active_pipelines.last.reader)
|
64
|
+
@active_pipelines << Pipeline.new
|
65
|
+
output = @active_pipelines.last.writer
|
66
|
+
error = ($stderr == $stdout ? output : $stderr)
|
67
|
+
pid = fork do # might not be necessary, spawn might cover it. Not risking it before testing
|
68
|
+
system_command(m, *args, out: output, input: input, err: error, except: true)
|
69
|
+
output.close
|
70
|
+
exit!(true)
|
71
|
+
end
|
72
|
+
output.close
|
73
|
+
@active_pipelines.last.link_process(pid)
|
74
|
+
end
|
75
|
+
|
76
|
+
class Pipeline
|
77
|
+
attr_reader :writer, :reader, :pid
|
78
|
+
|
79
|
+
def initialize
|
80
|
+
@reader, @writer = IO.pipe
|
81
|
+
end
|
82
|
+
|
83
|
+
def link_process(pid)
|
84
|
+
@pid ||= pid
|
85
|
+
self
|
86
|
+
end
|
87
|
+
|
88
|
+
def close
|
89
|
+
@writer.close
|
90
|
+
@reader.close
|
91
|
+
end
|
92
|
+
|
93
|
+
def terminate
|
94
|
+
self.close
|
95
|
+
Process.kill(:PIPE, @pid)
|
96
|
+
Process.wait(@pid)
|
97
|
+
end
|
98
|
+
|
99
|
+
def to_s
|
100
|
+
@pid
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
|
106
|
+
def in_pipeline(&block)
|
107
|
+
$env.make_pipeline(&block)
|
108
|
+
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.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Kellen Watt
|
@@ -17,9 +17,6 @@ dependencies:
|
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
19
|
version: '1.2'
|
20
|
-
- - ">="
|
21
|
-
- !ruby/object:Gem::Version
|
22
|
-
version: 1.2.0
|
23
20
|
type: :runtime
|
24
21
|
prerelease: false
|
25
22
|
version_requirements: !ruby/object:Gem::Requirement
|
@@ -27,9 +24,6 @@ dependencies:
|
|
27
24
|
- - "~>"
|
28
25
|
- !ruby/object:Gem::Version
|
29
26
|
version: '1.2'
|
30
|
-
- - ">="
|
31
|
-
- !ruby/object:Gem::Version
|
32
|
-
version: 1.2.0
|
33
27
|
description: A Ruby-based shell
|
34
28
|
email:
|
35
29
|
executables:
|
@@ -40,8 +34,10 @@ files:
|
|
40
34
|
- bin/rash
|
41
35
|
- lib/rash.rb
|
42
36
|
- lib/rash/aliasing.rb
|
37
|
+
- lib/rash/capturing.rb
|
43
38
|
- lib/rash/ext/filesystem.rb
|
44
39
|
- lib/rash/jobcontrol.rb
|
40
|
+
- lib/rash/pipeline.rb
|
45
41
|
- lib/rash/prompt/irb.rb
|
46
42
|
- lib/rash/redirection.rb
|
47
43
|
homepage: https://github.com/KellenWatt/rash
|
@@ -56,7 +52,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
56
52
|
requirements:
|
57
53
|
- - ">="
|
58
54
|
- !ruby/object:Gem::Version
|
59
|
-
version: '
|
55
|
+
version: '2.5'
|
60
56
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
61
57
|
requirements:
|
62
58
|
- - ">="
|