philiprehberger-task_runner 0.1.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 +7 -0
- data/CHANGELOG.md +18 -0
- data/LICENSE +21 -0
- data/README.md +85 -0
- data/lib/philiprehberger/task_runner/result.rb +38 -0
- data/lib/philiprehberger/task_runner/version.rb +7 -0
- data/lib/philiprehberger/task_runner.rb +102 -0
- metadata +55 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 99f699cbe02fe85cc5b4d5054b1751bce49d30b377e249fcf343da080a76b948
|
|
4
|
+
data.tar.gz: aeb84e373540392e271d0932e0b1000bdb4819fcb08d3b7bf1cf98b9ff044e65
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: d34d6526d118009beca3959b6560a7099c88b6a2fcc0143d0fcc542a77ec0617b8eadf92682759be240f8bd58a4cb09e93bc3171cec4bda80b13a0c5ab76e5e0
|
|
7
|
+
data.tar.gz: c8dce1f7683c24f6890f36e0f2e10efe2650fb3d2ae366d6c5dc6e1713bf61a049fde22bc475f03612fad1acb4315ba9b2271cb74127636ca1bb8806062b0e04
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this gem will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.1.0] - 2026-03-22
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Initial release
|
|
14
|
+
- Shell command execution with stdout and stderr capture
|
|
15
|
+
- Exit code and duration measurement on Result object
|
|
16
|
+
- Configurable timeout with TimeoutError
|
|
17
|
+
- Environment variable and working directory options
|
|
18
|
+
- Block-based streaming for line-by-line stdout processing
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 philiprehberger
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# philiprehberger-task_runner
|
|
2
|
+
|
|
3
|
+
[](https://github.com/philiprehberger/rb-task-runner/actions/workflows/ci.yml)
|
|
4
|
+
[](https://rubygems.org/gems/philiprehberger-task_runner)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
Shell command runner with output capture, timeout, and streaming.
|
|
8
|
+
|
|
9
|
+
## Requirements
|
|
10
|
+
|
|
11
|
+
- Ruby >= 3.1
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
Add to your Gemfile:
|
|
16
|
+
|
|
17
|
+
```ruby
|
|
18
|
+
gem "philiprehberger-task_runner"
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Or install directly:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
gem install philiprehberger-task_runner
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
```ruby
|
|
30
|
+
require "philiprehberger/task_runner"
|
|
31
|
+
|
|
32
|
+
result = Philiprehberger::TaskRunner.run('ls', '-la')
|
|
33
|
+
puts result.stdout
|
|
34
|
+
puts result.exit_code # => 0
|
|
35
|
+
puts result.success? # => true
|
|
36
|
+
puts result.duration # => 0.012
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Timeout
|
|
40
|
+
|
|
41
|
+
```ruby
|
|
42
|
+
result = Philiprehberger::TaskRunner.run('long-process', timeout: 30)
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Environment Variables and Working Directory
|
|
46
|
+
|
|
47
|
+
```ruby
|
|
48
|
+
result = Philiprehberger::TaskRunner.run(
|
|
49
|
+
'make', 'build',
|
|
50
|
+
env: { 'DEBUG' => '1' },
|
|
51
|
+
chdir: '/path/to/project'
|
|
52
|
+
)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Streaming Output
|
|
56
|
+
|
|
57
|
+
```ruby
|
|
58
|
+
Philiprehberger::TaskRunner.run('tail', '-f', '/var/log/app.log', timeout: 10) do |line|
|
|
59
|
+
puts ">> #{line}"
|
|
60
|
+
end
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## API
|
|
64
|
+
|
|
65
|
+
| Method / Class | Description |
|
|
66
|
+
|----------------|-------------|
|
|
67
|
+
| `.run(cmd, *args, timeout:, env:, chdir:)` | Run a command and return a Result |
|
|
68
|
+
| `.run(cmd) { \|line\| ... }` | Run with line-by-line stdout streaming |
|
|
69
|
+
| `Result#stdout` | Captured standard output |
|
|
70
|
+
| `Result#stderr` | Captured standard error |
|
|
71
|
+
| `Result#exit_code` | Process exit code |
|
|
72
|
+
| `Result#success?` | Whether exit code is 0 |
|
|
73
|
+
| `Result#duration` | Execution time in seconds |
|
|
74
|
+
|
|
75
|
+
## Development
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
bundle install
|
|
79
|
+
bundle exec rspec # Run tests
|
|
80
|
+
bundle exec rubocop # Check code style
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## License
|
|
84
|
+
|
|
85
|
+
MIT
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Philiprehberger
|
|
4
|
+
module TaskRunner
|
|
5
|
+
# Represents the result of a shell command execution.
|
|
6
|
+
class Result
|
|
7
|
+
# @return [String] standard output
|
|
8
|
+
attr_reader :stdout
|
|
9
|
+
|
|
10
|
+
# @return [String] standard error
|
|
11
|
+
attr_reader :stderr
|
|
12
|
+
|
|
13
|
+
# @return [Integer] process exit code
|
|
14
|
+
attr_reader :exit_code
|
|
15
|
+
|
|
16
|
+
# @return [Float] execution duration in seconds
|
|
17
|
+
attr_reader :duration
|
|
18
|
+
|
|
19
|
+
# @param stdout [String]
|
|
20
|
+
# @param stderr [String]
|
|
21
|
+
# @param exit_code [Integer]
|
|
22
|
+
# @param duration [Float]
|
|
23
|
+
def initialize(stdout:, stderr:, exit_code:, duration:)
|
|
24
|
+
@stdout = stdout
|
|
25
|
+
@stderr = stderr
|
|
26
|
+
@exit_code = exit_code
|
|
27
|
+
@duration = duration
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Whether the command exited successfully.
|
|
31
|
+
#
|
|
32
|
+
# @return [Boolean]
|
|
33
|
+
def success?
|
|
34
|
+
@exit_code == 0
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'open3'
|
|
4
|
+
require 'timeout'
|
|
5
|
+
|
|
6
|
+
require_relative 'task_runner/version'
|
|
7
|
+
require_relative 'task_runner/result'
|
|
8
|
+
|
|
9
|
+
module Philiprehberger
|
|
10
|
+
module TaskRunner
|
|
11
|
+
class Error < StandardError; end
|
|
12
|
+
class TimeoutError < Error; end
|
|
13
|
+
|
|
14
|
+
# Run a shell command with output capture, optional timeout, and streaming.
|
|
15
|
+
#
|
|
16
|
+
# When a block is given, each line of stdout is yielded as it arrives.
|
|
17
|
+
#
|
|
18
|
+
# @param cmd [String] the command to execute
|
|
19
|
+
# @param args [Array<String>] additional command arguments
|
|
20
|
+
# @param timeout [Numeric, nil] maximum seconds to wait (nil for no timeout)
|
|
21
|
+
# @param env [Hash, nil] environment variables to set
|
|
22
|
+
# @param chdir [String, nil] working directory for the command
|
|
23
|
+
# @yield [line] each line of stdout as it arrives (streaming mode)
|
|
24
|
+
# @yieldparam line [String] a line of output
|
|
25
|
+
# @return [Result] the command result
|
|
26
|
+
# @raise [TimeoutError] if the command exceeds the timeout
|
|
27
|
+
def self.run(cmd, *args, timeout: nil, env: nil, chdir: nil, &block)
|
|
28
|
+
full_cmd = args.empty? ? cmd : [cmd, *args]
|
|
29
|
+
spawn_opts = {}
|
|
30
|
+
spawn_opts[:chdir] = chdir if chdir
|
|
31
|
+
|
|
32
|
+
env_hash = env || {}
|
|
33
|
+
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
34
|
+
|
|
35
|
+
if block
|
|
36
|
+
run_streaming(env_hash, full_cmd, spawn_opts, timeout, start_time, &block)
|
|
37
|
+
else
|
|
38
|
+
run_capture(env_hash, full_cmd, spawn_opts, timeout, start_time)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# @api private
|
|
43
|
+
def self.run_capture(env_hash, full_cmd, spawn_opts, timeout, start_time)
|
|
44
|
+
stdout, stderr, status = if timeout
|
|
45
|
+
::Timeout.timeout(timeout, TimeoutError, 'command timed out') do
|
|
46
|
+
Open3.capture3(env_hash, *Array(full_cmd), **spawn_opts)
|
|
47
|
+
end
|
|
48
|
+
else
|
|
49
|
+
Open3.capture3(env_hash, *Array(full_cmd), **spawn_opts)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time
|
|
53
|
+
Result.new(stdout: stdout, stderr: stderr, exit_code: status.exitstatus || 1, duration: duration)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# @api private
|
|
57
|
+
def self.run_streaming(env_hash, full_cmd, spawn_opts, timeout, start_time, &block)
|
|
58
|
+
stdout_buf = +''
|
|
59
|
+
stderr_buf = +''
|
|
60
|
+
exit_status = nil
|
|
61
|
+
|
|
62
|
+
Open3.popen3(env_hash, *Array(full_cmd), **spawn_opts) do |_stdin, stdout, stderr, wait_thr|
|
|
63
|
+
_stdin.close
|
|
64
|
+
|
|
65
|
+
if timeout
|
|
66
|
+
::Timeout.timeout(timeout, TimeoutError, 'command timed out') do
|
|
67
|
+
read_streams(stdout, stderr, stdout_buf, stderr_buf, &block)
|
|
68
|
+
exit_status = wait_thr.value
|
|
69
|
+
end
|
|
70
|
+
else
|
|
71
|
+
read_streams(stdout, stderr, stdout_buf, stderr_buf, &block)
|
|
72
|
+
exit_status = wait_thr.value
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time
|
|
77
|
+
Result.new(
|
|
78
|
+
stdout: stdout_buf,
|
|
79
|
+
stderr: stderr_buf,
|
|
80
|
+
exit_code: exit_status&.exitstatus || 1,
|
|
81
|
+
duration: duration
|
|
82
|
+
)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# @api private
|
|
86
|
+
def self.read_streams(stdout, stderr, stdout_buf, stderr_buf)
|
|
87
|
+
threads = []
|
|
88
|
+
threads << Thread.new do
|
|
89
|
+
stdout.each_line do |line|
|
|
90
|
+
stdout_buf << line
|
|
91
|
+
yield line
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
threads << Thread.new do
|
|
95
|
+
stderr_buf << stderr.read
|
|
96
|
+
end
|
|
97
|
+
threads.each(&:join)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
private_class_method :run_capture, :run_streaming, :read_streams
|
|
101
|
+
end
|
|
102
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: philiprehberger-task_runner
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Philip Rehberger
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-03-22 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description: Run shell commands with captured stdout/stderr, exit code, duration measurement,
|
|
14
|
+
configurable timeout, environment variables, and line-by-line streaming via blocks.
|
|
15
|
+
email:
|
|
16
|
+
- me@philiprehberger.com
|
|
17
|
+
executables: []
|
|
18
|
+
extensions: []
|
|
19
|
+
extra_rdoc_files: []
|
|
20
|
+
files:
|
|
21
|
+
- CHANGELOG.md
|
|
22
|
+
- LICENSE
|
|
23
|
+
- README.md
|
|
24
|
+
- lib/philiprehberger/task_runner.rb
|
|
25
|
+
- lib/philiprehberger/task_runner/result.rb
|
|
26
|
+
- lib/philiprehberger/task_runner/version.rb
|
|
27
|
+
homepage: https://github.com/philiprehberger/rb-task-runner
|
|
28
|
+
licenses:
|
|
29
|
+
- MIT
|
|
30
|
+
metadata:
|
|
31
|
+
homepage_uri: https://github.com/philiprehberger/rb-task-runner
|
|
32
|
+
source_code_uri: https://github.com/philiprehberger/rb-task-runner
|
|
33
|
+
changelog_uri: https://github.com/philiprehberger/rb-task-runner/blob/main/CHANGELOG.md
|
|
34
|
+
bug_tracker_uri: https://github.com/philiprehberger/rb-task-runner/issues
|
|
35
|
+
rubygems_mfa_required: 'true'
|
|
36
|
+
post_install_message:
|
|
37
|
+
rdoc_options: []
|
|
38
|
+
require_paths:
|
|
39
|
+
- lib
|
|
40
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
41
|
+
requirements:
|
|
42
|
+
- - ">="
|
|
43
|
+
- !ruby/object:Gem::Version
|
|
44
|
+
version: 3.1.0
|
|
45
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
46
|
+
requirements:
|
|
47
|
+
- - ">="
|
|
48
|
+
- !ruby/object:Gem::Version
|
|
49
|
+
version: '0'
|
|
50
|
+
requirements: []
|
|
51
|
+
rubygems_version: 3.5.22
|
|
52
|
+
signing_key:
|
|
53
|
+
specification_version: 4
|
|
54
|
+
summary: Shell command runner with output capture, timeout, and streaming
|
|
55
|
+
test_files: []
|