srsh 0.6.1.pre.HOTFIX → 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.
Files changed (4) hide show
  1. checksums.yaml +4 -4
  2. data/bin/srsh +424 -244
  3. data/lib/srsh/version.rb +1 -1
  4. metadata +1 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bfa425d4d7aed693a0000a3e8c1720289ad8347e08a1a0cb7e22c3f4aa9a206a
4
- data.tar.gz: af8b9efb9c83c9583791eb37933bcc93a4f594d82b8d79e8e8f24de110b61c01
3
+ metadata.gz: 405e071c7030f9eb0a2af1212dfd1357b047ec0c6aa37a7df490300b28d4a033
4
+ data.tar.gz: dd50fbc336a12486d5479e547a964e7129d4d0a1f51fe680ec394e9cf6d02792
5
5
  SHA512:
6
- metadata.gz: 7bc729a3439ffcfc7e90c13f50e8c0a9535b5b8bac9ae88227b8111ae002584cbcb0e91ef7b36736b0f68afa84d0e863871811f63989cd3101d5a94161a987fc
7
- data.tar.gz: 211b5716b2734470f319007a75c213c80d6c7601081f7c7e5e076529b56f53d566fe3589230e60b1ed0ebeb335d206556c017796f9574d8c7aa1b8c42e69f26d
6
+ metadata.gz: fc2058fd9a0d13b6e91d2a4200acfab1c414040212c1d51c8755be0bb3d81279f7e86cfeeab14e13422f6454f463109fcdd1d1e0989191c68078b1ae10107930
7
+ data.tar.gz: 6057d29d402120f7dae36741aaacbbfa4cda2c09d5181c944fdc5bdaf03357c5c4cfecf6a1ad6fe4aca22b0044de4500cb3c0feceb4a3f0f2c476250bb731e98
data/bin/srsh CHANGED
@@ -7,7 +7,7 @@ require 'rbconfig'
7
7
  require 'io/console'
8
8
 
9
9
  # ---------------- Version ----------------
10
- SRSH_VERSION = "0.6.1"
10
+ SRSH_VERSION = "0.7.0"
11
11
 
12
12
  $0 = "srsh-#{SRSH_VERSION}"
13
13
  ENV['SHELL'] = "srsh-#{SRSH_VERSION}"
@@ -19,12 +19,17 @@ $child_pids = []
19
19
  $aliases = {}
20
20
  $last_render_rows = 0
21
21
 
22
+ $last_status = 0
23
+ $rsh_functions = {}
24
+ $rsh_positional = {}
25
+ $rsh_script_mode = false
26
+
22
27
  Signal.trap("INT", "IGNORE")
23
28
 
24
29
  # ---------------- History ----------------
25
30
  HISTORY_FILE = File.join(Dir.home, ".srsh_history")
26
31
  HISTORY = if File.exist?(HISTORY_FILE)
27
- File.readlines(HISTORY_FILE, chomp: true)
32
+ File.readlines(HISTORY_FILE, chomp: true)
28
33
  else
29
34
  []
30
35
  end
@@ -42,12 +47,12 @@ end
42
47
  RC_FILE = File.join(Dir.home, ".srshrc")
43
48
  begin
44
49
  unless File.exist?(RC_FILE)
45
- File.write(RC_FILE, <<~RC)
46
- # ~/.srshrc — srsh configuration
47
- # This file was created automatically by srsh v#{SRSH_VERSION}.
48
- # You can keep personal notes or planned settings here.
49
- # (Currently not sourced by srsh runtime.)
50
- RC
50
+ File.write(RC_FILE, <<~RC)
51
+ # ~/.srshrc — srsh configuration
52
+ # This file was created automatically by srsh v#{SRSH_VERSION}.
53
+ # You can keep personal notes or planned settings here.
54
+ # (Currently not sourced by srsh runtime.)
55
+ RC
51
56
  end
52
57
  rescue
53
58
  end
@@ -65,8 +70,15 @@ def rainbow_codes
65
70
  [31, 33, 32, 36, 34, 35, 91, 93, 92, 96, 94, 95]
66
71
  end
67
72
 
