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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 19cb922bc55b83424f375e375c2015dff74690720b4d611357646bfd11e04c44
4
- data.tar.gz: 48ce02ad077cd0a89a7fc62987907cbfc0ca80997e398c6e9699ac4beacf4521
3
+ metadata.gz: f1656e8fb0ce00b1053a2fb71e551e4cec9108f31b41e6bff096a48f2a39a45a
4
+ data.tar.gz: fc0fc83b298541d1b8593a484660b9ba3e1f9470f226e0db2062316650557234
5
5
  SHA512:
6
- metadata.gz: 6e1d9cd6e7270bd206d79db56b9ca20e2bab9b976c7e76b835c3af078e9a16816714807b8c96eafdc5f403369404bce243c31105bd33869fabb5fbffba65adf7
7
- data.tar.gz: 6e36ba48448f4aed666807150a8819855ccb2e8802093f56cfc6f357277cd24a6ff91c43de0889affd15fddd2d642b1601ffcd7c347949766a434c124b1d7b1b
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.size == 0
4
- exec("irb", "-r", "rash")
5
- else
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
@@ -1,7 +1,7 @@
1
1
  class Environment
2
2
 
3
3
  attr_reader :aliasing_disabled
4
- attr_reader :superuser_mode
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
- $env.chdir(dir)
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
- system(exe, *args.flatten.map{|a| a.to_s}, {out: $stdout, err: $stderr, in: $stdin})
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
- if $env.superuser_mode
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
@@ -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,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
@@ -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
- if $env.superuser_mode
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
@@ -17,5 +17,3 @@ end
17
17
  def as_background(&block)
18
18
  $env.async(&block)
19
19
  end
20
-
21
-
@@ -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
@@ -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.1.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: '0'
55
+ version: '2.5'
60
56
  required_rubygems_version: !ruby/object:Gem::Requirement
61
57
  requirements:
62
58
  - - ">="