philiprehberger-task_runner 0.5.0 → 0.7.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 +4 -4
- data/CHANGELOG.md +12 -0
- data/README.md +39 -0
- data/lib/philiprehberger/task_runner/result.rb +27 -0
- data/lib/philiprehberger/task_runner/version.rb +1 -1
- data/lib/philiprehberger/task_runner.rb +34 -0
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: dddebb2a9f61387b45cc9650f3d695c07be69388f6d2b988f551326b5cfba973
|
|
4
|
+
data.tar.gz: 2d53404bcb1eb3fbbb941091d8c7b4a4e68515fe224c6891b0473c9a609d0773
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b8d1d3691823020fdd69b5d7642fe61d26415c654f6d26d3069755aa4ee95e09a8fa3f7bd34ffc9fb004d67366c7806a297aa4a7890251a7cce421c9e70ae3f4
|
|
7
|
+
data.tar.gz: 5a0123b5b514d3d07b1d9ade8e02137a92773564253f2fc7331531de844190ff3f921e732c794fd0b5d8c86c1d393baed9281d0d4fe8342c7716d8c00333fd71
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.7.0] - 2026-05-20
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- `Result#parse_json` — parse stdout as JSON, raising `Philiprehberger::TaskRunner::ParseError` (new) when stdout is empty or not valid JSON
|
|
14
|
+
- `Result#json?` — predicate for safely probing whether stdout is parseable JSON
|
|
15
|
+
- Card image reference in the README for registry-side rendering
|
|
16
|
+
|
|
17
|
+
## [0.6.0] - 2026-04-28
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
- `TaskRunner.which(cmd)` — absolute path of an executable on `PATH`, or `nil` when not found. Honors `ENV['PATHEXT']` on Windows. Raises `ArgumentError` for nil/empty input.
|
|
21
|
+
|
|
10
22
|
## [0.5.0] - 2026-04-21
|
|
11
23
|
|
|
12
24
|
### Added
|
data/README.md
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
[](https://rubygems.org/gems/philiprehberger-task_runner)
|
|
5
5
|
[](https://github.com/philiprehberger/rb-task-runner/commits/main)
|
|
6
6
|
|
|
7
|
+

|
|
8
|
+
|
|
7
9
|
Shell command runner with output capture, timeout, streaming, signal handling, and stdin piping
|
|
8
10
|
|
|
9
11
|
## Requirements
|
|
@@ -60,6 +62,22 @@ if Philiprehberger::TaskRunner.run?('which', 'git')
|
|
|
60
62
|
end
|
|
61
63
|
```
|
|
62
64
|
|
|
65
|
+
### Which
|
|
66
|
+
|
|
67
|
+
Locate an executable on `PATH` (like the `which` shell builtin). Returns the
|
|
68
|
+
absolute path or `nil` when not found:
|
|
69
|
+
|
|
70
|
+
```ruby
|
|
71
|
+
Philiprehberger::TaskRunner.which("git")
|
|
72
|
+
# => "/usr/bin/git"
|
|
73
|
+
|
|
74
|
+
Philiprehberger::TaskRunner.which("definitely-not-installed")
|
|
75
|
+
# => nil
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
On Windows, candidate suffixes from `ENV["PATHEXT"]` (`.COM`, `.EXE`,
|
|
79
|
+
`.BAT`, `.CMD`) are tried automatically.
|
|
80
|
+
|
|
63
81
|
### Timeout
|
|
64
82
|
|
|
65
83
|
```ruby
|
|
@@ -118,6 +136,24 @@ Philiprehberger::TaskRunner.run('make', 'build') do |line, stream|
|
|
|
118
136
|
end
|
|
119
137
|
```
|
|
120
138
|
|
|
139
|
+
### Parsing JSON Output
|
|
140
|
+
|
|
141
|
+
```ruby
|
|
142
|
+
result = Philiprehberger::TaskRunner.run('kubectl', 'get', 'pod', 'web', '-o', 'json')
|
|
143
|
+
|
|
144
|
+
if result.json?
|
|
145
|
+
doc = result.parse_json
|
|
146
|
+
doc['status']['phase'] # => "Running"
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# parse_json raises ParseError when stdout isn't valid JSON
|
|
150
|
+
begin
|
|
151
|
+
result.parse_json
|
|
152
|
+
rescue Philiprehberger::TaskRunner::ParseError => e
|
|
153
|
+
warn "command did not return JSON: #{e.message}"
|
|
154
|
+
end
|
|
155
|
+
```
|
|
156
|
+
|
|
121
157
|
## API
|
|
122
158
|
|
|
123
159
|
| Method / Class | Description |
|
|
@@ -125,6 +161,7 @@ end
|
|
|
125
161
|
| `.run(cmd, *args, timeout:, env:, chdir:, signal:, kill_after:, stdin:)` | Run a command and return a Result |
|
|
126
162
|
| `.run!(cmd, *args, **opts)` | Same as `run`, raises `CommandError` on non-zero exit |
|
|
127
163
|
| `.run?(cmd, *args, **opts)` | Boolean shortcut — true only when exit code is 0; timeouts return false |
|
|
164
|
+
| `.which(cmd)` | Absolute path of `cmd` on PATH (or `nil`); honors `PATHEXT` on Windows |
|
|
128
165
|
| `CommandError#result` | The failed `Result` object |
|
|
129
166
|
| `.run(cmd) { \|line\| ... }` | Run with line-by-line stdout streaming |
|
|
130
167
|
| `.run(cmd) { \|line, stream\| ... }` | Run with stdout and stderr streaming |
|
|
@@ -136,6 +173,8 @@ end
|
|
|
136
173
|
| `Result#duration` | Execution time in seconds |
|
|
137
174
|
| `Result#signal` | Signal that killed the process (:TERM, :KILL, or nil) |
|
|
138
175
|
| `Result#timed_out?` | Whether the process was killed for exceeding its timeout |
|
|
176
|
+
| `Result#json?` | Whether stdout contains a parseable JSON document |
|
|
177
|
+
| `Result#parse_json` | Parse stdout as JSON; raises `ParseError` on invalid or empty stdout |
|
|
139
178
|
| `Result#to_h` | Hash representation of the result (includes `:success` and `:timed_out`) |
|
|
140
179
|
|
|
141
180
|
## Development
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
3
5
|
module Philiprehberger
|
|
4
6
|
module TaskRunner
|
|
5
7
|
# Represents the result of a shell command execution.
|
|
@@ -55,6 +57,31 @@ module Philiprehberger
|
|
|
55
57
|
%i[TERM KILL].include?(@signal)
|
|
56
58
|
end
|
|
57
59
|
|
|
60
|
+
# Whether stdout contains a parseable JSON document.
|
|
61
|
+
# Empty stdout is treated as non-JSON.
|
|
62
|
+
#
|
|
63
|
+
# @return [Boolean]
|
|
64
|
+
def json?
|
|
65
|
+
return false if @stdout.nil? || @stdout.strip.empty?
|
|
66
|
+
|
|
67
|
+
JSON.parse(@stdout)
|
|
68
|
+
true
|
|
69
|
+
rescue JSON::ParserError
|
|
70
|
+
false
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Parse stdout as JSON.
|
|
74
|
+
#
|
|
75
|
+
# @return [Object] the parsed JSON document (Hash, Array, String, Numeric, true, false, or nil)
|
|
76
|
+
# @raise [ParseError] if stdout is empty or not valid JSON
|
|
77
|
+
def parse_json
|
|
78
|
+
raise ParseError, 'stdout is empty' if @stdout.nil? || @stdout.strip.empty?
|
|
79
|
+
|
|
80
|
+
JSON.parse(@stdout)
|
|
81
|
+
rescue JSON::ParserError => e
|
|
82
|
+
raise ParseError, "stdout is not valid JSON: #{e.message}"
|
|
83
|
+
end
|
|
84
|
+
|
|
58
85
|
# Hash representation of the result.
|
|
59
86
|
#
|
|
60
87
|
# @return [Hash]
|
|
@@ -11,6 +11,9 @@ module Philiprehberger
|
|
|
11
11
|
class Error < StandardError; end
|
|
12
12
|
class TimeoutError < Error; end
|
|
13
13
|
|
|
14
|
+
# Raised by Result#parse_json when the stdout cannot be parsed as JSON.
|
|
15
|
+
class ParseError < Error; end
|
|
16
|
+
|
|
14
17
|
# Raised by run! when the command exits with a non-zero status.
|
|
15
18
|
class CommandError < Error
|
|
16
19
|
# @return [Result] the failed command result
|
|
@@ -81,6 +84,37 @@ module Philiprehberger
|
|
|
81
84
|
false
|
|
82
85
|
end
|
|
83
86
|
|
|
87
|
+
# Find the absolute path of an executable on PATH.
|
|
88
|
+
#
|
|
89
|
+
# Behaves like the `which` shell builtin: walks each entry in
|
|
90
|
+
# `ENV['PATH']` and returns the first directory that contains an
|
|
91
|
+
# executable file matching `cmd`. On Windows, also tries each suffix in
|
|
92
|
+
# `ENV['PATHEXT']`. Returns `nil` when nothing is found.
|
|
93
|
+
#
|
|
94
|
+
# @param cmd [String] the executable name to search for
|
|
95
|
+
# @return [String, nil] the absolute path, or nil when not found
|
|
96
|
+
# @raise [ArgumentError] if `cmd` is nil or empty
|
|
97
|
+
def self.which(cmd)
|
|
98
|
+
raise ArgumentError, 'cmd cannot be nil or empty' if cmd.nil? || cmd.to_s.empty?
|
|
99
|
+
|
|
100
|
+
exts = if Gem.win_platform?
|
|
101
|
+
(ENV['PATHEXT'] || '.COM;.EXE;.BAT;.CMD').split(';')
|
|
102
|
+
else
|
|
103
|
+
['']
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
ENV.fetch('PATH', '').split(File::PATH_SEPARATOR).each do |dir|
|
|
107
|
+
next if dir.empty?
|
|
108
|
+
|
|
109
|
+
exts.each do |ext|
|
|
110
|
+
candidate = File.join(dir, "#{cmd}#{ext}")
|
|
111
|
+
return File.expand_path(candidate) if File.file?(candidate) && File.executable?(candidate)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
nil
|
|
116
|
+
end
|
|
117
|
+
|
|
84
118
|
# @api private
|
|
85
119
|
def self.run_capture(env_hash, full_cmd, spawn_opts, timeout, start_time, signal, kill_after, stdin_data)
|
|
86
120
|
stdout_buf = +''
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: philiprehberger-task_runner
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.7.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Philip Rehberger
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-05-20 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: Run shell commands with captured stdout/stderr, exit code, duration measurement,
|
|
14
14
|
configurable timeout, environment variables, line-by-line streaming, graceful signal
|