73
+ # variable expansion: $VAR and $1, $2, $0 (script/function args)
68
74
  def expand_vars(str)
69
- str.gsub(/\$([a-zA-Z_][a-zA-Z0-9_]*)/) { ENV[$1] || "" }
75
+ s = str.gsub(/\$([a-zA-Z_][a-zA-Z0-9_]*)/) do
76
+ ENV[$1] || ""
77
+ end
78
+ s.gsub(/\$(\d+)/) do
79
+ idx = $1.to_i
80
+ ($rsh_positional && $rsh_positional[idx]) || ""
81
+ end
70
82
  end
71
83
 
72
84
  def parse_redirection(cmd)
@@ -118,241 +130,241 @@ end
118
130
 
119
131
  def strip_ansi(str)
120
132
  str.to_s.gsub(/\e\[[0-9;]*m/, '')
121
- end
122
-
123
- # ---------------- Aliases ----------------
124
- def expand_aliases(cmd, seen = [])
125
- return cmd if cmd.nil? || cmd.strip.empty?
126
- first_word, rest = cmd.strip.split(' ', 2)
127
- return cmd if seen.include?(first_word)
128
- seen << first_word
129
-
130
- if $aliases.key?(first_word)
131
- replacement = $aliases[first_word]
132
- expanded = expand_aliases(replacement, seen)
133
- rest ? "#{expanded} #{rest}" : expanded
134
- else
135
- cmd
136
- end
137
- end
138
-
139
- # ---------------- System Info ----------------
140
- def current_time
141
- Time.now.strftime("%Y-%m-%d %H:%M:%S %Z")
142
- end
143
-
144
- def detect_distro
145
- if File.exist?('/etc/os-release')
146
- line = File.read('/etc/os-release').lines.find { |l|
147
- l.start_with?('PRETTY_NAME="') || l.start_with?('PRETTY_NAME=')
148
- }
149
- return line.split('=').last.strip.delete('"') if line
150
- end
151
- "#{RbConfig::CONFIG['host_os']}"
152
- end
153
-
154
- def os_type
155
- host = RbConfig::CONFIG['host_os'].to_s
156
- case host
157
- when /linux/i
158
- :linux
159
- when /darwin/i
160
- :mac
161
- when /bsd/i
162
- :bsd
163
- else
164
- :other
165
- end
166
- end
167
-
168
- # ---------------- Quotes ----------------
169
- QUOTES = [
170
- "Keep calm and code on.",
171
- "Did you try turning it off and on again?",
172
- "There’s no place like 127.0.0.1.",
173
- "To iterate is human, to recurse divine.",
174
- "sudo rm -rf / – Just kidding, don’t do that!",
175
- "The shell is mightier than the sword.",
176
- "A journey of a thousand commits begins with a single push.",
177
- "In case of fire: git commit, git push, leave building.",
178
- "Debugging is like being the detective in a crime movie where you are also the murderer.",
179
- "Unix is user-friendly. It's just selective about who its friends are.",
180
- "Old sysadmins never die, they just become daemons.",
181
- "Listen you flatpaker! – Totally Terry Davis",
182
- "How is #{detect_distro}? 🤔",
183
- "Life is short, but your command history is eternal.",
184
- "If at first you don’t succeed, git commit and push anyway.",
185
- "rm -rf: the ultimate trust exercise.",
186
- "Coding is like magic, but with more coffee.",
187
- "There’s no bug, only undocumented features.",
188
- "Keep your friends close and your aliases closer.",
189
- "Why wait for the future when you can Ctrl+Z it?",
190
- "A watched process never completes.",
191
- "When in doubt, make it a function.",
192
- "Some call it procrastination, we call it debugging curiosity.",
193
- "Life is like a terminal; some commands just don’t execute.",
194
- "Good code is like a good joke; it needs no explanation.",
195
- "sudo: because sometimes responsibility is overrated.",
196
- "Pipes make the world go round.",
197
- "In bash we trust, in Ruby we wonder.",
198
- "A system without errors is like a day without coffee.",
199
- "Keep your loops tight and your sleeps short.",
200
- "Stack traces are just life giving you directions.",
201
- "Your mom called, she wants her semicolons back."
202
- ]
203
-
204
- $current_quote = QUOTES.sample
205
-
206
- def dynamic_quote
207
- chars = $current_quote.chars
208
- rainbow = rainbow_codes.cycle
209
- chars.map { |c| color(c, rainbow.next) }.join
210
- end
211
-
212
- # ---------------- CPU / RAM / Storage ----------------
213
- def read_cpu_times
214
- return [] unless File.exist?('/proc/stat')
215
- cpu_line = File.readlines('/proc/stat').find { |line| line.start_with?('cpu ') }
216
- return [] unless cpu_line
217
- cpu_line.split[1..-1].map(&:to_i)
218
- end
219
-
220
- def calculate_cpu_usage(prev, current)
221
- return 0.0 if prev.empty? || current.empty?
222
- prev_idle = prev[3] + (prev[4] || 0)
223
- idle = current[3] + (current[4] || 0)
224
- prev_non_idle = prev[0] + prev[1] + prev[2] +
225
- (prev[5] || 0) + (prev[6] || 0) + (prev[7] || 0)
226
- non_idle = current[0] + current[1] + current[2] +
227
- (current[5] || 0) + (current[6] || 0) + (current[7] || 0)
228
- prev_total = prev_idle + prev_non_idle
229
- total = idle + non_idle
230
- totald = total - prev_total
231
- idled = idle - prev_idle
232
- return 0.0 if totald <= 0
233
- ((totald - idled).to_f / totald) * 100
234
- end
235
-
236
- def cpu_cores_and_freq
237
- return [0, []] unless File.exist?('/proc/cpuinfo')
238
- cores = 0
239
- freqs = []
240
- File.foreach('/proc/cpuinfo') do |line|
241
- cores += 1 if line =~ /^processor\s*:\s*\d+/
242
- if line =~ /^cpu MHz\s*:\s*([\d.]+)/
243
- freqs << $1.to_f
244
- end
245
- end
246
- [cores, freqs.first(cores)]
247
- end
248
-
249
- def cpu_info
250
- usage = 0.0
251
- cores = 0
252
- freq_display = "N/A"
253
-
254
- case os_type
255
- when :linux
256
- prev = read_cpu_times
257
- sleep 0.05
258
- current = read_cpu_times
259
- usage = calculate_cpu_usage(prev, current).round(1)
260
- cores, freqs = cpu_cores_and_freq
261
- freq_display = freqs.empty? ? "N/A" : freqs.map { |f| "#{f.round(0)}MHz" }.join(', ')
262
- else
263
- cores = begin
264
- `sysctl -n hw.ncpu 2>/dev/null`.to_i
265
- rescue
266
- 0
267
- end
268
-
269
- raw_freq_hz = begin
270
- `sysctl -n hw.cpufrequency 2>/dev/null`.to_i
271
- rescue
272
- 0
273
- end
274
-
275
- freq_display =
276
- if raw_freq_hz > 0
277
- mhz = (raw_freq_hz.to_f / 1_000_000.0).round(0)
278
- "#{mhz.to_i}MHz"
279
- else
280
- "N/A"
281
- end
282
-
283
- usage = begin
284
- ps_output = `ps -A -o %cpu 2>/dev/null`
285
- lines = ps_output.lines
286
- values = lines[1..-1] || []
287
- sum = values.map { |l| l.to_f }.inject(0.0, :+)
288
- if cores > 0
289
- (sum / cores).round(1)
290
- else
291
- sum.round(1)
292
- end
293
- rescue
294
- 0.0
295
- end
296
- end
297
-
298
- "#{color("CPU Usage:",36)} #{color("#{usage}%",33)} | " \
299
- "#{color("Cores:",36)} #{color(cores.to_s,32)} | " \
300
- "#{color("Freqs:",36)} #{color(freq_display,35)}"
301
- end
302
-
303
- def ram_info
304
- case os_type
305
- when :linux
306
- if File.exist?('/proc/meminfo')
307
- meminfo = {}
308
- File.read('/proc/meminfo').each_line do |line|
309
- key, val = line.split(':')
310
- meminfo[key.strip] = val.strip.split.first.to_i * 1024 if key && val
311
- end
312
- total = meminfo['MemTotal'] || 0
313
- free = (meminfo['MemFree'] || 0) + (meminfo['Buffers'] || 0) + (meminfo['Cached'] || 0)
314
- used = total - free
315
- "#{color("RAM Usage:",36)} #{color(human_bytes(used),33)} / #{color(human_bytes(total),32)}"
316
- else
317
- "#{color("RAM Usage:",36)} Info not available"
318
- end
319
- else
320
- begin
321
- if os_type == :mac
322
- total = `sysctl -n hw.memsize 2>/dev/null`.to_i
323
- return "#{color("RAM Usage:",36)} Info not available" if total <= 0
324
-
325
- vm = `vm_stat 2>/dev/null`
326
- page_size = vm[/page size of (\d+) bytes/, 1].to_i
327
- page_size = 4096 if page_size <= 0
328
-
329
- stats = {}
330
- vm.each_line do |line|
331
- if line =~ /^(.+):\s+(\d+)\./
332
- stats[$1] = $2.to_i
333
- end
334
- end
335
-
336
- used_pages = 0
337
- %w[Pages active Pages wired down Pages occupied by compressor].each do |k|
338
- used_pages += stats[k].to_i
339
- end
340
- used = used_pages * page_size
341
-
342
- "#{color("RAM Usage:",36)} #{color(human_bytes(used),33)} / #{color(human_bytes(total),32)}"
343
- else
344
- total = `sysctl -n hw.physmem 2>/dev/null`.to_i
345
- total = `sysctl -n hw.realmem 2>/dev/null`.to_i if total <= 0
346
- return "#{color("RAM Usage:",36)} Info not available" if total <= 0
347
- "#{color("RAM Usage:",36)} #{color("Unknown",33)} / #{color(human_bytes(total),32)}"
348
- end
349
- rescue
350
- "#{color("RAM Usage:",36)} Info not available"
351
- end
352
- end
353
- end
354
-
355
- def storage_info
133
+ end
134
+
135
+ # ---------------- Aliases ----------------
136
+ def expand_aliases(cmd, seen = [])
137
+ return cmd if cmd.nil? || cmd.strip.empty?
138
+ first_word, rest = cmd.strip.split(' ', 2)
139
+ return cmd if seen.include?(first_word)
140
+ seen << first_word
141
+
142
+ if $aliases.key?(first_word)
143
+ replacement = $aliases[first_word]
144
+ expanded = expand_aliases(replacement, seen)
145
+ rest ? "#{expanded} #{rest}" : expanded
146
+ else
147
+ cmd
148
+ end
149
+ end
150
+
151
+ # ---------------- System Info ----------------
152
+ def current_time
153
+ Time.now.strftime("%Y-%m-%d %H:%M:%S %Z")
154
+ end
155
+
156
+ def detect_distro
157
+ if File.exist?('/etc/os-release')
158
+ line = File.read('/etc/os-release').lines.find { |l|
159
+ l.start_with?('PRETTY_NAME="') || l.start_with?('PRETTY_NAME=')
160
+ }
161
+ return line.split('=').last.strip.delete('"') if line
162
+ end
163
+ "#{RbConfig::CONFIG['host_os']}"
164
+ end
165
+
166
+ def os_type
167
+ host = RbConfig::CONFIG['host_os'].to_s
168
+ case host
169
+ when /linux/i
170
+ :linux
171
+ when /darwin/i
172
+ :mac
173
+ when /bsd/i
174
+ :bsd
175
+ else
176
+ :other
177
+ end
178
+ end
179
+
180
+ # ---------------- Quotes ----------------
181
+ QUOTES = [
182
+ "Keep calm and code on.",
183
+ "Did you try turning it off and on again?",
184
+ "There’s no place like 127.0.0.1.",
185
+ "To iterate is human, to recurse divine.",
186
+ "sudo rm -rf / – Just kidding, don’t do that!",
187
+ "The shell is mightier than the sword.",
188
+ "A journey of a thousand commits begins with a single push.",
189
+ "In case of fire: git commit, git push, leave building.",
190
+ "Debugging is like being the detective in a crime movie where you are also the murderer.",
191
+ "Unix is user-friendly. It's just selective about who its friends are.",
192
+ "Old sysadmins never die, they just become daemons.",
193
+ "Listen you flatpaker! – Totally Terry Davis",
194
+ "How is #{detect_distro}? 🤔",
195
+ "Life is short, but your command history is eternal.",
196
+ "If at first you don’t succeed, git commit and push anyway.",
197
+ "rm -rf: the ultimate trust exercise.",
198
+ "Coding is like magic, but with more coffee.",
199
+ "There’s no bug, only undocumented features.",
200
+ "Keep your friends close and your aliases closer.",
201
+ "Why wait for the future when you can Ctrl+Z it?",
202
+ "A watched process never completes.",
203
+ "When in doubt, make it a function.",
204
+ "Some call it procrastination, we call it debugging curiosity.",
205
+ "Life is like a terminal; some commands just don’t execute.",
206
+ "Good code is like a good joke; it needs no explanation.",
207
+ "sudo: because sometimes responsibility is overrated.",
208
+ "Pipes make the world go round.",
209
+ "In bash we trust, in Ruby we wonder.",
210
+ "A system without errors is like a day without coffee.",
211
+ "Keep your loops tight and your sleeps short.",
212
+ "Stack traces are just life giving you directions.",
213
+ "Your mom called, she wants her semicolons back."
214
+ ]
215
+
216
+ $current_quote = QUOTES.sample
217
+
218
+ def dynamic_quote
219
+ chars = $current_quote.chars
220
+ rainbow = rainbow_codes.cycle
221
+ chars.map { |c| color(c, rainbow.next) }.join
222
+ end
223
+
224
+ # ---------------- CPU / RAM / Storage ----------------
225
+ def read_cpu_times
226
+ return [] unless File.exist?('/proc/stat')
227
+ cpu_line = File.readlines('/proc/stat').find { |line| line.start_with?('cpu ') }
228
+ return [] unless cpu_line
229
+ cpu_line.split[1..-1].map(&:to_i)
230
+ end
231
+
232
+ def calculate_cpu_usage(prev, current)
233
+ return 0.0 if prev.empty? || current.empty?
234
+ prev_idle = prev[3] + (prev[4] || 0)
235
+ idle = current[3] + (current[4] || 0)
236
+ prev_non_idle = prev[0] + prev[1] + prev[2] +
237
+ (prev[5] || 0) + (prev[6] || 0) + (prev[7] || 0)
238
+ non_idle = current[0] + current[1] + current[2] +
239
+ (current[5] || 0) + (current[6] || 0) + (current[7] || 0)
240
+ prev_total = prev_idle + prev_non_idle
241
+ total = idle + non_idle
242
+ totald = total - prev_total
243
+ idled = idle - prev_idle
244
+ return 0.0 if totald <= 0
245
+ ((totald - idled).to_f / totald) * 100
246
+ end
247
+
248
+ def cpu_cores_and_freq
249
+ return [0, []] unless File.exist?('/proc/cpuinfo')
250
+ cores = 0
251
+ freqs = []
252
+ File.foreach('/proc/cpuinfo') do |line|
253
+ cores += 1 if line =~ /^processor\s*:\s*\d+/
254
+ if line =~ /^cpu MHz\s*:\s*([\d.]+)/
255
+ freqs << $1.to_f
256
+ end
257
+ end
258
+ [cores, freqs.first(cores)]
259
+ end
260
+
261
+ def cpu_info
262
+ usage = 0.0
263
+ cores = 0
264
+ freq_display = "N/A"
265
+
266
+ case os_type
267
+ when :linux
268
+ prev = read_cpu_times
269
+ sleep 0.05
270
+ current = read_cpu_times
271
+ usage = calculate_cpu_usage(prev, current).round(1)
272
+ cores, freqs = cpu_cores_and_freq
273
+ freq_display = freqs.empty? ? "N/A" : freqs.map { |f| "#{f.round(0)}MHz" }.join(', ')
274
+ else
275
+ cores = begin
276
+ `sysctl -n hw.ncpu 2>/dev/null`.to_i
277
+ rescue
278
+ 0
279
+ end
280
+
281
+ raw_freq_hz = begin
282
+ `sysctl -n hw.cpufrequency 2>/dev/null`.to_i
283
+ rescue
284
+ 0
285
+ end
286
+
287
+ freq_display =
288
+ if raw_freq_hz > 0
289
+ mhz = (raw_freq_hz.to_f / 1_000_000.0).round(0)
290
+ "#{mhz.to_i}MHz"
291
+ else
292
+ "N/A"
293
+ end
294
+
295
+ usage = begin
296
+ ps_output = `ps -A -o %cpu 2>/dev/null`
297
+ lines = ps_output.lines
298
+ values = lines[1..-1] || []
299
+ sum = values.map { |l| l.to_f }.inject(0.0, :+)
300
+ if cores > 0
301
+ (sum / cores).round(1)
302
+ else
303
+ sum.round(1)
304
+ end
305
+ rescue
306
+ 0.0
307
+ end
308
+ end
309
+
310
+ "#{color("CPU Usage:",36)} #{color("#{usage}%",33)} | " \
311
+ "#{color("Cores:",36)} #{color(cores.to_s,32)} | " \
312
+ "#{color("Freqs:",36)} #{color(freq_display,35)}"
313
+ end
314
+
315
+ def ram_info
316
+ case os_type
317
+ when :linux
318
+ if File.exist?('/proc/meminfo')
319
+ meminfo = {}
320
+ File.read('/proc/meminfo').each_line do |line|
321
+ key, val = line.split(':')
322
+ meminfo[key.strip] = val.strip.split.first.to_i * 1024 if key && val
323
+ end
324
+ total = meminfo['MemTotal'] || 0
325
+ free = (meminfo['MemFree'] || 0) + (meminfo['Buffers'] || 0) + (meminfo['Cached'] || 0)
326
+ used = total - free
327
+ "#{color("RAM Usage:",36)} #{color(human_bytes(used),33)} / #{color(human_bytes(total),32)}"
328
+ else
329
+ "#{color("RAM Usage:",36)} Info not available"
330
+ end
331
+ else
332
+ begin
333
+ if os_type == :mac
334
+ total = `sysctl -n hw.memsize 2>/dev/null`.to_i
335
+ return "#{color("RAM Usage:",36)} Info not available" if total <= 0
336
+
337
+ vm = `vm_stat 2>/dev/null`
338
+ page_size = vm[/page size of (\d+) bytes/, 1].to_i
339
+ page_size = 4096 if page_size <= 0
340
+
341
+ stats = {}
342
+ vm.each_line do |line|
343
+ if line =~ /^(.+):\s+(\d+)\./
344
+ stats[$1] = $2.to_i
345
+ end
346
+ end
347
+
348
+ used_pages = 0
349
+ %w[Pages active Pages wired down Pages occupied by compressor].each do |k|
350
+ used_pages += stats[k].to_i
351
+ end
352
+ used = used_pages * page_size
353
+
354
+ "#{color("RAM Usage:",36)} #{color(human_bytes(used),33)} / #{color(human_bytes(total),32)}"
355
+ else
356
+ total = `sysctl -n hw.physmem 2>/dev/null`.to_i
357
+ total = `sysctl -n hw.realmem 2>/dev/null`.to_i if total <= 0
358
+ return "#{color("RAM Usage:",36)} Info not available" if total <= 0
359
+ "#{color("RAM Usage:",36)} #{color("Unknown",33)} / #{color(human_bytes(total),32)}"
360
+ end
361
+ rescue
362
+ "#{color("RAM Usage:",36)} Info not available"
363
+ end
364
+ end
365
+ end
366
+
367
+ def storage_info
356
368
  begin
357
369
  require 'sys/filesystem'
358
370
  stat = Sys::Filesystem.stat(Dir.pwd)
@@ -382,6 +394,7 @@ def builtin_help
382
394
  puts color(sprintf("%-15s", "systemfetch"), "1;36") + "Display system information"
383
395
  puts color(sprintf("%-15s", "hist"), "1;36") + "Show shell history"
384
396
  puts color(sprintf("%-15s", "clearhist"), "1;36") + "Clear saved history (memory + file)"
397
+ puts color(sprintf("%-15s", "put"), "1;36") + "Print text (like echo)"
385
398
  puts color(sprintf("%-15s", "help"), "1;36") + "Show this help message"
386
399
  puts color('=' * 60, "1;35")
387
400
  end
@@ -560,6 +573,147 @@ def builtin_ls(path = ".")
560
573
  print_columns_colored(labels)
561
574
  end
562
575
 
576
+ # ---------------- rsh scripting helpers ----------------
577
+
578
+ # Evaluate rsh condition expressions, Ruby-style with $VARS
579
+ def eval_rsh_expr(expr)
580
+ return false if expr.nil? || expr.strip.empty?
581
+ s = expr.to_s
582
+
583
+ s = s.gsub(/\$([A-Za-z_][A-Za-z0-9_]*)/) do
584
+ (ENV[$1] || "").inspect
585
+ end
586
+
587
+ s = s.gsub(/\$(\d+)/) do
588
+ idx = $1.to_i
589
+ val = ($rsh_positional && $rsh_positional[idx]) || ""
590
+ val.inspect
591
+ end
592
+
593
+ begin
594
+ !!eval(s)
595
+ rescue
596
+ false
597
+ end
598
+ end
599
+
600
+ def rsh_find_if_bounds(lines, start_idx)
601
+ depth = 1
602
+ else_idx = nil
603
+ i = start_idx + 1
604
+ while i < lines.length
605
+ line = lines[i].to_s.strip
606
+ if line.start_with?("if ")
607
+ depth += 1
608
+ elsif line.start_with?("while ")
609
+ depth += 1
610
+ elsif line.start_with?("fn ")
611
+ depth += 1
612
+ elsif line == "end"
613
+ depth -= 1
614
+ return [else_idx, i] if depth == 0
615
+ elsif line == "else" && depth == 1
616
+ else_idx = i
617
+ end
618
+ i += 1
619
+ end
620
+ raise "Unmatched 'if' in rsh script"
621
+ end
622
+
623
+ def rsh_find_block_end(lines, start_idx)
624
+ depth = 1
625
+ i = start_idx + 1
626
+ while i < lines.length
627
+ line = lines[i].to_s.strip
628
+ if line.start_with?("if ") || line.start_with?("while ") || line.start_with?("fn ")
629
+ depth += 1
630
+ elsif line == "end"
631
+ depth -= 1
632
+ return i if depth == 0
633
+ end
634
+ i += 1
635
+ end
636
+ raise "Unmatched block in rsh script"
637
+ end
638
+
639
+ def run_rsh_block(lines, start_idx, end_idx)
640
+ i = start_idx
641
+ while i < end_idx
642
+ raw = lines[i]
643
+ i += 1
644
+ next if raw.nil?
645
+ line = raw.strip
646
+ next if line.empty? || line.start_with?("#")
647
+
648
+ if line.start_with?("if ")
649
+ cond_expr = line[3..-1].strip
650
+ else_idx, end_idx_2 = rsh_find_if_bounds(lines, i - 1)
651
+ if eval_rsh_expr(cond_expr)
652
+ body_end = else_idx || end_idx_2
653
+ run_rsh_block(lines, i, body_end)
654
+ elsif else_idx
655
+ run_rsh_block(lines, else_idx + 1, end_idx_2)
656
+ end
657
+ i = end_idx_2 + 1
658
+ next
659
+ elsif line.start_with?("while ")
660
+ cond_expr = line[6..-1].strip
661
+ block_end = rsh_find_block_end(lines, i - 1)
662
+ while eval_rsh_expr(cond_expr)
663
+ run_rsh_block(lines, i, block_end)
664
+ end
665
+ i = block_end + 1
666
+ next
667
+ elsif line.start_with?("fn ")
668
+ parts = line.split
669
+ name = parts[1]
670
+ argnames = parts[2..-1] || []
671
+ block_end = rsh_find_block_end(lines, i - 1)
672
+ $rsh_functions[name] = {
673
+ args: argnames,
674
+ body: lines[i...block_end]
675
+ }
676
+ i = block_end + 1
677
+ next
678
+ else
679
+ run_input_line(line)
680
+ end
681
+ end
682
+ end
683
+
684
+ def rsh_run_script(script_path, argv)
685
+ $rsh_script_mode = true
686
+ $rsh_positional = {}
687
+ $rsh_positional[0] = File.basename(script_path)
688
+ argv.each_with_index do |val, idx|
689
+ $rsh_positional[idx + 1] = val
690
+ end
691
+
692
+ lines = File.readlines(script_path, chomp: true)
693
+ if lines[0] && lines[0].start_with?("#!")
694
+ lines = lines[1..-1] || []
695
+ end
696
+ run_rsh_block(lines, 0, lines.length)
697
+ end
698
+
699
+ def rsh_call_function(name, argv)
700
+ fn = $rsh_functions[name]
701
+ return unless fn
702
+
703
+ saved_positional = $rsh_positional
704
+ $rsh_positional = {}
705
+ $rsh_positional[0] = name
706
+ fn[:args].each_with_index do |argname, idx|
707
+ val = argv[idx] || ""
708
+ ENV[argname] = val
709
+ $rsh_positional[idx + 1] = val
710
+ end
711
+
712
+ run_rsh_block(fn[:body], 0, fn[:body].length)
713
+ ensure
714
+ $rsh_positional = saved_positional
715
+ end
716
+
563
717
  # ---------------- External Execution Helper ----------------
564
718
  def exec_external(args, stdin_file, stdout_file, append)
565
719
  command_path = args[0]
@@ -601,6 +755,7 @@ def exec_external(args, stdin_file, stdout_file, append)
601
755
  $child_pids << pid
602
756
  begin
603
757
  Process.wait(pid)
758
+ $last_status = $?.exitstatus || 0
604
759
  rescue Interrupt
605
760
  ensure
606
761
  $child_pids.delete(pid)
@@ -616,6 +771,12 @@ def run_command(cmd)
616
771
  args = Shellwords.shellsplit(cmd) rescue []
617
772
  return if args.empty?
618
773
 
774
+ # rsh functions
775
+ if $rsh_functions.key?(args[0])
776
+ rsh_call_function(args[0], args[1..-1] || [])
777
+ return
778
+ end
779
+
619
780
  case args[0]
620
781
  when 'ls'
621
782
  if args.length == 1
@@ -677,6 +838,10 @@ def run_command(cmd)
677
838
  when 'clearhist'
678
839
  builtin_clearhist
679
840
  return
841
+ when 'put'
842
+ msg = args[1..-1].join(' ')
843
+ puts msg
844
+ return
680
845
  end
681
846
 
682
847
  exec_external(args, stdin_file, stdout_file, append)
@@ -935,6 +1100,10 @@ def read_line_with_ghost(prompt_str)
935
1100
  else
936
1101
  # ignore when line not empty
937
1102
  end
1103
+ when "\u0001" # Ctrl-A - move to beginning of line
1104
+ cursor = 0
1105
+ last_tab_prefix = nil
1106
+ tab_cycle = 0
938
1107
  when "\u007F", "\b" # Backspace
939
1108
  if cursor > 0
940
1109
  buffer.slice!(cursor - 1)
@@ -1022,6 +1191,17 @@ def print_welcome
1022
1191
  puts
1023
1192
  end
1024
1193
 
1194
+ # ---------------- Script vs interactive entry ----------------
1195
+ if ARGV[0]
1196
+ script_path = ARGV.shift
1197
+ begin
1198
+ rsh_run_script(script_path, ARGV)
1199
+ rescue => e
1200
+ STDERR.puts "rsh script error: #{e.class}: #{e.message}"
1201
+ end
1202
+ exit 0
1203
+ end
1204
+
1025
1205
  print_welcome
1026
1206
 
1027
1207
  # ---------------- Main Loop ----------------
data/lib/srsh/version.rb CHANGED
@@ -1,4 +1,4 @@
1
1
  # lib/srsh/version.rb
2
2
  module Srsh
3
- VERSION = "0.6.1-HOTFIX"
3
+ VERSION = "0.7.0"
4
4
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: srsh
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.1.pre.HOTFIX
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - RobertFlexx