ruby-sh 2.1.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []