srsh 0.6.2 → 0.7.1

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 +660 -213
  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: c04c9c3c80b2bf7d0e2b070ab02439dda32474e2d2e2abb55d326517f24d56ce
4
- data.tar.gz: 65aa5c728d64a1fd916d1e65bd1b0086c998041e60b4b914dfbbcb35c9e48101
3
+ metadata.gz: 0c0910382ad05f07b93041cfbecb7d3db98f24e23df1ecd5d23243aab35a7f63
4
+ data.tar.gz: c39ce2184076112bb950035ea2f09939bef9f4105b2c207e6ec73ee152fc3c89
5
5
  SHA512:
6
- metadata.gz: 1e5305c079a3fdf81d63aa18afe60ad56ee163df1071fda6ad087bbeac811779423e50e2fd3d3930ce18c5af06e192ae610e085c76f971866f80888e31bd8287
7
- data.tar.gz: 31900c7a88931ac85ab734cdc153df13c26f6b86ac2049ecb79f0f2dd34decd7d6f157d32f0e72ef970ba73a4c4bebcd18d3a7594d15c9b10913f8331ac17ba3
6
+ metadata.gz: ee7bd7e5ae91582ed612171eda2f3440d16d8d07988b465fd61470e094d80b0f766cfc6f248d2980f0371a0db228593a326934fe7f6c4367fb8e9b009213e26f
7
+ data.tar.gz: 6195a9d026471e57b1b7b8f87970362261302d0e7f8b936a030ecc02bce7de2c56d3420d2e7cee0b7810f60acc32f5a171bd45acd1181ffc7d2469389f37ebd7
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.2"
10
+ SRSH_VERSION = "0.7.1"
11
11
 
12
12
  $0 = "srsh-#{SRSH_VERSION}"
13
13
  ENV['SHELL'] = "srsh-#{SRSH_VERSION}"
@@ -15,228 +15,276 @@ print "\033]0;srsh-#{SRSH_VERSION}\007"
15
15
 
16
16
  Dir.chdir(ENV['HOME']) if ENV['HOME']
17
17
 
18
+ # ---------------- Globals ----------------
18
19
  $child_pids = []
19
20
  $aliases = {}
20
21
  $last_render_rows = 0
21
22
 
23
+ $last_status = 0
24
+ $rsh_functions = {}
25
+ $rsh_positional = {}
26
+ $rsh_script_mode = false
27
+
22
28
  Signal.trap("INT", "IGNORE")
23
29
 
24
- # ---------------- History ----------------
25
- HISTORY_FILE = File.join(Dir.home, ".srsh_history")
26
- HISTORY = if File.exist?(HISTORY_FILE)
27
- File.readlines(HISTORY_FILE, chomp: true)
28
- else
29
- []
30
- end
30
+ # Control-flow exceptions for the scripting engine
31
+ class RshBreak < StandardError; end
32
+ class RshContinue < StandardError; end
33
+ class RshReturn < StandardError; end
31
34
 
32
- at_exit do
33
- begin
34
- File.open(HISTORY_FILE, "w") do |f|
35
- HISTORY.each { |line| f.puts line }
35
+ # ---------------- History ----------------
36
+ HISTORY_FILE = File.join(Dir.home, ".srsh_history")
37
+ HISTORY = if File.exist?(HISTORY_FILE)
38
+ File.readlines(HISTORY_FILE, chomp: true)
39
+ else
40
+ []
41
+ end
42
+
43
+ at_exit do
44
+ begin
45
+ File.open(HISTORY_FILE, "w") do |f|
46
+ HISTORY.each { |line| f.puts line }
47
+ end
48
+ rescue
49
+ end
36
50
  end
37
- rescue
38
- end
39
- end
40
51
 
41
- # ---------------- RC file (create if missing) ----------------
42
- RC_FILE = File.join(Dir.home, ".srshrc")
43
- begin
44
- unless File.exist?(RC_FILE)
45
- File.write(RC_FILE, <<~RC)
52
+ # ---------------- RC file (create if missing) ----------------
53
+ RC_FILE = File.join(Dir.home, ".srshrc")
54
+ begin
55
+ unless File.exist?(RC_FILE)
56
+ File.write(RC_FILE, <<~RC)
46
57
  # ~/.srshrc — srsh configuration
47
58
  # This file was created automatically by srsh v#{SRSH_VERSION}.
48
59
  # You can keep personal notes or planned settings here.
49
60
  # (Currently not sourced by srsh runtime.)
