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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1e264781c1fb8c863a53e85b4d6b80f4bc1644a27bbeb17446473719d5efc10c
4
- data.tar.gz: d7b4719dab1483d43ec494ac3aa00718ecfd6d408b9241c21cbbd7695e4bce2d
3
+ metadata.gz: dddebb2a9f61387b45cc9650f3d695c07be69388f6d2b988f551326b5cfba973
4
+ data.tar.gz: 2d53404bcb1eb3fbbb941091d8c7b4a4e68515fe224c6891b0473c9a609d0773
5
5
  SHA512:
6
- metadata.gz: 8ea18413fb9724653c6621d57f24af1384567aa5361ea3ae4d9ba165f97d5685644d8b45c2ad2cc9e2537db2e9e19f223b4983c94bd2514640c325c8422a46dd
7
- data.tar.gz: bd148577cae30e1bb877622f60cdfda40887bee65653a5568c6b6faa420f37cf38bfe95d6d8317508a7a6502c3c67395a242218f6731d16227d338266131c862
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
  [![Gem Version](https://badge.fury.io/rb/philiprehberger-task_runner.svg)](https://rubygems.org/gems/philiprehberger-task_runner)
5
5
  [![Last updated](https://img.shields.io/github/last-commit/philiprehberger/rb-task-runner)](https://github.com/philiprehberger/rb-task-runner/commits/main)
6
6
 
7
+ ![philiprehberger-task_runner](https://raw.githubusercontent.com/philiprehberger/rb-task-runner/main/package-card.webp)
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]
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Philiprehberger
4
4
  module TaskRunner
5
- VERSION = '0.5.0'
5
+ VERSION = '0.7.0'
6
6
  end
7
7
  end
@@ -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.5.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-04-21 00:00:00.000000000 Z
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