rubyshell 1.4.0 → 1.5.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 +113 -2
- data/README.md +329 -83
- data/lib/rubyshell/chain_context.rb +11 -4
- data/lib/rubyshell/chainer.rb +17 -2
- data/lib/rubyshell/command.rb +7 -1
- data/lib/rubyshell/debugger.rb +31 -0
- data/lib/rubyshell/env_proxy.rb +27 -0
- data/lib/rubyshell/executor.rb +10 -4
- data/lib/rubyshell/parallel_executor.rb +60 -0
- data/lib/rubyshell/parser.rb +20 -0
- data/lib/rubyshell/parsers/base.rb +11 -0
- data/lib/rubyshell/parsers/csv.rb +13 -0
- data/lib/rubyshell/parsers/json.rb +13 -0
- data/lib/rubyshell/parsers/yaml.rb +13 -0
- data/lib/rubyshell/results/string_result.rb +8 -0
- data/lib/rubyshell/terminal_executor.rb +10 -2
- data/lib/rubyshell/version.rb +1 -1
- data/lib/rubyshell.rb +60 -3
- metadata +10 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 69051d4ef363ab387d6a7a92f4cdfa12ea51dd71983ebedf11583ca891ce00e4
|
|
4
|
+
data.tar.gz: 9bd6d572911e0c494833a8ab459b69c7648aacdaf18e13be95a03b7ff6270687
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6d17ebb0eae8370dc331dc55cf45bb357d7f83841e2845db69af3da128ca128da3c284468d234230ab0faf4b374202cc374d45bf27adecf99dc79f547481c9b5
|
|
7
|
+
data.tar.gz: bfe7a64951d3fb6ddd5ca5988de9afcd90ef6cc5bf66fba063796c004103028daa20c33c373bb75de1b26d317702ff03771ff37499a5ef3c10100d5912bcbdcf
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,116 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
1
5
|
## [Unreleased]
|
|
2
6
|
|
|
3
|
-
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **Parallel Execution** ([#34](https://github.com/albertalef/rubyshell/pull/34))
|
|
10
|
+
```ruby
|
|
11
|
+
sh do
|
|
12
|
+
results = parallel do
|
|
13
|
+
curl("https://api1.example.com")
|
|
14
|
+
curl("https://api2.example.com")
|
|
15
|
+
chain { ls | wc("-l") }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
results.each { |r| puts r }
|
|
19
|
+
end
|
|
20
|
+
```
|
|
21
|
+
- Returns an Enumerator with results in completion order
|
|
22
|
+
- Supports regular commands, chains, and `sh()` calls
|
|
23
|
+
- Errors are captured and returned as values (not raised)
|
|
24
|
+
|
|
25
|
+
- **Chain Options** ([#34](https://github.com/albertalef/rubyshell/pull/34))
|
|
26
|
+
```ruby
|
|
27
|
+
# Debug mode for chains
|
|
28
|
+
chain(debug: true) { ls | grep("test") }
|
|
29
|
+
|
|
30
|
+
# Parse chain output
|
|
31
|
+
chain(parse: :json) { curl("https://api.example.com") }
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
- **Environment Variables** ([#47](https://github.com/albertalef/rubyshell/pull/47))
|
|
35
|
+
```ruby
|
|
36
|
+
# Command-level
|
|
37
|
+
sh.npm("start", _env: { NODE_ENV: "production" })
|
|
38
|
+
|
|
39
|
+
# Block-level
|
|
40
|
+
sh(env: { DATABASE_URL: "postgres://localhost/db" }) do
|
|
41
|
+
rake("db:migrate")
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Global
|
|
45
|
+
RubyShell.env[:API_KEY] = "secret"
|
|
46
|
+
RubyShell.config(env: { DEBUG: "true" })
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
- **Debug Mode** ([#39](https://github.com/albertalef/rubyshell/pull/39))
|
|
50
|
+
```ruby
|
|
51
|
+
# Global
|
|
52
|
+
RubyShell.debug = true
|
|
53
|
+
|
|
54
|
+
# Block scope
|
|
55
|
+
RubyShell.debug { sh.ls }
|
|
56
|
+
|
|
57
|
+
# Per command
|
|
58
|
+
sh.git("status", _debug: true)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
- **Output Parsers** ([#38](https://github.com/albertalef/rubyshell/pull/38))
|
|
62
|
+
```ruby
|
|
63
|
+
sh.cat("data.json", _parse: :json) # => Hash
|
|
64
|
+
sh.cat("config.yml", _parse: :yaml) # => Hash
|
|
65
|
+
sh.cat("users.csv", _parse: :csv) # => Array
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Changed
|
|
69
|
+
|
|
70
|
+
- **BREAKING:** Renamed `_env` to `env` in `RubyShell.config` and `sh` block options ([#34](https://github.com/albertalef/rubyshell/pull/34))
|
|
71
|
+
```ruby
|
|
72
|
+
# Before
|
|
73
|
+
RubyShell.config(_env: { DEBUG: "true" })
|
|
74
|
+
sh(_env: { NODE_ENV: "production" }) { ... }
|
|
75
|
+
|
|
76
|
+
# After
|
|
77
|
+
RubyShell.config(env: { DEBUG: "true" })
|
|
78
|
+
sh(env: { NODE_ENV: "production" }) { ... }
|
|
79
|
+
```
|
|
80
|
+
Note: Command-level `_env` option remains unchanged.
|
|
81
|
+
|
|
82
|
+
- Refactored specs directory structure
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
## [1.4.0] - 2026-01-23
|
|
86
|
+
|
|
87
|
+
### Added
|
|
88
|
+
|
|
89
|
+
- **Executable generator** ([#29](https://github.com/albertalef/rubyshell/pull/29))
|
|
90
|
+
```bash
|
|
91
|
+
$ rubyshell new myscript
|
|
92
|
+
# Creates executable file with chmod +x
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
- **Stdin parameter support**
|
|
96
|
+
- `_stdin` option to pass string or command output to stdin
|
|
97
|
+
```ruby
|
|
98
|
+
xclip(_stdin: "text")
|
|
99
|
+
wc("-l", _stdin: some_command!)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
- **Array values in hash params**
|
|
103
|
+
```ruby
|
|
104
|
+
sed(e: ["one", "two", "three"])
|
|
105
|
+
# equivalent to: sed -e 'one' -e 'two' -e 'three'
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
- **Direct command execution for special syntax**
|
|
109
|
+
```ruby
|
|
110
|
+
sh("notify-send", "hello")
|
|
111
|
+
sh("wl-copy", text)
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Previous
|
|
4
115
|
|
|
5
|
-
-
|
|
116
|
+
- Not Tracked
|
data/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<img alt="RubyShell" src="./docs/images/rubyshelllogo.png" width="60%">
|
|
3
3
|
</h1>
|
|
4
4
|
|
|
5
|
-
<h3 align="center"
|
|
5
|
+
<h3 align="center">The Rubyist way to write shell scripts</h3>
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
8
|
<a href="https://rubygems.org/gems/rubyshell">
|
|
@@ -23,11 +23,11 @@
|
|
|
23
23
|
·
|
|
24
24
|
<a href="#usage">Usage</a>
|
|
25
25
|
·
|
|
26
|
-
<a href="
|
|
26
|
+
<a href="https://github.com/albertalef/rubyshell/wiki">Wiki</a>
|
|
27
27
|
·
|
|
28
|
-
<a href="#
|
|
28
|
+
<a href="#real-world-examples">Examples</a>
|
|
29
29
|
·
|
|
30
|
-
<a href="#
|
|
30
|
+
<a href="#contributing">Contributing</a>
|
|
31
31
|
</p>
|
|
32
32
|
</p>
|
|
33
33
|
<br />
|
|
@@ -41,162 +41,412 @@
|
|
|
41
41
|
end
|
|
42
42
|
```
|
|
43
43
|
|
|
44
|
-
Yes, that
|
|
44
|
+
Yes, that's valid Ruby!
|
|
45
45
|
`ls` and `cat` are just shell commands, but **RubyShell** makes them behave like Ruby methods.
|
|
46
46
|
|
|
47
47
|
## Installation
|
|
48
48
|
|
|
49
|
-
|
|
49
|
+
```bash
|
|
50
|
+
bundle add rubyshell
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Or install directly:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
gem install rubyshell
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Why RubyShell?
|
|
60
|
+
|
|
61
|
+
### The Problem
|
|
62
|
+
|
|
63
|
+
Ever written something like this?
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
# Bash: Find large files modified in the last 7 days, show top 10 with human sizes
|
|
67
|
+
find . -type f -mtime -7 -exec ls -lh {} \; 2>/dev/null | \
|
|
68
|
+
awk '{print $5, $9}' | \
|
|
69
|
+
sort -hr | \
|
|
70
|
+
head -10
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Or tried to do error handling in bash?
|
|
50
74
|
|
|
51
|
-
|
|
75
|
+
```bash
|
|
76
|
+
# Bash: Hope nothing goes wrong...
|
|
77
|
+
output=$(some_command 2>&1) || echo "failed somehow"
|
|
78
|
+
```
|
|
52
79
|
|
|
53
|
-
|
|
80
|
+
### The Solution
|
|
54
81
|
|
|
55
|
-
|
|
82
|
+
```ruby
|
|
83
|
+
sh do
|
|
84
|
+
# Ruby + Shell: Same task, actually readable
|
|
85
|
+
find(".", type: "f", mtime: "-7")
|
|
86
|
+
.lines
|
|
87
|
+
.map { |f| [File.size(f.strip), f.strip] }
|
|
88
|
+
.sort_by(&:first)
|
|
89
|
+
.last(10)
|
|
90
|
+
.each { |size, file| puts "#{size / 1024}KB #{file}" }
|
|
91
|
+
rescue RubyShell::CommandError => e
|
|
92
|
+
puts "Failed: #{e.message}"
|
|
93
|
+
puts "Exit code: #{e.status}"
|
|
94
|
+
end
|
|
95
|
+
```
|
|
56
96
|
|
|
57
97
|
## Usage
|
|
58
98
|
|
|
59
|
-
###
|
|
60
|
-
With RubyShell, every shell command can be used inside the ruby, you just need to call
|
|
99
|
+
### Basic Commands
|
|
61
100
|
|
|
62
101
|
```ruby
|
|
102
|
+
require 'rubyshell'
|
|
103
|
+
|
|
63
104
|
sh do
|
|
64
|
-
|
|
105
|
+
pwd # Run any command
|
|
106
|
+
ls("-la") # With arguments
|
|
107
|
+
mkdir("project") # Create directories
|
|
108
|
+
docker("ps", all: true) # --all flag
|
|
109
|
+
git("status", s: true) # -s flag
|
|
65
110
|
end
|
|
111
|
+
|
|
112
|
+
# Or chain directly
|
|
113
|
+
sh.git("log", oneline: true, n: 5)
|
|
66
114
|
```
|
|
67
115
|
|
|
68
|
-
###
|
|
69
|
-
If you want to start an irb or pry session, and run commands as first-class citizens then do the following.
|
|
116
|
+
### Pipelines
|
|
70
117
|
|
|
71
118
|
```ruby
|
|
72
|
-
|
|
119
|
+
sh do
|
|
120
|
+
# Using chain block
|
|
121
|
+
chain { cat("access.log") | grep("ERROR") | wc("-l") }
|
|
73
122
|
|
|
74
|
-
|
|
123
|
+
# Using bang pattern
|
|
124
|
+
(cat!("data.csv") | sort! | uniq!).exec
|
|
125
|
+
end
|
|
75
126
|
```
|
|
76
127
|
|
|
77
|
-
###
|
|
78
|
-
Here we have different ways to pass arguments to a command.
|
|
79
|
-
You can separate strings, use only one, use hashes, anyway will work
|
|
128
|
+
### Directory Scoping
|
|
80
129
|
|
|
81
130
|
```ruby
|
|
82
131
|
sh do
|
|
83
|
-
|
|
132
|
+
cd "/var/log" do
|
|
133
|
+
# Commands run here, then return to original dir
|
|
134
|
+
tail("-n", "100", "syslog")
|
|
135
|
+
end
|
|
136
|
+
# Back to original directory
|
|
137
|
+
end
|
|
138
|
+
```
|
|
84
139
|
|
|
85
|
-
|
|
140
|
+
### Error Handling
|
|
86
141
|
|
|
87
|
-
|
|
142
|
+
```ruby
|
|
143
|
+
sh do
|
|
144
|
+
begin
|
|
145
|
+
rm("-rf", "important_folder")
|
|
146
|
+
rescue RubyShell::CommandError => e
|
|
147
|
+
puts "Command: #{e.command}"
|
|
148
|
+
puts "Stderr: #{e.stderr}"
|
|
149
|
+
puts "Exit code: #{e.status}"
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Parallel Execution
|
|
88
155
|
|
|
89
|
-
|
|
156
|
+
Run multiple commands concurrently and get results as they complete:
|
|
157
|
+
|
|
158
|
+
```ruby
|
|
159
|
+
sh do
|
|
160
|
+
results = parallel do
|
|
161
|
+
curl("https://api1.example.com")
|
|
162
|
+
curl("https://api2.example.com")
|
|
163
|
+
chain { ls | wc("-l") }
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
results.each { |r| puts r }
|
|
90
167
|
end
|
|
91
168
|
```
|
|
92
169
|
|
|
93
|
-
|
|
94
|
-
|
|
170
|
+
Returns an Enumerator with results in completion order. Errors are captured and returned as values (not raised).
|
|
171
|
+
|
|
172
|
+
### Environment Variables
|
|
173
|
+
|
|
174
|
+
```ruby
|
|
175
|
+
# Command-level
|
|
176
|
+
sh.npm("start", _env: { NODE_ENV: "production" })
|
|
177
|
+
|
|
178
|
+
# Block-level
|
|
179
|
+
sh(env: { DATABASE_URL: "postgres://localhost/db" }) do
|
|
180
|
+
rake("db:migrate")
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Global
|
|
184
|
+
RubyShell.env[:API_KEY] = "secret"
|
|
185
|
+
RubyShell.config(env: { DEBUG: "true" })
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### Debug Mode
|
|
189
|
+
|
|
190
|
+
```ruby
|
|
191
|
+
# Global
|
|
192
|
+
RubyShell.debug = true
|
|
193
|
+
|
|
194
|
+
# Block scope
|
|
195
|
+
RubyShell.debug { sh.ls }
|
|
196
|
+
|
|
197
|
+
# Per command
|
|
198
|
+
sh.git("status", _debug: true)
|
|
199
|
+
# Output:
|
|
200
|
+
# Executed: git status
|
|
201
|
+
# Duration: 0.003521s
|
|
202
|
+
# Pid: 12345
|
|
203
|
+
# Exit code: 0
|
|
204
|
+
# Stdout: "On branch main..."
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### Output Parsers
|
|
208
|
+
|
|
209
|
+
Parse command output directly into Ruby objects:
|
|
210
|
+
|
|
211
|
+
```ruby
|
|
212
|
+
sh.cat("data.json", _parse: :json) # => Hash
|
|
213
|
+
sh.cat("config.yml", _parse: :yaml) # => Hash
|
|
214
|
+
sh.cat("users.csv", _parse: :csv) # => Array
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### Chain Options
|
|
218
|
+
|
|
219
|
+
```ruby
|
|
220
|
+
# Debug mode for chains
|
|
221
|
+
chain(debug: true) { ls | grep("test") }
|
|
222
|
+
|
|
223
|
+
# Parse chain output
|
|
224
|
+
chain(parse: :json) { curl("https://api.example.com") }
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
## Real-World Examples
|
|
228
|
+
|
|
229
|
+
### Git Workflow Automation
|
|
95
230
|
|
|
96
|
-
##### Chaging code folder
|
|
97
231
|
```ruby
|
|
98
232
|
sh do
|
|
99
|
-
|
|
233
|
+
# Stash changes, pull, pop, and show what changed
|
|
234
|
+
changes = git("status", porcelain: true).lines
|
|
235
|
+
|
|
236
|
+
if changes.any?
|
|
237
|
+
puts "Stashing #{changes.count} changed files..."
|
|
238
|
+
git("stash")
|
|
239
|
+
git("pull", rebase: true)
|
|
240
|
+
git("stash", "pop")
|
|
241
|
+
else
|
|
242
|
+
git("pull", rebase: true)
|
|
243
|
+
end
|
|
100
244
|
|
|
101
|
-
|
|
245
|
+
# Show recent commits by author
|
|
246
|
+
git("log", oneline: true, n: 100)
|
|
247
|
+
.lines
|
|
248
|
+
.map { |line| `git show -s --format='%an' #{line.split.first}`.strip }
|
|
249
|
+
.tally
|
|
250
|
+
.sort_by { |_, count| -count }
|
|
251
|
+
.first(5)
|
|
252
|
+
.each { |author, count| puts "#{author}: #{count} commits" }
|
|
253
|
+
end
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
### Log Analysis
|
|
102
257
|
|
|
103
|
-
|
|
258
|
+
```ruby
|
|
259
|
+
sh do
|
|
260
|
+
cd "/var/log" do
|
|
261
|
+
# Parse nginx logs: top 10 IPs by request count
|
|
262
|
+
cat("nginx/access.log")
|
|
263
|
+
.lines
|
|
264
|
+
.map { |line| line.split.first } # Extract IP
|
|
265
|
+
.tally
|
|
266
|
+
.sort_by { |_, count| -count }
|
|
267
|
+
.first(10)
|
|
268
|
+
.each { |ip, count| puts "#{ip.ljust(15)} #{count} requests" }
|
|
269
|
+
end
|
|
104
270
|
end
|
|
105
271
|
```
|
|
106
272
|
|
|
107
|
-
|
|
273
|
+
### Docker Cleanup
|
|
108
274
|
|
|
109
275
|
```ruby
|
|
110
276
|
sh do
|
|
111
|
-
|
|
112
|
-
|
|
277
|
+
# Remove containers that exited more than a day ago
|
|
278
|
+
containers = docker("ps", a: true, format: "{{.ID}} {{.Status}}")
|
|
279
|
+
.lines
|
|
280
|
+
.select { |line| line.include?("Exited") }
|
|
281
|
+
.map { |line| line.split.first }
|
|
282
|
+
|
|
283
|
+
if containers.any?
|
|
284
|
+
puts "Removing #{containers.count} dead containers..."
|
|
285
|
+
docker("rm", *containers)
|
|
113
286
|
end
|
|
114
287
|
|
|
115
|
-
|
|
288
|
+
# Remove dangling images
|
|
289
|
+
images = docker("images", f: "dangling=true", q: true).lines.map(&:strip)
|
|
290
|
+
|
|
291
|
+
if images.any?
|
|
292
|
+
puts "Removing #{images.count} dangling images..."
|
|
293
|
+
docker("rmi", *images)
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
puts "Disk usage:"
|
|
297
|
+
puts docker("system", "df")
|
|
116
298
|
end
|
|
117
299
|
```
|
|
118
300
|
|
|
119
|
-
###
|
|
120
|
-
The `chain` method make possible we use shell operators inside the ruby, like `& && | > >> < <<`
|
|
301
|
+
### Batch File Processing
|
|
121
302
|
|
|
122
303
|
```ruby
|
|
123
304
|
sh do
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
305
|
+
# Convert all PNGs to WebP, preserving directory structure
|
|
306
|
+
find(".", name: "*.png")
|
|
307
|
+
.lines
|
|
308
|
+
.map(&:strip)
|
|
309
|
+
.each do |png|
|
|
310
|
+
webp = png.sub(/\.png$/, ".webp")
|
|
311
|
+
puts "Converting: #{png}"
|
|
312
|
+
|
|
313
|
+
begin
|
|
314
|
+
cwebp("-q", "80", png, o: webp)
|
|
315
|
+
rm(png)
|
|
316
|
+
rescue RubyShell::CommandError => e
|
|
317
|
+
puts " Failed: #{e.message}"
|
|
318
|
+
end
|
|
319
|
+
end
|
|
127
320
|
end
|
|
321
|
+
```
|
|
128
322
|
|
|
323
|
+
### System Health Check
|
|
324
|
+
|
|
325
|
+
```ruby
|
|
129
326
|
sh do
|
|
130
|
-
|
|
327
|
+
puts "=== System Health ==="
|
|
328
|
+
|
|
329
|
+
# Disk usage warnings
|
|
330
|
+
df("-h")
|
|
331
|
+
.lines
|
|
332
|
+
.drop(1)
|
|
333
|
+
.each do |line|
|
|
334
|
+
parts = line.split
|
|
335
|
+
usage = parts[4].to_i
|
|
336
|
+
mount = parts[5]
|
|
337
|
+
puts "WARNING: #{mount} at #{usage}%" if usage > 80
|
|
338
|
+
end
|
|
131
339
|
|
|
132
|
-
|
|
340
|
+
# Memory info
|
|
341
|
+
mem = cat("/proc/meminfo")
|
|
342
|
+
.lines
|
|
343
|
+
.first(3)
|
|
344
|
+
.to_h { |l| k, v = l.split(":"); [k, v.strip] }
|
|
345
|
+
|
|
346
|
+
puts "\nMemory: #{mem['MemAvailable']} available of #{mem['MemTotal']}"
|
|
347
|
+
|
|
348
|
+
# Top 5 CPU consumers
|
|
349
|
+
puts "\nTop CPU processes:"
|
|
350
|
+
ps("aux", sort: "-%cpu")
|
|
351
|
+
.lines
|
|
352
|
+
.drop(1)
|
|
353
|
+
.first(5)
|
|
354
|
+
.each { |proc| puts " #{proc.split[10]}% - #{proc.split[10..-1].join(' ').slice(0, 40)}" }
|
|
133
355
|
end
|
|
134
356
|
```
|
|
135
357
|
|
|
136
|
-
###
|
|
137
|
-
The `sh` method can receive any method call, and execute shell commands
|
|
358
|
+
### Interactive Script with Confirmation
|
|
138
359
|
|
|
139
360
|
```ruby
|
|
140
|
-
sh
|
|
361
|
+
sh do
|
|
362
|
+
files = find(".", name: "*.tmp", mtime: "+30").lines.map(&:strip)
|
|
363
|
+
|
|
364
|
+
if files.empty?
|
|
365
|
+
puts "No old temp files found."
|
|
366
|
+
exit
|
|
367
|
+
end
|
|
141
368
|
|
|
142
|
-
|
|
369
|
+
puts "Found #{files.count} temp files older than 30 days:"
|
|
370
|
+
files.first(10).each { |f| puts " #{f}" }
|
|
371
|
+
puts " ... and #{files.count - 10} more" if files.count > 10
|
|
143
372
|
|
|
144
|
-
|
|
373
|
+
total_size = files.sum { |f| File.size(f) rescue 0 }
|
|
374
|
+
puts "\nTotal size: #{total_size / 1024 / 1024}MB"
|
|
375
|
+
|
|
376
|
+
print "\nDelete all? [y/N] "
|
|
377
|
+
if gets.strip.downcase == 'y'
|
|
378
|
+
files.each { |f| rm(f) }
|
|
379
|
+
puts "Deleted #{files.count} files."
|
|
380
|
+
end
|
|
381
|
+
end
|
|
145
382
|
```
|
|
146
383
|
|
|
147
|
-
|
|
384
|
+
### Deploy Script
|
|
148
385
|
|
|
149
386
|
```ruby
|
|
150
387
|
#!/usr/bin/env ruby
|
|
388
|
+
require 'rubyshell'
|
|
151
389
|
|
|
152
|
-
|
|
153
|
-
|
|
390
|
+
APP_NAME = "myapp"
|
|
391
|
+
DEPLOY_PATH = "/var/www/#{APP_NAME}"
|
|
154
392
|
|
|
155
393
|
sh do
|
|
156
|
-
|
|
394
|
+
puts "Deploying #{APP_NAME}..."
|
|
157
395
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
end
|
|
163
|
-
end
|
|
396
|
+
# Ensure clean state
|
|
397
|
+
git("status", porcelain: true).lines.tap do |changes|
|
|
398
|
+
abort "Uncommitted changes!" if changes.any?
|
|
399
|
+
end
|
|
164
400
|
|
|
165
|
-
|
|
401
|
+
# Run tests
|
|
402
|
+
puts "Running tests..."
|
|
403
|
+
rake("spec")
|
|
166
404
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
405
|
+
# Build and deploy
|
|
406
|
+
cd DEPLOY_PATH do
|
|
407
|
+
git("pull", "origin", "main")
|
|
408
|
+
bundle("install", deployment: true)
|
|
409
|
+
rake("db:migrate")
|
|
410
|
+
|
|
411
|
+
# Restart with zero downtime
|
|
412
|
+
puts "Restarting..."
|
|
413
|
+
systemctl("reload", APP_NAME)
|
|
170
414
|
end
|
|
171
|
-
ensure
|
|
172
|
-
rm "-rf files"
|
|
173
|
-
end
|
|
174
415
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
#
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
# qkRKcZHqu2Moq1se
|
|
182
|
-
# nUPluln9GM1ydtoz
|
|
183
|
-
# rkdYsc1RBhkeN1dq
|
|
184
|
-
# ZPXZMqzYfyFfjPHF
|
|
416
|
+
puts "Deployed successfully!"
|
|
417
|
+
|
|
418
|
+
rescue RubyShell::CommandError => e
|
|
419
|
+
puts "Deploy failed: #{e.message}"
|
|
420
|
+
exit 1
|
|
421
|
+
end
|
|
185
422
|
```
|
|
186
423
|
|
|
187
|
-
##
|
|
424
|
+
## Comparison
|
|
188
425
|
|
|
189
|
-
|
|
426
|
+
| Task | Bash | RubyShell |
|
|
427
|
+
|------|------|-----------|
|
|
428
|
+
| Error handling | `cmd \|\| echo "fail"` | `rescue CommandError` |
|
|
429
|
+
| String manipulation | `echo $var \| sed \| awk` | `result.gsub(/.../)` |
|
|
430
|
+
| Data structures | Arrays only | Hashes, objects, classes |
|
|
431
|
+
| Iteration | `for f in *; do` | `.each`, `.map`, `.select` |
|
|
432
|
+
| Testing | DIY | RSpec, Minitest |
|
|
190
433
|
|
|
191
|
-
##
|
|
434
|
+
## Documentation
|
|
192
435
|
|
|
193
|
-
|
|
436
|
+
See [Wiki](https://github.com/albertalef/rubyshell/wiki) for complete documentation including all options and advanced features.
|
|
194
437
|
|
|
195
|
-
|
|
438
|
+
## Development
|
|
439
|
+
|
|
440
|
+
```bash
|
|
441
|
+
bin/setup # Install dependencies
|
|
442
|
+
rake spec # Run tests
|
|
443
|
+
rake rubocop # Lint code
|
|
444
|
+
bin/console # Interactive console
|
|
445
|
+
```
|
|
196
446
|
|
|
197
447
|
## Contributing
|
|
198
448
|
|
|
199
|
-
Bug reports and pull requests are welcome on GitHub
|
|
449
|
+
Bug reports and pull requests are welcome on [GitHub](https://github.com/albertalef/rubyshell).
|
|
200
450
|
|
|
201
451
|
## Sponsors
|
|
202
452
|
|
|
@@ -206,8 +456,4 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/albert
|
|
|
206
456
|
|
|
207
457
|
## License
|
|
208
458
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
## Code of Conduct
|
|
212
|
-
|
|
213
|
-
Everyone interacting in the Rubysh project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/albertalef/rubyshell/blob/master/CODE_OF_CONDUCT.md).
|
|
459
|
+
MIT License - see [LICENSE](https://opensource.org/licenses/MIT).
|
|
@@ -2,15 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
module RubyShell
|
|
4
4
|
class ChainContext
|
|
5
|
-
def
|
|
5
|
+
def initialize(options = {})
|
|
6
|
+
@options = options
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def sh(command, *args)
|
|
6
10
|
method_missing(command, *args)
|
|
7
11
|
end
|
|
8
12
|
|
|
9
|
-
def
|
|
10
|
-
RubyShell::Chainer.new(
|
|
13
|
+
def method_missing(method_name, *args, &block)
|
|
14
|
+
RubyShell::Chainer.new(
|
|
15
|
+
RubyShell::Command.new(method_name, *args, &block),
|
|
16
|
+
@options
|
|
17
|
+
)
|
|
11
18
|
end
|
|
12
19
|
|
|
13
|
-
def
|
|
20
|
+
def respond_to_missing?(_name, _include_private)
|
|
14
21
|
false
|
|
15
22
|
end
|
|
16
23
|
end
|
data/lib/rubyshell/chainer.rb
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "debug"
|
|
3
4
|
module RubyShell
|
|
4
5
|
class Chainer
|
|
5
|
-
attr_reader :parts
|
|
6
|
+
attr_reader :parts, :options
|
|
6
7
|
|
|
7
8
|
def initialize(command, options = {})
|
|
8
9
|
@parts = [command]
|
|
@@ -19,6 +20,14 @@ module RubyShell
|
|
|
19
20
|
self
|
|
20
21
|
end
|
|
21
22
|
|
|
23
|
+
def shared_settings
|
|
24
|
+
%i[env debug]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def settings
|
|
28
|
+
@options.select { shared_settings.include?(_1.to_sym) }.transform_keys { :"_#{_1}" }
|
|
29
|
+
end
|
|
30
|
+
|
|
22
31
|
def method_missing(method_name, *args, &block)
|
|
23
32
|
if method_name.start_with?(/[^A-Za-z0-9]/)
|
|
24
33
|
handle_chain(method_name, args.first)
|
|
@@ -32,7 +41,13 @@ module RubyShell
|
|
|
32
41
|
end
|
|
33
42
|
|
|
34
43
|
def exec_commands
|
|
35
|
-
RubyShell::
|
|
44
|
+
result = RubyShell::Debugger.run_wrapper(self, debug: @options[:debug]) do
|
|
45
|
+
RubyShell::TerminalExecutor.capture(to_shell, settings)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
result = RubyShell::Parser.parse(@options[:parse], result) if @options[:parse]
|
|
49
|
+
|
|
50
|
+
result
|
|
36
51
|
end
|
|
37
52
|
|
|
38
53
|
alias exec exec_commands
|
data/lib/rubyshell/command.rb
CHANGED
|
@@ -16,7 +16,13 @@ module RubyShell
|
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
def exec_command
|
|
19
|
-
RubyShell::
|
|
19
|
+
result = RubyShell::Debugger.run_wrapper(self, debug: @options[:_debug]) do
|
|
20
|
+
RubyShell::TerminalExecutor.capture(to_shell, @options)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
result = RubyShell::Parser.parse(@options[:_parse], result) if @options[:_parse]
|
|
24
|
+
|
|
25
|
+
result
|
|
20
26
|
end
|
|
21
27
|
|
|
22
28
|
alias exec exec_command
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyShell
|
|
4
|
+
module Debugger
|
|
5
|
+
class << self
|
|
6
|
+
def run_wrapper(command, debug: nil)
|
|
7
|
+
if debug || RubyShell.debug?
|
|
8
|
+
|
|
9
|
+
time_one = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
10
|
+
|
|
11
|
+
result = yield
|
|
12
|
+
|
|
13
|
+
time_two = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
14
|
+
|
|
15
|
+
RubyShell.log(<<~TEXT
|
|
16
|
+
\nExecuted: #{command.to_shell.chomp}
|
|
17
|
+
Duration: #{format("%.6f", time_two - time_one)}s
|
|
18
|
+
Pid: #{result.metadata[:exit_status].pid}
|
|
19
|
+
Exit code: #{result.metadata[:exit_status].to_i}
|
|
20
|
+
Stdout: #{result.to_s.inspect}
|
|
21
|
+
TEXT
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
result
|
|
25
|
+
else
|
|
26
|
+
yield
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyShell
|
|
4
|
+
module EnvProxy
|
|
5
|
+
class << self
|
|
6
|
+
def env
|
|
7
|
+
@env ||= {}
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def to_h
|
|
11
|
+
env
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def []=(key, value)
|
|
15
|
+
env[key.to_s] = value
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def [](key)
|
|
19
|
+
env[key.to_s]
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def set(hash)
|
|
23
|
+
@env = hash&.transform_keys(&:to_s) || {}
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
data/lib/rubyshell/executor.rb
CHANGED
|
@@ -6,12 +6,18 @@ module RubyShell
|
|
|
6
6
|
|
|
7
7
|
include RubyShell::OverwritedCommands
|
|
8
8
|
|
|
9
|
-
def chain(&block)
|
|
10
|
-
RubyShell::ChainContext.
|
|
9
|
+
def chain(options = {}, &block)
|
|
10
|
+
RubyShell::ChainContext.new(options).instance_exec(&block).exec_commands
|
|
11
11
|
end
|
|
12
12
|
|
|
13
|
-
def
|
|
14
|
-
|
|
13
|
+
def parallel(options = {}, &block)
|
|
14
|
+
RubyShell.debug(options[:debug]) do
|
|
15
|
+
RubyShell::ParallelExecutor.new(options)._evaluate(&block)._run
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def method_missing(method_name, *args, **kwargs)
|
|
20
|
+
command = RubyShell::Command.new(method_name.to_s.gsub(/!$/, ""), *args, **kwargs)
|
|
15
21
|
|
|
16
22
|
if method_name.to_s.match?(/!$/)
|
|
17
23
|
command
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyShell
|
|
4
|
+
class ParallelExecutor
|
|
5
|
+
def initialize(options = {})
|
|
6
|
+
@commands = []
|
|
7
|
+
@options = options
|
|
8
|
+
|
|
9
|
+
@queue = Queue.new
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def shared_settings
|
|
13
|
+
%i[env debug]
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def settings
|
|
17
|
+
@options.select { shared_settings.include?(_1.to_sym) }.transform_keys { :"_#{_1}" }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def _evaluate(&block)
|
|
21
|
+
instance_exec(&block)
|
|
22
|
+
|
|
23
|
+
self
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def _run
|
|
27
|
+
@commands.each do |command|
|
|
28
|
+
Thread.new do
|
|
29
|
+
@queue << begin
|
|
30
|
+
command.exec
|
|
31
|
+
rescue StandardError => e
|
|
32
|
+
e
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
Enumerator.new do |y|
|
|
38
|
+
@commands.size.times { y << @queue.pop }
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def chain(options = {}, &block)
|
|
43
|
+
@commands << RubyShell::ChainContext.new(settings.merge(options)).instance_exec(&block)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def sh(command, *args)
|
|
47
|
+
method_missing(command, *args)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def method_missing(method_name, *args, **kwargs)
|
|
51
|
+
command = RubyShell::Command.new(method_name.to_s.gsub(/!$/, ""), *args, **settings, **kwargs)
|
|
52
|
+
|
|
53
|
+
@commands << command
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def respond_to_missing?(_name, _include_private)
|
|
57
|
+
false
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyShell
|
|
4
|
+
class Parser
|
|
5
|
+
class ParserNotFound < StandardError
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def self.parse(parser_key, value)
|
|
9
|
+
parser_class = locate_parser(parser_key)
|
|
10
|
+
|
|
11
|
+
parser_class.parse(value)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.locate_parser(parser_key)
|
|
15
|
+
Object.const_get("RubyShell::Parsers::#{parser_key.to_s.split("_").map(&:capitalize).join}")
|
|
16
|
+
rescue NameError
|
|
17
|
+
raise ParserNotFound
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -13,7 +13,9 @@ module RubyShell
|
|
|
13
13
|
options[:_stdin]
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
env_hash = RubyShell.env.to_h.merge(options[:_env]&.transform_keys(&:to_s) || {})
|
|
17
|
+
|
|
18
|
+
Open3.popen3(env_hash, command) do |stdin, stdout, stderr, w_thread|
|
|
17
19
|
stdin.write(stdin_value) if stdin_value
|
|
18
20
|
|
|
19
21
|
stdin.close
|
|
@@ -64,7 +66,13 @@ module RubyShell
|
|
|
64
66
|
)
|
|
65
67
|
end
|
|
66
68
|
|
|
67
|
-
RubyShell::Results::StringResult.new(
|
|
69
|
+
RubyShell::Results::StringResult.new(
|
|
70
|
+
output.chomp,
|
|
71
|
+
metadata: {
|
|
72
|
+
command: command,
|
|
73
|
+
exit_status: status
|
|
74
|
+
}
|
|
75
|
+
)
|
|
68
76
|
end
|
|
69
77
|
rescue StandardError => e
|
|
70
78
|
raise e if e.is_a?(RubyShell::CommandError)
|
data/lib/rubyshell/version.rb
CHANGED
data/lib/rubyshell.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "logger"
|
|
4
|
+
|
|
3
5
|
require_relative "rubyshell/version"
|
|
4
6
|
require_relative "rubyshell/command"
|
|
5
7
|
require_relative "rubyshell/chainer"
|
|
@@ -10,15 +12,70 @@ require_relative "rubyshell/error"
|
|
|
10
12
|
require_relative "rubyshell/results/string_result"
|
|
11
13
|
require_relative "rubyshell/terminal_executor"
|
|
12
14
|
require_relative "rubyshell/sanitizer"
|
|
15
|
+
require_relative "rubyshell/parser"
|
|
16
|
+
require_relative "rubyshell/parsers/base"
|
|
17
|
+
require_relative "rubyshell/debugger"
|
|
18
|
+
require_relative "rubyshell/env_proxy"
|
|
19
|
+
require_relative "rubyshell/parallel_executor"
|
|
20
|
+
|
|
21
|
+
module RubyShell
|
|
22
|
+
class << self
|
|
23
|
+
def debug=(value)
|
|
24
|
+
@debug_mode = !!value
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def debug(value = true) # rubocop:disable Style/OptionalBooleanParameter
|
|
28
|
+
previous_value = @debug_mode
|
|
29
|
+
|
|
30
|
+
@debug_mode = value
|
|
31
|
+
|
|
32
|
+
result = yield
|
|
33
|
+
|
|
34
|
+
@debug_mode = previous_value
|
|
35
|
+
|
|
36
|
+
result
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def debug?
|
|
40
|
+
@debug_mode == true
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
attr_writer :logger
|
|
44
|
+
|
|
45
|
+
def logger
|
|
46
|
+
@logger ||= Logger.new($stdout)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def log_level=(level)
|
|
50
|
+
@log_level = level.to_s
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def log(text)
|
|
54
|
+
logger.send(@log_level || :info, text)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def env
|
|
58
|
+
RubyShell::EnvProxy
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def config(kwargs)
|
|
62
|
+
env.set(kwargs[:env]) if kwargs[:env]
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
13
66
|
|
|
14
67
|
module Kernel
|
|
15
|
-
def sh(command = nil, *args, &block)
|
|
68
|
+
def sh(command = nil, *args, **kwargs, &block)
|
|
16
69
|
if command
|
|
17
|
-
RubyShell::Executor.send(command, *args)
|
|
70
|
+
RubyShell::Executor.send(command, *args, **kwargs)
|
|
18
71
|
elsif block.nil?
|
|
19
72
|
RubyShell::Executor
|
|
20
73
|
else
|
|
21
|
-
RubyShell
|
|
74
|
+
RubyShell.config(kwargs)
|
|
75
|
+
|
|
76
|
+
RubyShell.debug(kwargs[:debug]) do
|
|
77
|
+
RubyShell::Executor.class_eval(&block)
|
|
78
|
+
end
|
|
22
79
|
end
|
|
23
80
|
end
|
|
24
81
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rubyshell
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- albertalef
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-02-06 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: A rubist way to run shell commands
|
|
14
14
|
email:
|
|
@@ -28,9 +28,17 @@ files:
|
|
|
28
28
|
- lib/rubyshell/chain_context.rb
|
|
29
29
|
- lib/rubyshell/chainer.rb
|
|
30
30
|
- lib/rubyshell/command.rb
|
|
31
|
+
- lib/rubyshell/debugger.rb
|
|
32
|
+
- lib/rubyshell/env_proxy.rb
|
|
31
33
|
- lib/rubyshell/error.rb
|
|
32
34
|
- lib/rubyshell/executor.rb
|
|
33
35
|
- lib/rubyshell/overwrited_commands.rb
|
|
36
|
+
- lib/rubyshell/parallel_executor.rb
|
|
37
|
+
- lib/rubyshell/parser.rb
|
|
38
|
+
- lib/rubyshell/parsers/base.rb
|
|
39
|
+
- lib/rubyshell/parsers/csv.rb
|
|
40
|
+
- lib/rubyshell/parsers/json.rb
|
|
41
|
+
- lib/rubyshell/parsers/yaml.rb
|
|
34
42
|
- lib/rubyshell/results/string_result.rb
|
|
35
43
|
- lib/rubyshell/sanitizer.rb
|
|
36
44
|
- lib/rubyshell/terminal_executor.rb
|