ruby-sh 2.1.4

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.
@@ -0,0 +1,243 @@
1
+ require "open3"
2
+
3
+ module Rubsh
4
+ class RunningPipeline
5
+ SPECIAL_KWARGS = %i[
6
+ _in_data
7
+ _in
8
+ _out
9
+ _err
10
+ _err_to_out
11
+ _capture
12
+ _bg
13
+ _env
14
+ _timeout
15
+ _cwd
16
+ _ok_code
17
+ _out_bufsize
18
+ _err_bufsize
19
+ _no_out
20
+ _no_err
21
+ ]
22
+
23
+ # @!attribute [r] exit_code
24
+ # @return [Number]
25
+ attr_reader :exit_code
26
+
27
+ # @!attribute [r] stdout_data
28
+ # @return [String]
29
+ attr_reader :stdout_data
30
+
31
+ # @!attribute [r] stderr_data
32
+ # @return [String]
33
+ attr_reader :stderr_data
34
+
35
+ def initialize(sh)
36
+ @sh = sh
37
+ @rcmds = []
38
+
39
+ # Runtime
40
+ @prog_with_args = nil
41
+ @exit_code = nil
42
+ @stdout_data = "".force_encoding(::Encoding.default_external)
43
+ @stderr_data = "".force_encoding(::Encoding.default_external)
44
+ @in_rd = nil
45
+ @in_wr = nil
46
+ @out_rd = nil
47
+ @out_wr = nil
48
+ @err_rd = nil
49
+ @err_wr = nil
50
+ @out_rd_reader = nil
51
+ @err_rd_reader = nil
52
+ @waiters = []
53
+
54
+ # Special Kwargs - Controlling Input/Output
55
+ @_in_data = nil
56
+ @_in = nil
57
+ @_out = nil
58
+ @_err = nil
59
+ @_err_to_out = false
60
+ @_capture = nil
61
+
62
+ # Special Kwargs - Execution
63
+ @_bg = false
64
+ @_env = nil
65
+ @_timeout = nil
66
+ @_cwd = nil
67
+ @_ok_code = [0]
68
+
69
+ # Special Kwargs - Performance & Optimization
70
+ @_out_bufsize = 0
71
+ @_err_bufsize = 0
72
+ @_no_out = false
73
+ @_no_err = false
74
+ end
75
+
76
+ # @return [void]
77
+ def wait(timeout: nil)
78
+ timeout_occurred = false
79
+ last_status = nil
80
+
81
+ if timeout
82
+ begin
83
+ ::Timeout.timeout(timeout) { last_status = @waiters.map(&:value)[-1] }
84
+ rescue ::Timeout::Error
85
+ timeout_occurred = true
86
+
87
+ failures = []
88
+ @waiters.each { |w| ::Process.kill("TERM", w.pid) } # graceful stop
89
+ @waiters.each { |w|
90
+ _, status = nil, nil
91
+ 30.times do
92
+ _, status = ::Process.wait2(w.pid, ::Process::WNOHANG | ::Process::WUNTRACED)
93
+ break if status
94
+ sleep 0.1
95
+ rescue ::Errno::ECHILD, ::Errno::ESRCH
96
+ status = true
97
+ end
98
+ failures << w.pid if status.nil?
99
+ }
100
+ failures.each { |pid| ::Process.kill("KILL", pid) } # forceful stop
101
+ end
102
+ else
103
+ last_status = @waiters.map(&:value)[-1]
104
+ end
105
+
106
+ @exit_code = last_status&.exitstatus
107
+ raise Exceptions::CommandTimeoutError, "execution expired" if timeout_occurred
108
+ rescue ::Errno::ECHILD, ::Errno::ESRCH
109
+ raise Exceptions::CommandTimeoutError, "execution expired" if timeout_occurred
110
+ ensure
111
+ @out_rd_reader&.wait
112
+ @err_rd_reader&.wait
113
+ end
114
+
115
+ # @return [String]
116
+ def inspect
117
+ format("#<Rubsh::RunningPipeline '%s'>", @prog_with_args)
118
+ end
119
+
120
+ # @!visibility private
121
+ def __add_running_command(cmd)
122
+ @rcmds << cmd
123
+ end
124
+
125
+ # @!visibility private
126
+ def __run(**kwargs)
127
+ raise Exceptions::CommandNotFoundError, format("no commands") if @rcmds.length == 0
128
+ extract_opts(**kwargs)
129
+ @_bg ? run_in_background : run_in_foreground
130
+ end
131
+
132
+ private
133
+
134
+ def extract_opts(**kwargs)
135
+ kwargs.each do |k, v|
136
+ raise ::ArgumentError, format("unsupported special kwarg `%s'", k) unless SPECIAL_KWARGS.include?(k.to_sym)
137
+ case k.to_sym
138
+ when :_in_data
139
+ @_in_data = v
140
+ when :_in
141
+ @_in = v
142
+ when :_out
143
+ @_out = v
144
+ when :_err
145
+ @_err = v
146
+ when :_err_to_out
147
+ @_err_to_out = v
148
+ when :_capture
149
+ @_capture = v
150
+ when :_bg
151
+ @_bg = v
152
+ when :_env
153
+ @_env = v.transform_keys(&:to_s).transform_values(&:to_s)
154
+ when :_timeout
155
+ @_timeout = v
156
+ when :_cwd
157
+ @_cwd = v
158
+ when :_ok_code
159
+ @_ok_code = [*v]
160
+ when :_out_bufsize
161
+ @_out_bufsize = v
162
+ when :_err_bufsize
163
+ @_err_bufsize = v
164
+ when :_no_out
165
+ @_no_out = v
166
+ when :_no_err
167
+ @_no_err = v
168
+ end
169
+ end
170
+ end
171
+
172
+ def compile_redirection_args
173
+ args = {}
174
+
175
+ if @_in
176
+ args[:in] = @_in
177
+ else
178
+ @in_rd, @in_wr = ::IO.pipe
179
+ @in_wr.sync = true
180
+ args[:in] = @in_rd.fileno
181
+ end
182
+
183
+ if @_out
184
+ args[:out] = @_out
185
+ else
186
+ @out_rd, @out_wr = ::IO.pipe
187
+ args[:out] = @out_wr.fileno
188
+ end
189
+
190
+ if @_err_to_out
191
+ args[:err] = [:child, :out]
192
+ elsif @_err
193
+ args[:err] = @_err
194
+ else
195
+ @err_rd, @err_wr = ::IO.pipe
196
+ args[:err] = @err_wr.fileno
197
+ end
198
+
199
+ args
200
+ end
201
+
202
+ def spawn
203
+ cmds = @rcmds.map { |r| r.__spawn_arguments(env: @_env, cwd: @_cwd, redirection_args: {}) }
204
+ @prog_with_args = @rcmds.map(&:__prog_with_args).join(" | ")
205
+ @waiters = ::Open3.pipeline_start(*cmds, compile_redirection_args)
206
+ @in_wr&.write(@_in_data) if @_in_data
207
+ @in_wr&.close
208
+
209
+ if @out_rd
210
+ @out_rd_reader = StreamReader.new(@out_rd, bufsize: @_capture ? @_out_bufsize : nil, &proc { |chunk|
211
+ @stdout_data << chunk unless @_no_out
212
+ @_capture&.call(chunk, nil)
213
+ })
214
+ end
215
+ if @err_rd
216
+ @err_rd_reader = StreamReader.new(@err_rd, bufsize: @_capture ? @_err_bufsize : nil, &proc { |chunk|
217
+ @stderr_data << chunk unless @_no_err
218
+ @_capture&.call(nil, chunk)
219
+ })
220
+ end
221
+ ensure
222
+ @in_rd&.close
223
+ @out_wr&.close
224
+ @err_wr&.close
225
+ end
226
+
227
+ def handle_return_code
228
+ return if @_ok_code.include?(@exit_code)
229
+ message = format("\n\n RAN: %s\n\n STDOUT:\n%s\n STDERR:\n%s\n", @prog_with_args, @stdout_data, @stderr_data)
230
+ raise Exceptions::CommandReturnFailureError.new(@exit_code, message)
231
+ end
232
+
233
+ def run_in_background
234
+ spawn
235
+ end
236
+
237
+ def run_in_foreground
238
+ spawn
239
+ wait(timeout: @_timeout)
240
+ handle_return_code
241
+ end
242
+ end
243
+ end
@@ -0,0 +1,15 @@
1
+ module Rubsh
2
+ class Shell
3
+ class Env
4
+ attr_reader :path
5
+
6
+ def initialize
7
+ @path = ::ENV["PATH"].split(::File::PATH_SEPARATOR)
8
+ end
9
+
10
+ def path=(path)
11
+ @path = [*path]
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,24 @@
1
+ module Rubsh
2
+ class Shell
3
+ # @!attribute [r] env
4
+ # @return [Env]
5
+ attr_reader :env
6
+
7
+ def initialize
8
+ @env = Env.new
9
+ end
10
+
11
+ # @return [Command]
12
+ def command(prog)
13
+ Command.new(self, prog)
14
+ end
15
+ alias_method :cmd, :command
16
+
17
+ # @return [RunningPipeline]
18
+ def pipeline(**kwarg)
19
+ r = RunningPipeline.new(self).tap { |x| yield x }
20
+ r.__run(**kwarg)
21
+ r
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,39 @@
1
+ module Rubsh
2
+ class StreamReader
3
+ BUFSIZE = 16 * 1024
4
+
5
+ def initialize(rd, bufsize: nil, &block)
6
+ @thr = ::Thread.new do
7
+ if ::Thread.current.respond_to?(:report_on_exception)
8
+ ::Thread.current.report_on_exception = false
9
+ end
10
+
11
+ readers = [rd]
12
+ while readers.any?
13
+ ready = ::IO.select(readers, nil, readers)
14
+ ready[0].each do |reader|
15
+ if bufsize.nil?
16
+ chunk = reader.readpartial(BUFSIZE)
17
+ elsif bufsize == 0
18
+ chunk = reader.readline
19
+ else
20
+ chunk = reader.read(bufsize)
21
+ raise ::EOFError if chunk.nil?
22
+ end
23
+
24
+ chunk.force_encoding(::Encoding.default_external)
25
+ block.call(chunk)
26
+ rescue ::EOFError, ::Errno::EPIPE, ::Errno::EIO
27
+ readers.delete(reader)
28
+ reader.close
29
+ end
30
+ end
31
+ end
32
+ end
33
+
34
+ # @return [void]
35
+ def wait
36
+ @thr.join
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubsh
4
+ VERSION = "2.1.4"
5
+ end
data/lib/rubsh.rb ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "rubsh/argument"
4
+ require_relative "rubsh/command"
5
+ require_relative "rubsh/exceptions"
6
+ require_relative "rubsh/option"
7
+ require_relative "rubsh/running_command"
8
+ require_relative "rubsh/running_pipeline"
9
+ require_relative "rubsh/shell/env"
10
+ require_relative "rubsh/shell"
11
+ require_relative "rubsh/stream_reader"
12
+ require_relative "rubsh/version"
data/rubsh.gemspec ADDED
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/rubsh/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "ruby-sh"
7
+ spec.version = Rubsh::VERSION
8
+ spec.authors = ["John Doe"]
9
+ spec.email = ["johndoe@example.com"]
10
+
11
+ spec.summary = "Rubsh (a.k.a. ruby-sh) - Inspired by python-sh, allows you to call any program as if it were a function."
12
+ spec.description = "Rubsh (a.k.a. ruby-sh) - Inspired by python-sh, allows you to call any program as if it were a function."
13
+ spec.homepage = "https://github.com/souk4711/rubsh"
14
+ spec.license = "MIT"
15
+ spec.required_ruby_version = ">= 2.7.0"
16
+
17
+ spec.metadata["homepage_uri"] = spec.homepage
18
+ spec.metadata["source_code_uri"] = "https://github.com/souk4711/rubsh"
19
+ spec.metadata["changelog_uri"] = "https://github.com/souk4711/rubsh"
20
+
21
+ # Specify which files should be added to the gem when it is released.
22
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
23
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
24
+ `git ls-files -z`.split("\x0").reject do |f|
25
+ (f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
26
+ end
27
+ end
28
+ spec.bindir = "exe"
29
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
30
+ spec.require_paths = ["lib"]
31
+
32
+ # Uncomment to register a new dependency of your gem
33
+ # spec.add_dependency "example-gem", "~> 1.0"
34
+
35
+ # For more information and examples about making a new gem, check out our
36
+ # guide at: https://bundler.io/guides/creating_gem.html
37
+ spec.metadata["rubygems_mfa_required"] = "true"
38
+ end
data/sig/rubsh.rbs ADDED
@@ -0,0 +1,4 @@
1
+ module Rubsh
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,73 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ruby-sh
3
+ version: !ruby/object:Gem::Version
4
+ version: 2.1.4
5
+ platform: ruby
6
+ authors:
7
+ - John Doe
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2022-06-05 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Rubsh (a.k.a. ruby-sh) - Inspired by python-sh, allows you to call any
14
+ program as if it were a function.
15
+ email:
16
+ - johndoe@example.com
17
+ executables: []
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - ".overcommit.yml"
22
+ - ".rspec"
23
+ - ".rubocop"
24
+ - ".rubocop.yml"
25
+ - ".yardopts"
26
+ - CODE_OF_CONDUCT.md
27
+ - Gemfile
28
+ - Gemfile.lock
29
+ - LICENSE.txt
30
+ - README.md
31
+ - Rakefile
32
+ - lib/rubsh.rb
33
+ - lib/rubsh/argument.rb
34
+ - lib/rubsh/command.rb
35
+ - lib/rubsh/exceptions.rb
36
+ - lib/rubsh/option.rb
37
+ - lib/rubsh/running_command.rb
38
+ - lib/rubsh/running_pipeline.rb
39
+ - lib/rubsh/shell.rb
40
+ - lib/rubsh/shell/env.rb
41
+ - lib/rubsh/stream_reader.rb
42
+ - lib/rubsh/version.rb
43
+ - rubsh.gemspec
44
+ - sig/rubsh.rbs
45
+ homepage: https://github.com/souk4711/rubsh
46
+ licenses:
47
+ - MIT
48
+ metadata:
49
+ homepage_uri: https://github.com/souk4711/rubsh
50
+ source_code_uri: https://github.com/souk4711/rubsh
51
+ changelog_uri: https://github.com/souk4711/rubsh
52
+ rubygems_mfa_required: 'true'
53
+ post_install_message:
54
+ rdoc_options: []
55
+ require_paths:
56
+ - lib
57
+ required_ruby_version: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: 2.7.0
62
+ required_rubygems_version: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ version: '0'
67
+ requirements: []
68
+ rubygems_version: 3.3.7
69
+ signing_key:
70
+ specification_version: 4
71
+ summary: Rubsh (a.k.a. ruby-sh) - Inspired by python-sh, allows you to call any program
72
+ as if it were a function.
73
+ test_files: []