50
- RC
51
- end
52
- rescue
53
- end
54
-
55
- # ---------------- Utilities ----------------
56
- def color(text, code)
57
- "\e[#{code}m#{text}\e[0m"
58
- end
59
-
60
- def random_color
61
- [31, 32, 33, 34, 35, 36, 37].sample
62
- end
63
-
64
- def rainbow_codes
65
- [31, 33, 32, 36, 34, 35, 91, 93, 92, 96, 94, 95]
66
- end
67
-
68
- def expand_vars(str)
69
- str.gsub(/\$([a-zA-Z_][a-zA-Z0-9_]*)/) { ENV[$1] || "" }
70
- end
71
-
72
- def parse_redirection(cmd)
73
- stdin_file = nil
74
- stdout_file = nil
75
- append = false
76
-
77
- if cmd =~ /(.*)>>\s*(\S+)/
78
- cmd = $1.strip
79
- stdout_file = $2.strip
80
- append = true
81
- elsif cmd =~ /(.*)>\s*(\S+)/
82
- cmd = $1.strip
83
- stdout_file = $2.strip
84
- end
85
-
86
- if cmd =~ /(.*)<\s*(\S+)/
87
- cmd = $1.strip
88
- stdin_file = $2.strip
89
- end
90
-
91
- [cmd, stdin_file, stdout_file, append]
92
- end
93
-
94
- def human_bytes(bytes)
95
- units = ['B', 'KB', 'MB', 'GB', 'TB']
96
- size = bytes.to_f
97
- unit = units.shift
98
- while size > 1024 && !units.empty?
99
- size /= 1024
100
- unit = units.shift
101
- end
102
- "#{format('%.2f', size)} #{unit}"
103
- end
104
-
105
- def nice_bar(p, w = 30, code = 32)
106
- p = [[p, 0.0].max, 1.0].min
107
- f = (p * w).round
108
- b = "█" * f + "░" * (w - f)
109
- pct = (p * 100).to_i
110
- "#{color("[#{b}]", code)} #{color(sprintf("%3d%%", pct), 37)}"
111
- end
112
-
113
- def terminal_width
114
- IO.console.winsize[1]
115
- rescue
116
- 80
117
- end
118
-
119
- def strip_ansi(str)
120
- 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
61
+ RC
62
+ end
63
+ rescue
64
+ end
138
65
 
139
- # ---------------- System Info ----------------
140
- def current_time
141
- Time.now.strftime("%Y-%m-%d %H:%M:%S %Z")
142
- end
66
+ # ---------------- Utilities ----------------
67
+ def color(text, code)
68
+ "\e[#{code}m#{text}\e[0m"
69
+ end
143
70
 
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
71
+ def random_color
72
+ [31, 32, 33, 34, 35, 36, 37].sample
73
+ end
153
74
 
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
75
+ def rainbow_codes
76
+ [31, 33, 32, 36, 34, 35, 91, 93, 92, 96, 94, 95]
77
+ end
167
78
 
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
79
+ def human_bytes(bytes)
80
+ units = ['B', 'KB', 'MB', 'GB', 'TB']
81
+ size = bytes.to_f
82
+ unit = units.shift
83
+ while size > 1024 && !units.empty?
84
+ size /= 1024
85
+ unit = units.shift
86
+ end
87
+ "#{format('%.2f', size)} #{unit}"
88
+ end
211
89
 
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
90
+ def nice_bar(p, w = 30, code = 32)
91
+ p = [[p, 0.0].max, 1.0].min
92
+ f = (p * w).round
93
+ b = "█" * f + "░" * (w - f)
94
+ pct = (p * 100).to_i
95
+ "#{color("[#{b}]", code)} #{color(sprintf("%3d%%", pct), 37)}"
96
+ end
219
97
 
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
98
+ def terminal_width
99
+ IO.console.winsize[1]
100
+ rescue
101
+ 80
102
+ end
235
103
 
236
- def cpu_cores_and_freq
237
- return [0, []] unless File.exist?('/proc/cpuinfo')
238
- cores = 0
239
- freqs = []
104
+ def strip_ansi(str)
105
+ str.to_s.gsub(/\e\[[0-9;]*m/, '')
106
+ end
107
+
108
+ # Simple $(...) command substitution (no nesting)
109
+ def expand_command_substitutions(str)
110
+ return "" if str.nil?
111
+ s = str.to_s.dup
112
+
113
+ s.gsub(/\$\(([^()]*)\)/) do
114
+ inner = $1.to_s.strip
115
+ next "" if inner.empty?
116
+ begin
117
+ out = `#{inner} 2>/dev/null`
118
+ out.to_s.strip
119
+ rescue
120
+ ""
121
+ end
122
+ end
123
+ end
124
+
125
+ # variable expansion: $VAR, $1, $2, $0, $?
126
+ def expand_vars(str)
127
+ return "" if str.nil?
128
+
129
+ # First handle $(...) substitution
130
+ s = expand_command_substitutions(str.to_s)
131
+
132
+ # $VARNAME from ENV
133
+ s = s.gsub(/\$([a-zA-Z_][a-zA-Z0-9_]*)/) do
134
+ ENV[$1] || ""
135
+ end
136
+
137
+ # $1, $2, $0 from positional table
138
+ s = s.gsub(/\$(\d+)/) do
139
+ idx = $1.to_i
140
+ ($rsh_positional && $rsh_positional[idx]) || ""
141
+ end
142
+
143
+ # $? -> last exit status
144
+ s = s.gsub(/\$\?/) { $last_status.to_s }
145
+
146
+ s
147
+ end
148
+
149
+ def parse_redirection(cmd)
150
+ stdin_file = nil
151
+ stdout_file = nil
152
+ append = false
153
+
154
+ if cmd =~ /(.*)>>\s*(\S+)/
155
+ cmd = $1.strip
156
+ stdout_file = $2.strip
157
+ append = true
158
+ elsif cmd =~ /(.*)>\s*(\S+)/
159
+ cmd = $1.strip
160
+ stdout_file = $2.strip
161
+ end
162
+
163
+ if cmd =~ /(.*)<\s*(\S+)/
164
+ cmd = $1.strip
165
+ stdin_file = $2.strip
166
+ end
167
+
168
+ [cmd, stdin_file, stdout_file, append]
169
+ end
170
+
171
+ # ---------------- Aliases ----------------
172
+ def expand_aliases(cmd, seen = [])
173
+ return cmd if cmd.nil? || cmd.strip.empty?
174
+ first_word, rest = cmd.strip.split(' ', 2)
175
+ return cmd if seen.include?(first_word)
176
+ seen << first_word
177
+
178
+ if $aliases.key?(first_word)
179
+ replacement = $aliases[first_word]
180
+ expanded = expand_aliases(replacement, seen)
181
+ rest ? "#{expanded} #{rest}" : expanded
182
+ else
183
+ cmd
184
+ end
185
+ end
186
+
187
+ # ---------------- System Info ----------------
188
+ def current_time
189
+ Time.now.strftime("%Y-%m-%d %H:%M:%S %Z")
190
+ end
191
+
192
+ def detect_distro
193
+ if File.exist?('/etc/os-release')
194
+ line = File.read('/etc/os-release').lines.find { |l|
195
+ l.start_with?('PRETTY_NAME="') || l.start_with?('PRETTY_NAME=')
196
+ }
197
+ return line.split('=').last.strip.delete('"') if line
198
+ end
199
+ "#{RbConfig::CONFIG['host_os']}"
200
+ end
201
+
202
+ def os_type
203
+ host = RbConfig::CONFIG['host_os'].to_s
204
+ case host
205
+ when /linux/i
206
+ :linux
207
+ when /darwin/i
208
+ :mac
209
+ when /bsd/i
210
+ :bsd
211
+ else
212
+ :other
213
+ end
214
+ end
215
+
216
+ # ---------------- Quotes ----------------
217
+ QUOTES = [
218
+ "Keep calm and code on.",
219
+ "Did you try turning it off and on again?",
220
+ "There’s no place like 127.0.0.1.",
221
+ "To iterate is human, to recurse divine.",
222
+ "sudo rm -rf / – Just kidding, don’t do that!",
223
+ "The shell is mightier than the sword.",
224
+ "A journey of a thousand commits begins with a single push.",
225
+ "In case of fire: git commit, git push, leave building.",
226
+ "Debugging is like being the detective in a crime movie where you are also the murderer.",
227
+ "Unix is user-friendly. It's just selective about who its friends are.",
228
+ "Old sysadmins never die, they just become daemons.",
229
+ "Listen you flatpaker! – Totally Terry Davis",
230
+ "How is #{detect_distro}? 🤔",
231
+ "Life is short, but your command history is eternal.",
232
+ "If at first you don’t succeed, git commit and push anyway.",
233
+ "rm -rf: the ultimate trust exercise.",
234
+ "Coding is like magic, but with more coffee.",
235
+ "There’s no bug, only undocumented features.",
236
+ "Keep your friends close and your aliases closer.",
237
+ "Why wait for the future when you can Ctrl+Z it?",
238
+ "A watched process never completes.",
239
+ "When in doubt, make it a function.",
240
+ "Some call it procrastination, we call it debugging curiosity.",
241
+ "Life is like a terminal; some commands just don’t execute.",
242
+ "Good code is like a good joke; it needs no explanation.",
243
+ "sudo: because sometimes responsibility is overrated.",
244
+ "Pipes make the world go round.",
245
+ "In bash we trust, in Ruby we wonder.",
246
+ "A system without errors is like a day without coffee.",
247
+ "Keep your loops tight and your sleeps short.",
248
+ "Stack traces are just life giving you directions.",
249
+ "Your mom called, she wants her semicolons back."
250
+ ]
251
+
252
+ $current_quote = QUOTES.sample
253
+
254
+ def dynamic_quote
255
+ chars = $current_quote.chars
256
+ rainbow = rainbow_codes.cycle
257
+ chars.map { |c| color(c, rainbow.next) }.join
258
+ end
259
+
260
+ # ---------------- CPU / RAM / Storage ----------------
261
+ def read_cpu_times
262
+ return [] unless File.exist?('/proc/stat')
263
+ cpu_line = File.readlines('/proc/stat').find { |line| line.start_with?('cpu ') }
264
+ return [] unless cpu_line
265
+ cpu_line.split[1..-1].map(&:to_i)
266
+ end
267
+
268
+ def calculate_cpu_usage(prev, current)
269
+ return 0.0 if prev.empty? || current.empty?
270
+ prev_idle = prev[3] + (prev[4] || 0)
271
+ idle = current[3] + (current[4] || 0)
272
+ prev_non_idle = prev[0] + prev[1] + prev[2] +
273
+ (prev[5] || 0) + (prev[6] || 0) + (prev[7] || 0)
274
+ non_idle = current[0] + current[1] + current[2] +
275
+ (current[5] || 0) + (current[6] || 0) + (current[7] || 0)
276
+ prev_total = prev_idle + prev_non_idle
277
+ total = idle + non_idle
278
+ totald = total - prev_total
279
+ idled = idle - prev_idle
280
+ return 0.0 if totald <= 0
281
+ ((totald - idled).to_f / totald) * 100
282
+ end
283
+
284
+ def cpu_cores_and_freq
285
+ return [0, []] unless File.exist?('/proc/cpuinfo')
286
+ cores = 0
287
+ freqs = []
240
288
  File.foreach('/proc/cpuinfo') do |line|
241
289
  cores += 1 if line =~ /^processor\s*:\s*\d+/
242
290
  if line =~ /^cpu MHz\s*:\s*([\d.]+)/
@@ -382,6 +430,15 @@ def builtin_help
382
430
  puts color(sprintf("%-15s", "systemfetch"), "1;36") + "Display system information"
383
431
  puts color(sprintf("%-15s", "hist"), "1;36") + "Show shell history"
384
432
  puts color(sprintf("%-15s", "clearhist"), "1;36") + "Clear saved history (memory + file)"
433
+ puts color(sprintf("%-15s", "put"), "1;36") + "Print text (like echo)"
434
+ puts color(sprintf("%-15s", "set"), "1;36") + "Set or list variables"
435
+ puts color(sprintf("%-15s", "unset"), "1;36") + "Unset a variable"
436
+ puts color(sprintf("%-15s", "read"), "1;36") + "Read a line into a variable"
437
+ puts color(sprintf("%-15s", "sleep"), "1;36") + "Sleep for N seconds"
438
+ puts color(sprintf("%-15s", "true / false"), "1;36")+ "Always succeed / fail"
439
+ puts color(sprintf("%-15s", "source / ."), "1;36") + "Run another rsh script"
440
+ puts color(sprintf("%-15s", "break / continue"), "1;36") + "Loop control (in scripts)"
441
+ puts color(sprintf("%-15s", "return"), "1;36") + "Return from a function"
385
442
  puts color(sprintf("%-15s", "help"), "1;36") + "Show this help message"
386
443
  puts color('=' * 60, "1;35")
387
444
  end
@@ -560,6 +617,187 @@ def builtin_ls(path = ".")
560
617
  print_columns_colored(labels)
561
618
  end
562
619
 
620
+ # ---------------- rsh scripting helpers ----------------
621
+
622
+ # Evaluate rsh condition expressions, Ruby-style with $VARS
623
+ def eval_rsh_expr(expr)
624
+ return false if expr.nil? || expr.strip.empty?
625
+ s = expr.to_s
626
+
627
+ s = s.gsub(/\$([A-Za-z_][A-Za-z0-9_]*)/) do
628
+ (ENV[$1] || "").inspect
629
+ end
630
+
631
+ s = s.gsub(/\$(\d+)/) do
632
+ idx = $1.to_i
633
+ val = ($rsh_positional && $rsh_positional[idx]) || ""
634
+ val.inspect
635
+ end
636
+
637
+ begin
638
+ !!eval(s)
639
+ rescue
640
+ false
641
+ end
642
+ end
643
+
644
+ def rsh_find_if_bounds(lines, start_idx)
645
+ depth = 1
646
+ else_idx = nil
647
+ i = start_idx + 1
648
+ while i < lines.length
649
+ line = strip_rsh_comment(lines[i].to_s).strip
650
+ if line.start_with?("if ")
651
+ depth += 1
652
+ elsif line.start_with?("while ")
653
+ depth += 1
654
+ elsif line.start_with?("fn ")
655
+ depth += 1
656
+ elsif line == "end"
657
+ depth -= 1
658
+ return [else_idx, i] if depth == 0
659
+ elsif line == "else" && depth == 1
660
+ else_idx = i
661
+ end
662
+ i += 1
663
+ end
664
+ raise "Unmatched 'if' in rsh script"
665
+ end
666
+
667
+ def rsh_find_block_end(lines, start_idx)
668
+ depth = 1
669
+ i = start_idx + 1
670
+ while i < lines.length
671
+ line = strip_rsh_comment(lines[i].to_s).strip
672
+ if line.start_with?("if ") || line.start_with?("while ") || line.start_with?("fn ")
673
+ depth += 1
674
+ elsif line == "end"
675
+ depth -= 1
676
+ return i if depth == 0
677
+ end
678
+ i += 1
679
+ end
680
+ raise "Unmatched block in rsh script"
681
+ end
682
+
683
+ def strip_rsh_comment(line)
684
+ in_single = false
685
+ in_double = false
686
+ escaped = false
687
+ i = 0
688
+
689
+ while i < line.length
690
+ ch = line[i]
691
+ if escaped
692
+ escaped = false
693
+ elsif ch == '\\'
694
+ escaped = true
695
+ elsif ch == "'" && !in_double
696
+ in_single = !in_single
697
+ elsif ch == '"' && !in_single
698
+ in_double = !in_double
699
+ elsif ch == '#' && !in_single && !in_double
700
+ return line[0...i]
701
+ end
702
+ i += 1
703
+ end
704
+
705
+ line
706
+ end
707
+
708
+ def run_rsh_block(lines, start_idx, end_idx)
709
+ i = start_idx
710
+ while i < end_idx
711
+ raw = lines[i]
712
+ i += 1
713
+ next if raw.nil?
714
+
715
+ line = strip_rsh_comment(raw).strip
716
+ next if line.empty?
717
+
718
+ if line.start_with?("if ")
719
+ cond_expr = line[3..-1].strip
720
+ else_idx, end_idx_2 = rsh_find_if_bounds(lines, i - 1)
721
+ if eval_rsh_expr(cond_expr)
722
+ body_end = else_idx || end_idx_2
723
+ run_rsh_block(lines, i, body_end)
724
+ elsif else_idx
725
+ run_rsh_block(lines, else_idx + 1, end_idx_2)
726
+ end
727
+ i = end_idx_2 + 1
728
+ next
729
+
730
+ elsif line.start_with?("while ")
731
+ cond_expr = line[6..-1].strip
732
+ block_end = rsh_find_block_end(lines, i - 1)
733
+ while eval_rsh_expr(cond_expr)
734
+ begin
735
+ run_rsh_block(lines, i, block_end)
736
+ rescue RshBreak
737
+ break
738
+ rescue RshContinue
739
+ next
740
+ end
741
+ end
742
+ i = block_end + 1
743
+ next
744
+
745
+ elsif line.start_with?("fn ")
746
+ parts = line.split
747
+ name = parts[1]
748
+ argnames = parts[2..-1] || []
749
+ block_end = rsh_find_block_end(lines, i - 1)
750
+ $rsh_functions[name] = {
751
+ args: argnames,
752
+ body: lines[i...block_end]
753
+ }
754
+ i = block_end + 1
755
+ next
756
+
757
+ else
758
+ run_input_line(line)
759
+ end
760
+ end
761
+ end
762
+
763
+ def rsh_run_script(script_path, argv)
764
+ $rsh_script_mode = true
765
+ $rsh_positional = {}
766
+ $rsh_positional[0] = File.basename(script_path)
767
+ argv.each_with_index do |val, idx|
768
+ $rsh_positional[idx + 1] = val
769
+ end
770
+
771
+ lines = File.readlines(script_path, chomp: true)
772
+ if lines[0] && lines[0].start_with?("#!")
773
+ lines = lines[1..-1] || []
774
+ end
775
+ run_rsh_block(lines, 0, lines.length)
776
+ end
777
+
778
+ def rsh_call_function(name, argv)
779
+ fn = $rsh_functions[name]
780
+ return unless fn
781
+
782
+ saved_positional = $rsh_positional
783
+ $rsh_positional = {}
784
+ $rsh_positional[0] = name
785
+
786
+ fn[:args].each_with_index do |argname, idx|
787
+ val = argv[idx] || ""
788
+ ENV[argname] = val
789
+ $rsh_positional[idx + 1] = val
790
+ end
791
+
792
+ begin
793
+ run_rsh_block(fn[:body], 0, fn[:body].length)
794
+ rescue RshReturn
795
+ # swallow return
796
+ ensure
797
+ $rsh_positional = saved_positional
798
+ end
799
+ end
800
+
563
801
  # ---------------- External Execution Helper ----------------
564
802
  def exec_external(args, stdin_file, stdout_file, append)
565
803
  command_path = args[0]
@@ -601,6 +839,7 @@ def exec_external(args, stdin_file, stdout_file, append)
601
839
  $child_pids << pid
602
840
  begin
603
841
  Process.wait(pid)
842
+ $last_status = $?.exitstatus || 0
604
843
  rescue Interrupt
605
844
  ensure
606
845
  $child_pids.delete(pid)
@@ -609,37 +848,79 @@ end
609
848
 
610
849
  # ---------------- Command Execution ----------------
611
850
  def run_command(cmd)
612
- cmd = cmd.to_s
613
- cmd = expand_aliases(cmd.strip)
614
- cmd = expand_vars(cmd.strip)
851
+ cmd = cmd.to_s.strip
852
+ return if cmd.empty?
853
+
854
+ cmd = expand_aliases(cmd)
855
+ cmd = expand_vars(cmd)
856
+
857
+ # ---------------- Assignments ----------------
858
+ # Expression-style: VAR = Ruby_expression
859
+ if (m = cmd.match(/\A([A-Za-z_][A-Za-z0-9_]*)\s+=\s+(.+)\z/))
860
+ var = m[1]
861
+ rhs = m[2]
862
+ begin
863
+ value = eval(rhs)
864
+ ENV[var] = value.is_a?(String) ? value : value.to_s
865
+ rescue Exception
866
+ ENV[var] = rhs
867
+ end
868
+ $last_status = 0
869
+ return
870
+ end
871
+
872
+ # Simple shell-style: VAR=value (no spaces)
873
+ if (m = cmd.match(/\A([A-Za-z_][A-Za-z0-9_]*)=(.*)\z/))
874
+ var = m[1]
875
+ val = m[2] || ""
876
+ ENV[var] = val
877
+ $last_status = 0
878
+ return
879
+ end
880
+
881
+ # ---------------- Redirections + args ----------------
615
882
  cmd, stdin_file, stdout_file, append = parse_redirection(cmd)
616
883
  args = Shellwords.shellsplit(cmd) rescue []
617
884
  return if args.empty?
618
885
 
886
+ # rsh functions
887
+ if $rsh_functions.key?(args[0])
888
+ rsh_call_function(args[0], args[1..-1] || [])
889
+ $last_status = 0
890
+ return
891
+ end
892
+
619
893
  case args[0]
620
894
  when 'ls'
621
895
  if args.length == 1
622
896
  builtin_ls(".")
623
- return
624
897
  elsif args.length == 2 && !args[1].start_with?("-")
625
898
  builtin_ls(args[1])
899
+ else
900
+ exec_external(args, stdin_file, stdout_file, append)
626
901
  return
627
902
  end
628
- exec_external(args, stdin_file, stdout_file, append)
903
+ $last_status = 0
629
904
  return
905
+
630
906
  when 'cd'
631
907
  path = args[1] ? File.expand_path(args[1]) : ENV['HOME']
632
908
  if !File.exist?(path)
633
909
  puts color("cd: no such file or directory: #{args[1]}", 31)
910
+ $last_status = 1
634
911
  elsif !File.directory?(path)
635
912
  puts color("cd: not a directory: #{args[1]}", 31)
913
+ $last_status = 1
636
914
  else
637
915
  Dir.chdir(path)
916
+ $last_status = 0
638
917
  end
639
918
  return
919
+
640
920
  when 'exit','quit'
641
921
  $child_pids.each { |pid| Process.kill("TERM", pid) rescue nil }
642
922
  exit 0
923
+
643
924
  when 'alias'
644
925
  if args[1].nil?
645
926
  $aliases.each { |k,v| puts "#{k}='#{v}'" }
@@ -651,42 +932,188 @@ def run_command(cmd)
651
932
  puts color("Invalid alias format", 31)
652
933
  end
653
934
  end
935
+ $last_status = 0
654
936
  return
937
+
655
938
  when 'unalias'
656
939
  if args[1]
657
940
  $aliases.delete(args[1])
941
+ $last_status = 0
658
942
  else
659
943
  puts color("unalias: usage: unalias name", 31)
944
+ $last_status = 1
660
945
  end
661
946
  return
947
+
662
948
  when 'help'
663
949
  builtin_help
950
+ $last_status = 0
664
951
  return
952
+
665
953
  when 'systemfetch'
666
954
  builtin_systemfetch
955
+ $last_status = 0
667
956
  return
957
+
668
958
  when 'jobs'
669
959
  builtin_jobs
960
+ $last_status = 0
670
961
  return
962
+
671
963
  when 'pwd'
672
964
  puts color(Dir.pwd, 36)
965
+ $last_status = 0
673
966
  return
967
+
674
968
  when 'hist'
675
969
  builtin_hist
970
+ $last_status = 0
676
971
  return
972
+
677
973
  when 'clearhist'
678
974
  builtin_clearhist
975
+ $last_status = 0
976
+ return
977
+
978
+ when 'put'
979
+ msg = args[1..-1].join(' ')
980
+ puts msg
981
+ $last_status = 0
982
+ return
983
+
984
+ # -------- New scripting builtins --------
985
+ when 'break'
986
+ raise RshBreak
987
+
988
+ when 'continue'
989
+ raise RshContinue
990
+
991
+ when 'return'
992
+ raise RshReturn
993
+
994
+ when 'set'
995
+ if args.length == 1
996
+ ENV.keys.sort.each do |k|
997
+ puts "#{k}=#{ENV[k]}"
998
+ end
999
+ else
1000
+ var = args[1]
1001
+ val = args[2..-1].join(' ')
1002
+ ENV[var] = val
1003
+ end
1004
+ $last_status = 0
1005
+ return
1006
+
1007
+ when 'unset'
1008
+ if args[1]
1009
+ ENV.delete(args[1])
1010
+ $last_status = 0
1011
+ else
1012
+ puts color("unset: usage: unset VAR", 31)
1013
+ $last_status = 1
1014
+ end
1015
+ return
1016
+
1017
+ when 'read'
1018
+ var = args[1]
1019
+ unless var
1020
+ puts color("read: usage: read VAR", 31)
1021
+ $last_status = 1
1022
+ return
1023
+ end
1024
+ line = STDIN.gets
1025
+ ENV[var] = (line ? line.chomp : "")
1026
+ $last_status = 0
1027
+ return
1028
+
1029
+ when 'true'
1030
+ $last_status = 0
1031
+ return
1032
+
1033
+ when 'false'
1034
+ $last_status = 1
1035
+ return
1036
+
1037
+ when 'sleep'
1038
+ secs = (args[1] || "1").to_f
1039
+ begin
1040
+ Kernel.sleep(secs)
1041
+ $last_status = 0
1042
+ rescue
1043
+ $last_status = 1
1044
+ end
1045
+ return
1046
+
1047
+ when 'source', '.'
1048
+ file = args[1]
1049
+ if file.nil?
1050
+ puts color("source: usage: source FILE", 31)
1051
+ $last_status = 1
1052
+ return
1053
+ end
1054
+ begin
1055
+ rsh_run_script(file, args[2..-1] || [])
1056
+ $last_status = 0
1057
+ rescue => e
1058
+ STDERR.puts "source error: #{e.class}: #{e.message}"
1059
+ $last_status = 1
1060
+ end
679
1061
  return
680
1062
  end
681
1063
 
1064
+ # Fallback to external command
682
1065
  exec_external(args, stdin_file, stdout_file, append)
683
1066
  end
684
1067
 
685
1068
  # ---------------- Chained Commands ----------------
1069
+ def split_commands(input)
1070
+ return [] if input.nil?
1071
+
1072
+ cmds = []
1073
+ buf = +""
1074
+ in_single = false
1075
+ in_double = false
1076
+ escaped = false
1077
+ i = 0
1078
+
1079
+ while i < input.length
1080
+ ch = input[i]
1081
+
1082
+ if escaped
1083
+ buf << ch
1084
+ escaped = false
1085
+ elsif ch == '\\'
1086
+ escaped = true
1087
+ buf << ch
1088
+ elsif ch == "'" && !in_double
1089
+ in_single = !in_single
1090
+ buf << ch
1091
+ elsif ch == '"' && !in_single
1092
+ in_double = !in_double
1093
+ buf << ch
1094
+ elsif !in_single && !in_double && ch == ';'
1095
+ cmd = buf.strip
1096
+ cmds << cmd unless cmd.empty?
1097
+ buf = +""
1098
+ elsif !in_single && !in_double && ch == '&' && input[i + 1] == '&'
1099
+ cmd = buf.strip
1100
+ cmds << cmd unless cmd.empty?
1101
+ buf = +""
1102
+ i += 1
1103
+ else
1104
+ buf << ch
1105
+ end
1106
+
1107
+ i += 1
1108
+ end
1109
+
1110
+ cmd = buf.strip
1111
+ cmds << cmd unless cmd.empty?
1112
+ cmds
1113
+ end
1114
+
686
1115
  def run_input_line(input)
687
- commands = input.split(/&&|;/).map(&:strip)
688
- commands.each do |cmd|
689
- next if cmd.empty?
1116
+ split_commands(input).each do |cmd|
690
1117
  run_command(cmd)
691
1118
  end
692
1119
  end
@@ -919,12 +1346,14 @@ def read_line_with_ghost(prompt_str)
919
1346
  STDOUT.print("\r\n")
920
1347
  STDOUT.flush
921
1348
  break
1349
+
922
1350
  when "\u0003" # Ctrl-C
923
1351
  STDOUT.print("^C\r\n")
924
1352
  STDOUT.flush
925
1353
  status = :interrupt
926
1354
  buffer = ""
927
1355
  break
1356
+
928
1357
  when "\u0004" # Ctrl-D
929
1358
  if buffer.empty?
930
1359
  status = :eof
@@ -935,6 +1364,12 @@ def read_line_with_ghost(prompt_str)
935
1364
  else
936
1365
  # ignore when line not empty
937
1366
  end
1367
+
1368
+ when "\u0001" # Ctrl-A - move to beginning of line
1369
+ cursor = 0
1370
+ last_tab_prefix = nil
1371
+ tab_cycle = 0
1372
+
938
1373
  when "\u007F", "\b" # Backspace
939
1374
  if cursor > 0
940
1375
  buffer.slice!(cursor - 1)
@@ -942,12 +1377,12 @@ def read_line_with_ghost(prompt_str)
942
1377
  end
943
1378
  last_tab_prefix = nil
944
1379
  tab_cycle = 0
1380
+
945
1381
  when "\t" # Tab completion
946
1382
  buffer, cursor, last_tab_prefix, tab_cycle, printed =
947
1383
  handle_tab_completion(prompt_str, buffer, cursor, last_tab_prefix, tab_cycle)
948
- # After showing completion list, reset render rows so the next prompt
949
- # redraw only clears the current input line, not the completion block.
950
1384
  $last_render_rows = 1 if printed
1385
+
951
1386
  when "\e" # Escape sequences (arrows, home/end)
952
1387
  seq1 = io.getch
953
1388
  seq2 = io.getch
@@ -992,6 +1427,7 @@ def read_line_with_ghost(prompt_str)
992
1427
  end
993
1428
  last_tab_prefix = nil
994
1429
  tab_cycle = 0
1430
+
995
1431
  else
996
1432
  if ch.ord >= 32 && ch.ord != 127
997
1433
  buffer.insert(cursor, ch)
@@ -1022,6 +1458,17 @@ def print_welcome
1022
1458
  puts
1023
1459
  end
1024
1460
 
1461
+ # ---------------- Script vs interactive entry ----------------
1462
+ if ARGV[0]
1463
+ script_path = ARGV.shift
1464
+ begin
1465
+ rsh_run_script(script_path, ARGV)
1466
+ rescue => e
1467
+ STDERR.puts "rsh script error: #{e.class}: #{e.message}"
1468
+ end
1469
+ exit 0
1470
+ end
1471
+
1025
1472
  print_welcome
1026
1473
 
1027
1474
  # ---------------- 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.2"
3
+ VERSION = "0.7.1"
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.2
4
+ version: 0.7.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - RobertFlexx