process_executer 2.0.0 → 3.0.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: 658921c991aa6654711a48a4638e0de6d998f9b5bcdabf8314cad15bb7294a0f
4
- data.tar.gz: 7aee46a6fe1459cbd4e0bc9a67895a3c9946741b192e3766937b90ddad416c03
3
+ metadata.gz: a13a780c8c9064d19873068266b40be2a2a8ba7fe1d46866d6e8cb15806d5e53
4
+ data.tar.gz: e727aab59452dac6ef74819bff2462ad0e1eb56c5b0c1ff1f7e018aed5dbdf77
5
5
  SHA512:
6
- metadata.gz: c25a01c7d819932a0495b18fc332744c860df5d25c8234df15071ac3ee66a9f6bc6b6e0101e2b02bc0fb691af3a7ac5bf9a77a98565b44964abcbc3d63f4451e
7
- data.tar.gz: 915c369b55e999c726c22b2e3ef0b377ac0253afd115615d4cd2cb8ae17244c5f042bde4018bf31cb76ec30baf924761cb26e045577959b29e3f7aedcceddb65
6
+ metadata.gz: 6e349893ee5fbf19410e35e2a3a43907ccb1b1610f266788e0bd01b972ccc0e875fa2df95a9f9b90aefa0d8910f535a9d2ad34432332df07dbd58a33d299169d
7
+ data.tar.gz: 29d7455610df17c93b8616fcff42fe44077f46de3e855eba573b45071e8bd51322240763529e2c5b578191ff20ada709073bac2989e6bbfcca20e0687fac09f5
data/CHANGELOG.md CHANGED
@@ -5,6 +5,29 @@ All notable changes to the process_executer gem will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## v3.0.0 (2025-03-18)
9
+
10
+ [Full Changelog](https://github.com/main-branch/process_executer/compare/v2.0.0..v3.0.0)
11
+
12
+ Changes since v2.0.0:
13
+
14
+ * 3d337de feat: remove Options setter methods and add `with` method
15
+ * 706d78a docs: add a list the breaking changes for each major release in the README.md
16
+ * 2903c80 feat: report all option errors instead of just the first one
17
+ * 247150d feat!: do not capture stdout and stderr by default in `ProcessExecuter.run`
18
+ * 4b3ac02 feat: support redirection destinations in the form [:child, fd] and :close
19
+ * ed4620f docs: update README.md to highlight the important parts of this gem
20
+ * 48b4695 fix: allow Integer or IO are used as a redirection source
21
+ * 4424a44 feat!: remove the :merge option from ProcessExecuter.run
22
+ * 7257e5d chore: allow SpawnOptions to accept Integer and IO redirection sources
23
+ * 92441d0 chore: move all options related classes to a new Options module
24
+ * 92c096c chore: remove unneeded test file
25
+ * 91d0db3 feat: implement all possible redirection destinations
26
+ * a58af4a fix: fix complexity error reported by CodeClimate
27
+ * 66d97b7 chore: do not fail the CI build for low coverage on JRuby and TruffleRuby
28
+ * 2fb0ccf feat: refactor options classes
29
+ * bcf35d5 chore: do not fail the CI build for low coverage on JRuby and TruffleRuby
30
+
8
31
  ## v2.0.0 (2025-03-03)
9
32
 
10
33
  [Full Changelog](https://github.com/main-branch/process_executer/compare/v1.3.0..v2.0.0)
data/README.md CHANGED
@@ -10,10 +10,41 @@
10
10
  Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-%23FE5196?logo=conventionalcommits&logoColor=white)](https://conventionalcommits.org)
11
11
  [![Slack](https://img.shields.io/badge/slack-main--branch/process__executer-yellow.svg?logo=slack)](https://main-branch.slack.com/archives/C07NG2BPG8Y)
12
12
 
13
+ ProcessExecuter provides an enhanced API for executing commands in subprocesses,
14
+ extending Ruby's built-in `Process.spawn` functionality.
15
+
16
+ It has additional features like capturing output, handling timeouts, streaming output
17
+ to multiple destinations, and providing detailed result information.
18
+
19
+ This README documents the HEAD version of process_executer which may contain
20
+ unrelease information. To see the README for the version you are using, consult
21
+ RubyGems.org. Go to the [process_executer page in
22
+ RubyGems.org](https://rubygems.org/gems/process_executer), select your version, and
23
+ then click the "Documentation" link.
24
+
25
+ ## Requirements
26
+
27
+ * Ruby 3.1.0 or later
28
+ * Compatible with MRI 3.1+, TruffleRuby 24+, and JRuby 9.4+
29
+ * Works on Mac, Linux, and Windows platforms
30
+
31
+ ## Table of Contents
32
+
33
+ * [Requirements](#requirements)
34
+ * [Table of Contents](#table-of-contents)
13
35
  * [Usage](#usage)
14
- * [ProcessExecuter.run](#processexecuterrun)
15
36
  * [ProcessExecuter::MonitoredPipe](#processexecutermonitoredpipe)
37
+ * [ProcessExecuter::Result](#processexecuterresult)
16
38
  * [ProcessExecuter.spawn\_and\_wait](#processexecuterspawn_and_wait)
39
+ * [ProcessExecuter.run](#processexecuterrun)
40
+ * [Breaking Changes](#breaking-changes)
41
+ * [2.x](#2x)
42
+ * [`ProcessExecuter.spawn`](#processexecuterspawn)
43
+ * [`ProcessExecuter.run`](#processexecuterrun-1)
44
+ * [`ProcessExecuter::Result`](#processexecuterresult-1)
45
+ * [Other](#other)
46
+ * [3.x](#3x)
47
+ * [`ProcessExecuter.run`](#processexecuterrun-2)
17
48
  * [Installation](#installation)
18
49
  * [Contributing](#contributing)
19
50
  * [Reporting Issues](#reporting-issues)
@@ -25,53 +56,43 @@ Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-%23FE5196?log
25
56
 
26
57
  ## Usage
27
58
 
28
- [Full YARD documentation](https://rubydoc.info/gems/process_executer/) for this
29
- gem is hosted on RubyGems.org. Read below of an overview and several examples.
30
-
31
- This gem contains the following important classes:
32
-
33
- ### ProcessExecuter.run
34
-
35
- `ProcessExecuter.run` execute the given command as a subprocess blocking until it is finished.
36
-
37
- A Result object is returned which includes the process's status and output.
59
+ [Full YARD documentation](https://rubydoc.info/gems/process_executer/) for this gem
60
+ is hosted on RubyGems.org. Read below for an overview and several examples.
38
61
 
39
- Supports the same features as
40
- [Process.spawn](https://docs.ruby-lang.org/en/3.3/Process.html#method-c-spawn).
41
- In addition, it (1) blocks until the command has exited, (2) captures stdout and
42
- stderr to a buffer or file, and (3) can optionally kill the command if it exceeds
43
- a given timeout duration.
62
+ This gem contains two public classes and two public methods:
44
63
 
45
- This command takes two forms:
64
+ Classes:
46
65
 
47
- 1. When passing a single string the command is passed to a shell:
66
+ * `ProcessExecuter::MonitoredPipe`: allows use of any object with a `#write` method
67
+ or an array of objects as a redirection destination in `Process.spawn`
68
+ * `ProcessExecuter::Result`: an extension of `Process::Status` that includes more
69
+ information about the subprocess including timeout status, the command that was
70
+ run, the subprocess options given, and (in some cases) stdout and stderr captured
71
+ from the subprocess.
48
72
 
49
- `ProcessExecuter.run([env, ] command_line, options = {}) ->` {ProcessExecuter::Result}
73
+ Methods:
50
74
 
51
- 2. When passing an array of strings the command is run directly (bypassing the shell):
52
-
53
- `ProcessExecuter.run([env, ] exe_path, *args, options = {}) ->` {ProcessExecuter::Result}
54
-
55
- Argument env, if given, is a hash that affects ENV for the new process; see
56
- [Execution
57
- Environment](https://docs.ruby-lang.org/en/3.3/Process.html#module-Process-label-Execution+Environment).
58
-
59
- Argument options is a hash of options for the new process; see the options listed below.
60
-
61
- See comprehensive examples in the YARD documentation for this method.
75
+ * `ProcessExecuter.spawn_and_wait`: execute a subprocess and wait for it to exit with
76
+ an optional timeout. Supports the same interface and features as `Process.spawn`.
77
+ * `ProcessExecuter.run`: builds upon `.spawn_and_wait` adding (1) automatically
78
+ wrapping stdout and stderr destinations (if given) in a `MonitoredPipe` and (2)
79
+ raises errors for any problem executing the subprocess (can be turned off).
62
80
 
63
81
  ### ProcessExecuter::MonitoredPipe
64
82
 
65
- `ProcessExecuter::MonitoredPipe` streams data sent through a pipe to one or more writers.
83
+ `ProcessExecuter::MonitoredPipe` objects can be used as a redirection destination for
84
+ `Process.spawn` to stream output from a subprocess to one or more destinations.
85
+ Destinations are given in this class's initializer.
66
86
 
67
- When a new `MonitoredPipe` is created, a pipe is created (via IO.pipe) and
68
- a thread is created which reads data as it is written written to the pipe.
87
+ The destinations are all the redirection destinations allowed by `Process.spawn` plus
88
+ the following:
69
89
 
70
- Data that is read from the pipe is written one or more writers passed to
71
- `MonitoredPipe#initialize`.
90
+ * Any object with a #write method even if it does not have a file descriptor (like
91
+ instances of StringIO)
92
+ * An array of destinations so that output can be tee'd to several sources
72
93
 
73
- This is useful for streaming process output (stdout and/or stderr) to anything that has a
74
- `#write` method: a string buffer, a file, or stdout/stderr as seen in the following example:
94
+ Example of capturing stdout to a StringIO (which is not directly possible with
95
+ `Process.spawn`):
75
96
 
76
97
  ```ruby
77
98
  require 'stringio'
@@ -80,11 +101,15 @@ require 'process_executer'
80
101
  output_buffer = StringIO.new
81
102
  out_pipe = ProcessExecuter::MonitoredPipe.new(output_buffer)
82
103
  pid, status = Process.wait2(Process.spawn('echo "Hello World"', out: out_pipe))
104
+ out_pipe.close # Close the pipe so all the data is flushed and resources are not leaked
83
105
  output_buffer.string #=> "Hello World\n"
84
106
  ```
85
107
 
86
- `MonitoredPipe#initialize` can take more than one writer so that pipe output can be
87
- streamed (or `tee`d) to multiple writers at the same time:
108
+ Any object that implements `#write` can be used as a destination (not just StringIO).
109
+ For instance, you can use it to parse process output as a stream which might be useful
110
+ for long XML or JSON output.
111
+
112
+ Example of tee'ing stdout to multiple destinations:
88
113
 
89
114
  ```ruby
90
115
  require 'stringio'
@@ -92,24 +117,42 @@ require 'process_executer'
92
117
 
93
118
  output_buffer = StringIO.new
94
119
  output_file = File.open('process.out', 'w')
95
- out_pipe = ProcessExecuter::MonitoredPipe.new(output_buffer, output_file)
120
+ out_pipe = ProcessExecuter::MonitoredPipe.new([:tee, output_buffer, output_file])
96
121
  pid, status = Process.wait2(Process.spawn('echo "Hello World"', out: out_pipe))
122
+ out_pipe.close
97
123
  output_file.close
98
124
  output_buffer.string #=> "Hello World\n"
99
125
  File.read('process.out') #=> "Hello World\n"
100
126
  ```
101
127
 
102
- Since the data is streamed, any object that implements `#write` can be used. For
103
- insance, you can use it to parse process output as a stream which might be useful for
104
- long XML or JSON output.
128
+ ### ProcessExecuter::Result
129
+
130
+ An instance of this class is returned from both `.spawn_and_wait` and `.run`.
131
+
132
+ This class is an extension of
133
+ [Process::Status](https://docs.ruby-lang.org/en/3.3/Process/Status.html) so it
134
+ supports the same interface with the following additions:
135
+
136
+ * `#command`: the command given to `.spawn_and_wait` or `.run`
137
+ * `#options`: the options given to `.spawn_and_wait` or `.run` (possibly with some
138
+ changes)
139
+ * `#timed_out?`: true if the process was killed after running for `:timeout_after`
140
+ seconds
141
+ * `#elapsed_time`: the number of seconds the process was running
142
+ * `#stdout`: the captured stdout from the subprocess (if the stdout destination was
143
+ wrapped by a `MonitoredPipe`)
144
+ * `#stderr`: the captured stderr from the subprocess (if the stderr destination was
145
+ wrapped by a `MonitoredPipe`)
105
146
 
106
147
  ### ProcessExecuter.spawn_and_wait
107
148
 
108
- `ProcessExecuter.spawn` has the same interface as `Process.spawn` but has two
109
- important behaviorial differences:
149
+ `ProcessExecuter.spawn_and_wait` has the same interface and features as
150
+ [Process.spawn](https://docs.ruby-lang.org/en/3.3/Process.html#method-c-spawn)
151
+ with the following differences:
110
152
 
111
- 1. It blocks until the subprocess finishes
153
+ 1. It waits for the subprocess to exit
112
154
  2. A timeout can be specified using the `:timeout_after` option
155
+ 3. It returns a `ProcessExecuter::Result` instead of a `Process::Status`
113
156
 
114
157
  If the command does not terminate before the number of seconds specified by
115
158
  `:timeout_after`, the process is killed by sending it the SIGKILL signal. The
@@ -122,6 +165,73 @@ result.termsig #=> 9
122
165
  result.timed_out? #=> true
123
166
  ```
124
167
 
168
+ If the destination for stdout and stderr are wrapped by a
169
+ ProcessExecuter::MonitoredPipe, the result will return the stdout and stderr
170
+ subprocess output from its `#stdout` and `#stderr` methods.
171
+
172
+ ### ProcessExecuter.run
173
+
174
+ `ProcessExecuter.run` builds upon `ProcessExecuter.spawn_and_wait` adding the
175
+ following features:
176
+
177
+ * It automatically wraps any given stdout and stderr destination with a
178
+ MonitoredPipe. The pipe will be closed when the command exits.
179
+ * It raises an error if there is any problem with the subprocess. This behavior can
180
+ be turned off with the `raise_errors: false` option.
181
+
182
+ ```ruby
183
+ result = ProcessExecuter.run('echo "Hello World"', out: StringIO.new)
184
+ result.stdout #=> "Hello World\n"
185
+ ```
186
+
187
+ ## Breaking Changes
188
+
189
+ ### 2.x
190
+
191
+ This major release focused on changes to the interface to make it more understandable.
192
+
193
+ #### `ProcessExecuter.spawn`
194
+
195
+ * This method was renamed to `ProcessExecuter.spawn_and_wait`
196
+ * The `:timeout` option was renamed to `:timeout_after`
197
+
198
+ #### `ProcessExecuter.run`
199
+
200
+ * The `:timeout` option was renamed to `:timeout_after`
201
+
202
+ #### `ProcessExecuter::Result`
203
+
204
+ * The `#timeout` method was renamed to `#timed_out`
205
+
206
+ #### Other
207
+
208
+ * Dropped support for Ruby 3.0
209
+
210
+ ### 3.x
211
+
212
+ #### `ProcessExecuter.run`
213
+
214
+ * The `:merge` option was removed
215
+
216
+ This was removed because `Process.spawn` already provides this functionality but in
217
+ a different way. To merge, you will need to define a redirection where the source
218
+ is an array of the file descriptors you want to merge. For instance:
219
+
220
+ ```Ruby
221
+ [:out, :err] => 'output.txt'
222
+ ```
223
+
224
+ will merge stdout and stderr from the subprocess into the file output.txt.
225
+
226
+ * Stdout and stderr redirections are no longer default to a new instance of StringIO
227
+
228
+ Calls to `ProcessExecuter.run` that do not define a redirection for stdout or
229
+ stderr will have to add explicit redirection(s) in order to capture the output.
230
+
231
+ This is to align with the functionality in `Process.spawn`. In `Process.spawn`, when
232
+ an explicit redirection is not given for stdout and stderr, this output will be
233
+ passed through to the parent process's stdout and stderr.
234
+
125
235
  ## Installation
126
236
 
127
237
  Install the gem and add to the application's Gemfile by executing:
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ProcessExecuter
4
+ # Base class for all destination handlers
5
+ #
6
+ # Provides the common interface and functionality for all destination
7
+ # classes that handle different types of output redirection.
8
+ #
9
+ # @api private
10
+ class DestinationBase
11
+ # Initializes a new destination handler
12
+ #
13
+ # @param destination [Object] the destination to write to
14
+ # @return [DestinationBase] a new destination handler instance
15
+ def initialize(destination)
16
+ @destination = destination
17
+ @data_written = []
18
+ end
19
+
20
+ # The destination object this handler manages
21
+ #
22
+ # @return [Object] the destination object
23
+ attr_reader :destination
24
+
25
+ # The data written to the destination
26
+ #
27
+ # @return [Array<String>] the data written to the destination
28
+ attr_reader :data_written
29
+
30
+ # The data written to the destination as a single string
31
+ # @return [String]
32
+ def string
33
+ data_written.join
34
+ end
35
+
36
+ # Writes data to the destination
37
+ #
38
+ # This is an abstract method that must be implemented by subclasses.
39
+ #
40
+ # @param data [String] the data to write
41
+ # @return [void]
42
+ # @raise [NotImplementedError] if the subclass doesn't implement this method
43
+ def write(data)
44
+ @data_written << data
45
+ end
46
+
47
+ # Closes the destination if necessary
48
+ #
49
+ # By default, this method does nothing. Subclasses should override
50
+ # this method if they need to perform cleanup.
51
+ #
52
+ # @return [void]
53
+ def close; end
54
+
55
+ # Determines if this class can handle the given destination
56
+ #
57
+ # This is an abstract class method that must be implemented by subclasses.
58
+ #
59
+ # @param destination [Object] the destination to check
60
+ # @return [Boolean] true if this class can handle the destination
61
+ # @raise [NotImplementedError] if the subclass doesn't implement this method
62
+ def self.handles?(destination)
63
+ raise NotImplementedError
64
+ end
65
+
66
+ # Determines if this destination class can be wrapped by MonitoredPipe
67
+ #
68
+ # All destination types can be wrapped by MonitoredPipe unless they explicitly
69
+ # opt out.
70
+ #
71
+ # @return [Boolean]
72
+ # @api private
73
+ def self.compatible_with_monitored_pipe? = true
74
+
75
+ # Determines if this destination instance can be wrapped by MonitoredPipe
76
+ #
77
+ # @return [Boolean]
78
+ # @api private
79
+ def compatible_with_monitored_pipe?
80
+ self.class.compatible_with_monitored_pipe?
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ProcessExecuter
4
+ module Destinations
5
+ # Handles generic objects that respond to write
6
+ #
7
+ # @api private
8
+ class ChildRedirection < ProcessExecuter::DestinationBase
9
+ # Determines if this class can handle the given destination
10
+ #
11
+ # @param destination [Object] the destination to check
12
+ # @return [Boolean] true if destination responds to write but is not an IO with fileno
13
+ def self.handles?(destination)
14
+ destination.is_a?(Array) && destination.size == 2 && destination[0] == :child
15
+ end
16
+
17
+ # This class should not be wrapped in a monitored pipe
18
+ # @return [Boolean]
19
+ # @api private
20
+ def self.compatible_with_monitored_pipe? = false
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ProcessExecuter
4
+ module Destinations
5
+ # Handles generic objects that respond to write
6
+ #
7
+ # @api private
8
+ class Close < ProcessExecuter::DestinationBase
9
+ # Determines if this class can handle the given destination
10
+ #
11
+ # @param destination [Object] the destination to check
12
+ # @return [Boolean] true if destination responds to write but is not an IO with fileno
13
+ def self.handles?(destination)
14
+ destination == :close
15
+ end
16
+
17
+ # This class should not be wrapped in a monitored pipe
18
+ # @return [Boolean]
19
+ # @api private
20
+ def self.compatible_with_monitored_pipe? = false
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'process_executer/destination_base'
4
+
5
+ module ProcessExecuter
6
+ module Destinations
7
+ # Handles numeric file descriptors
8
+ #
9
+ # @api private
10
+ class FileDescriptor < ProcessExecuter::DestinationBase
11
+ # Writes data to the file descriptor
12
+ #
13
+ # @param data [String] the data to write
14
+ # @return [Integer] the number of bytes written
15
+ # @raise [SystemCallError] if the file descriptor is invalid
16
+ #
17
+ # @example
18
+ # fd_handler = ProcessExecuter::Destinations::FileDescriptor.new(3)
19
+ # fd_handler.write("Hello world")
20
+ def write(data)
21
+ super
22
+ io = ::IO.open(destination, mode: 'a', autoclose: false)
23
+ io.write(data)
24
+ io.close
25
+ end
26
+
27
+ # Determines if this class can handle the given destination
28
+ #
29
+ # @param destination [Object] the destination to check
30
+ # @return [Boolean] true if destination is an Integer that's not stdout/stderr
31
+ def self.handles?(destination)
32
+ destination.is_a?(Integer) && ![:out, 1, :err, 2].include?(destination)
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ProcessExecuter
4
+ module Destinations
5
+ # Handles file path destinations
6
+ #
7
+ # @api private
8
+ class FilePath < ProcessExecuter::DestinationBase
9
+ # Initializes a new file path destination handler
10
+ #
11
+ # Opens the file at the given path for writing.
12
+ #
13
+ # @param destination [String] the file path to write to
14
+ # @return [FilePath] a new file path destination handler
15
+ # @raise [Errno::ENOENT] if the file path is invalid
16
+ def initialize(destination)
17
+ super
18
+ @file = File.open(destination, 'w', 0o644)
19
+ end
20
+
21
+ # The opened file object
22
+ #
23
+ # @return [File] the opened file
24
+ attr_reader :file
25
+
26
+ # Writes data to the file
27
+ #
28
+ # @param data [String] the data to write
29
+ # @return [Integer] the number of bytes written
30
+ # @raise [IOError] if the file is closed
31
+ #
32
+ # @example
33
+ # file_handler = ProcessExecuter::Destinations::FilePath.new("output.log")
34
+ # file_handler.write("Log entry")
35
+ def write(data)
36
+ super
37
+ file.write data
38
+ end
39
+
40
+ # Closes the file if it's open
41
+ #
42
+ # @return [void]
43
+ def close
44
+ file.close unless file.closed?
45
+ end
46
+
47
+ # Determines if this class can handle the given destination
48
+ #
49
+ # @param destination [Object] the destination to check
50
+ # @return [Boolean] true if destination is a String
51
+ def self.handles?(destination)
52
+ destination.is_a? String
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ProcessExecuter
4
+ module Destinations
5
+ # Handles file paths with specific open modes
6
+ #
7
+ # @api private
8
+ class FilePathMode < ProcessExecuter::DestinationBase
9
+ # Initializes a new file path with mode destination handler
10
+ #
11
+ # Opens the file at the given path with the specified mode.
12
+ #
13
+ # @param destination [Array<String, String>] array with file path and mode
14
+ # @return [FilePathMode] a new file path with mode destination handler
15
+ # @raise [Errno::ENOENT] if the file path is invalid
16
+ # @raise [ArgumentError] if the mode is invalid
17
+ def initialize(destination)
18
+ super
19
+ @file = File.open(destination[0], destination[1], 0o644)
20
+ end
21
+
22
+ # The opened file object
23
+ #
24
+ # @return [File] the opened file
25
+ attr_reader :file
26
+
27
+ # Writes data to the file
28
+ #
29
+ # @param data [String] the data to write
30
+ # @return [Integer] the number of bytes written
31
+ # @raise [IOError] if the file is closed
32
+ #
33
+ # @example
34
+ # mode_handler = ProcessExecuter::Destinations::FilePathMode.new(["output.log", "a"])
35
+ # mode_handler.write("Appended log entry")
36
+ def write(data)
37
+ super
38
+ file.write data
39
+ end
40
+
41
+ # Closes the file if it's open
42
+ #
43
+ # @return [void]
44
+ def close
45
+ file.close unless file.closed?
46
+ end
47
+
48
+ # Determines if this class can handle the given destination
49
+ #
50
+ # @param destination [Object] the destination to check
51
+ # @return [Boolean] true if destination is an Array with path and mode
52
+ def self.handles?(destination)
53
+ destination.is_a?(Array) &&
54
+ destination.size == 2 &&
55
+ destination[0].is_a?(String) &&
56
+ destination[1].is_a?(String)
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ProcessExecuter
4
+ module Destinations
5
+ # Handles file paths with specific open modes and permissions
6
+ #
7
+ # @api private
8
+ class FilePathModePerms < ProcessExecuter::DestinationBase
9
+ # Initializes a new file path with mode and permissions destination handler
10
+ #
11
+ # Opens the file at the given path with the specified mode and permissions.
12
+ #
13
+ # @param destination [Array<String, String, Integer>] array with file path, mode, and permissions
14
+ # @return [FilePathModePerms] a new handler instance
15
+ # @raise [Errno::ENOENT] if the file path is invalid
16
+ # @raise [ArgumentError] if the mode is invalid
17
+ def initialize(destination)
18
+ super
19
+ @file = File.open(destination[0], destination[1], destination[2])
20
+ end
21
+
22
+ # The opened file object
23
+ #
24
+ # @return [File] the opened file
25
+ attr_reader :file
26
+
27
+ # Writes data to the file
28
+ #
29
+ # @param data [String] the data to write
30
+ # @return [Integer] the number of bytes written
31
+ # @raise [IOError] if the file is closed
32
+ #
33
+ # @example
34
+ # perms_handler = ProcessExecuter::Destinations::FilePathModePerms.new(["output.log", "w", 0644])
35
+ # perms_handler.write("Log entry with specific permissions")
36
+ def write(data)
37
+ super
38
+ file.write data
39
+ end
40
+
41
+ # Closes the file if it's open
42
+ #
43
+ # @return [void]
44
+ def close
45
+ file.close unless file.closed?
46
+ end
47
+
48
+ # Determines if this class can handle the given destination
49
+ #
50
+ # @param destination [Object] the destination to check
51
+ # @return [Boolean] true if destination is an Array with path, mode, and permissions
52
+ def self.handles?(destination)
53
+ destination.is_a?(Array) &&
54
+ destination.size == 3 &&
55
+ destination[0].is_a?(String) &&
56
+ destination[1].is_a?(String) &&
57
+ destination[2].is_a?(Integer)
58
+ end
59
+ end
60
+ end
61
+ end