srsh 0.7.1 → 0.8.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/LICENSE +9 -14
- data/README.md +8 -229
- data/exe/srsh +6 -0
- data/lib/srsh/runner.rb +2416 -0
- data/lib/srsh.rb +9 -0
- metadata +13 -12
- data/bin/srsh +0 -1491
- data/lib/srsh/version.rb +0 -4
data/bin/srsh
DELETED
|
@@ -1,1491 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env ruby
|
|
2
|
-
require 'shellwords'
|
|
3
|
-
require 'socket'
|
|
4
|
-
require 'time'
|
|
5
|
-
require 'etc'
|
|
6
|
-
require 'rbconfig'
|
|
7
|
-
require 'io/console'
|
|
8
|
-
|
|
9
|
-
# ---------------- Version ----------------
|
|
10
|
-
SRSH_VERSION = "0.7.1"
|
|
11
|
-
|
|
12
|
-
$0 = "srsh-#{SRSH_VERSION}"
|
|
13
|
-
ENV['SHELL'] = "srsh-#{SRSH_VERSION}"
|
|
14
|
-
print "\033]0;srsh-#{SRSH_VERSION}\007"
|
|
15
|
-
|
|
16
|
-
Dir.chdir(ENV['HOME']) if ENV['HOME']
|
|
17
|
-
|
|
18
|
-
# ---------------- Globals ----------------
|
|
19
|
-
$child_pids = []
|
|
20
|
-
$aliases = {}
|
|
21
|
-
$last_render_rows = 0
|
|
22
|
-
|
|
23
|
-
$last_status = 0
|
|
24
|
-
$rsh_functions = {}
|
|
25
|
-
$rsh_positional = {}
|
|
26
|
-
$rsh_script_mode = false
|
|
27
|
-
|
|
28
|
-
Signal.trap("INT", "IGNORE")
|
|
29
|
-
|
|
30
|
-
# Control-flow exceptions for the scripting engine
|
|
31
|
-
class RshBreak < StandardError; end
|
|
32
|
-
class RshContinue < StandardError; end
|
|
33
|
-
class RshReturn < StandardError; end
|
|
34
|
-
|
|
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
|
|
50
|
-
end
|
|
51
|
-
|
|
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)
|
|
57
|
-
# ~/.srshrc — srsh configuration
|
|
58
|
-
# This file was created automatically by srsh v#{SRSH_VERSION}.
|
|
59
|
-
# You can keep personal notes or planned settings here.
|
|
60
|
-
# (Currently not sourced by srsh runtime.)
|
|
61
|
-
RC
|
|
62
|
-
end
|
|
63
|
-
rescue
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
# ---------------- Utilities ----------------
|
|
67
|
-
def color(text, code)
|
|
68
|
-
"\e[#{code}m#{text}\e[0m"
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
def random_color
|
|
72
|
-
[31, 32, 33, 34, 35, 36, 37].sample
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
def rainbow_codes
|
|
76
|
-
[31, 33, 32, 36, 34, 35, 91, 93, 92, 96, 94, 95]
|
|
77
|
-
end
|
|
78
|
-
|
|
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
|
|
89
|
-
|
|
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
|
|
97
|
-
|
|
98
|
-
def terminal_width
|
|
99
|
-
IO.console.winsize[1]
|
|
100
|
-
rescue
|
|
101
|
-
80
|
|
102
|
-
end
|
|
103
|
-
|
|
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 = []
|
|
288
|
-
File.foreach('/proc/cpuinfo') do |line|
|
|
289
|
-
cores += 1 if line =~ /^processor\s*:\s*\d+/
|
|
290
|
-
if line =~ /^cpu MHz\s*:\s*([\d.]+)/
|
|
291
|
-
freqs << $1.to_f
|
|
292
|
-
end
|
|
293
|
-
end
|
|
294
|
-
[cores, freqs.first(cores)]
|
|
295
|
-
end
|
|
296
|
-
|
|
297
|
-
def cpu_info
|
|
298
|
-
usage = 0.0
|
|
299
|
-
cores = 0
|
|
300
|
-
freq_display = "N/A"
|
|
301
|
-
|
|
302
|
-
case os_type
|
|
303
|
-
when :linux
|
|
304
|
-
prev = read_cpu_times
|
|
305
|
-
sleep 0.05
|
|
306
|
-
current = read_cpu_times
|
|
307
|
-
usage = calculate_cpu_usage(prev, current).round(1)
|
|
308
|
-
cores, freqs = cpu_cores_and_freq
|
|
309
|
-
freq_display = freqs.empty? ? "N/A" : freqs.map { |f| "#{f.round(0)}MHz" }.join(', ')
|
|
310
|
-
else
|
|
311
|
-
cores = begin
|
|
312
|
-
`sysctl -n hw.ncpu 2>/dev/null`.to_i
|
|
313
|
-
rescue
|
|
314
|
-
0
|
|
315
|
-
end
|
|
316
|
-
|
|
317
|
-
raw_freq_hz = begin
|
|
318
|
-
`sysctl -n hw.cpufrequency 2>/dev/null`.to_i
|
|
319
|
-
rescue
|
|
320
|
-
0
|
|
321
|
-
end
|
|
322
|
-
|
|
323
|
-
freq_display =
|
|
324
|
-
if raw_freq_hz > 0
|
|
325
|
-
mhz = (raw_freq_hz.to_f / 1_000_000.0).round(0)
|
|
326
|
-
"#{mhz.to_i}MHz"
|
|
327
|
-
else
|
|
328
|
-
"N/A"
|
|
329
|
-
end
|
|
330
|
-
|
|
331
|
-
usage = begin
|
|
332
|
-
ps_output = `ps -A -o %cpu 2>/dev/null`
|
|
333
|
-
lines = ps_output.lines
|
|
334
|
-
values = lines[1..-1] || []
|
|
335
|
-
sum = values.map { |l| l.to_f }.inject(0.0, :+)
|
|
336
|
-
if cores > 0
|
|
337
|
-
(sum / cores).round(1)
|
|
338
|
-
else
|
|
339
|
-
sum.round(1)
|
|
340
|
-
end
|
|
341
|
-
rescue
|
|
342
|
-
0.0
|
|
343
|
-
end
|
|
344
|
-
end
|
|
345
|
-
|
|
346
|
-
"#{color("CPU Usage:",36)} #{color("#{usage}%",33)} | " \
|
|
347
|
-
"#{color("Cores:",36)} #{color(cores.to_s,32)} | " \
|
|
348
|
-
"#{color("Freqs:",36)} #{color(freq_display,35)}"
|
|
349
|
-
end
|
|
350
|
-
|
|
351
|
-
def ram_info
|
|
352
|
-
case os_type
|
|
353
|
-
when :linux
|
|
354
|
-
if File.exist?('/proc/meminfo')
|
|
355
|
-
meminfo = {}
|
|
356
|
-
File.read('/proc/meminfo').each_line do |line|
|
|
357
|
-
key, val = line.split(':')
|
|
358
|
-
meminfo[key.strip] = val.strip.split.first.to_i * 1024 if key && val
|
|
359
|
-
end
|
|
360
|
-
total = meminfo['MemTotal'] || 0
|
|
361
|
-
free = (meminfo['MemFree'] || 0) + (meminfo['Buffers'] || 0) + (meminfo['Cached'] || 0)
|
|
362
|
-
used = total - free
|
|
363
|
-
"#{color("RAM Usage:",36)} #{color(human_bytes(used),33)} / #{color(human_bytes(total),32)}"
|
|
364
|
-
else
|
|
365
|
-
"#{color("RAM Usage:",36)} Info not available"
|
|
366
|
-
end
|
|
367
|
-
else
|
|
368
|
-
begin
|
|
369
|
-
if os_type == :mac
|
|
370
|
-
total = `sysctl -n hw.memsize 2>/dev/null`.to_i
|
|
371
|
-
return "#{color("RAM Usage:",36)} Info not available" if total <= 0
|
|
372
|
-
|
|
373
|
-
vm = `vm_stat 2>/dev/null`
|
|
374
|
-
page_size = vm[/page size of (\d+) bytes/, 1].to_i
|
|
375
|
-
page_size = 4096 if page_size <= 0
|
|
376
|
-
|
|
377
|
-
stats = {}
|
|
378
|
-
vm.each_line do |line|
|
|
379
|
-
if line =~ /^(.+):\s+(\d+)\./
|
|
380
|
-
stats[$1] = $2.to_i
|
|
381
|
-
end
|
|
382
|
-
end
|
|
383
|
-
|
|
384
|
-
used_pages = 0
|
|
385
|
-
%w[Pages active Pages wired down Pages occupied by compressor].each do |k|
|
|
386
|
-
used_pages += stats[k].to_i
|
|
387
|
-
end
|
|
388
|
-
used = used_pages * page_size
|
|
389
|
-
|
|
390
|
-
"#{color("RAM Usage:",36)} #{color(human_bytes(used),33)} / #{color(human_bytes(total),32)}"
|
|
391
|
-
else
|
|
392
|
-
total = `sysctl -n hw.physmem 2>/dev/null`.to_i
|
|
393
|
-
total = `sysctl -n hw.realmem 2>/dev/null`.to_i if total <= 0
|
|
394
|
-
return "#{color("RAM Usage:",36)} Info not available" if total <= 0
|
|
395
|
-
"#{color("RAM Usage:",36)} #{color("Unknown",33)} / #{color(human_bytes(total),32)}"
|
|
396
|
-
end
|
|
397
|
-
rescue
|
|
398
|
-
"#{color("RAM Usage:",36)} Info not available"
|
|
399
|
-
end
|
|
400
|
-
end
|
|
401
|
-
end
|
|
402
|
-
|
|
403
|
-
def storage_info
|
|
404
|
-
begin
|
|
405
|
-
require 'sys/filesystem'
|
|
406
|
-
stat = Sys::Filesystem.stat(Dir.pwd)
|
|
407
|
-
total = stat.bytes_total
|
|
408
|
-
free = stat.bytes_available
|
|
409
|
-
used = total - free
|
|
410
|
-
"#{color("Storage Usage (#{Dir.pwd}):",36)} #{color(human_bytes(used),33)} / #{color(human_bytes(total),32)}"
|
|
411
|
-
rescue LoadError
|
|
412
|
-
"#{color("Install 'sys-filesystem' gem for storage info:",31)} #{color('gem install sys-filesystem',33)}"
|
|
413
|
-
rescue
|
|
414
|
-
"#{color("Storage Usage:",36)} Info not available"
|
|
415
|
-
end
|
|
416
|
-
end
|
|
417
|
-
|
|
418
|
-
# ---------------- Builtin helpers ----------------
|
|
419
|
-
def builtin_help
|
|
420
|
-
puts color('=' * 60, "1;35")
|
|
421
|
-
puts color("srsh #{SRSH_VERSION} - Builtin Commands", "1;33")
|
|
422
|
-
puts color(sprintf("%-15s%-45s", "Command", "Description"), "1;36")
|
|
423
|
-
puts color('-' * 60, "1;34")
|
|
424
|
-
puts color(sprintf("%-15s", "cd"), "1;36") + "Change directory"
|
|
425
|
-
puts color(sprintf("%-15s", "pwd"), "1;36") + "Print working directory"
|
|
426
|
-
puts color(sprintf("%-15s", "exit / quit"), "1;36") + "Exit the shell"
|
|
427
|
-
puts color(sprintf("%-15s", "alias"), "1;36") + "Create or list aliases"
|
|
428
|
-
puts color(sprintf("%-15s", "unalias"), "1;36") + "Remove alias"
|
|
429
|
-
puts color(sprintf("%-15s", "jobs"), "1;36") + "Show background jobs (tracked pids)"
|
|
430
|
-
puts color(sprintf("%-15s", "systemfetch"), "1;36") + "Display system information"
|
|
431
|
-
puts color(sprintf("%-15s", "hist"), "1;36") + "Show shell history"
|
|
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"
|
|
442
|
-
puts color(sprintf("%-15s", "help"), "1;36") + "Show this help message"
|
|
443
|
-
puts color('=' * 60, "1;35")
|
|
444
|
-
end
|
|
445
|
-
|
|
446
|
-
def builtin_systemfetch
|
|
447
|
-
user = ENV['USER'] || Etc.getlogin || Etc.getpwuid.name rescue ENV['USER'] || Etc.getlogin
|
|
448
|
-
host = Socket.gethostname
|
|
449
|
-
os = detect_distro
|
|
450
|
-
ruby_ver = RUBY_VERSION
|
|
451
|
-
|
|
452
|
-
cpu_percent = begin
|
|
453
|
-
case os_type
|
|
454
|
-
when :linux
|
|
455
|
-
prev = read_cpu_times
|
|
456
|
-
sleep 0.05
|
|
457
|
-
cur = read_cpu_times
|
|
458
|
-
calculate_cpu_usage(prev, cur).round(1)
|
|
459
|
-
else
|
|
460
|
-
cores = `sysctl -n hw.ncpu 2>/dev/null`.to_i rescue 0
|
|
461
|
-
ps_output = `ps -A -o %cpu 2>/dev/null`
|
|
462
|
-
lines = ps_output.lines
|
|
463
|
-
values = lines[1..-1] || []
|
|
464
|
-
sum = values.map { |l| l.to_f }.inject(0.0, :+)
|
|
465
|
-
if cores > 0
|
|
466
|
-
(sum / cores).round(1)
|
|
467
|
-
else
|
|
468
|
-
sum.round(1)
|
|
469
|
-
end
|
|
470
|
-
end
|
|
471
|
-
rescue
|
|
472
|
-
0.0
|
|
473
|
-
end
|
|
474
|
-
|
|
475
|
-
mem_percent = begin
|
|
476
|
-
case os_type
|
|
477
|
-
when :linux
|
|
478
|
-
if File.exist?('/proc/meminfo')
|
|
479
|
-
meminfo = {}
|
|
480
|
-
File.read('/proc/meminfo').each_line do |line|
|
|
481
|
-
k, v = line.split(':')
|
|
482
|
-
meminfo[k.strip] = v.strip.split.first.to_i * 1024 if k && v
|
|
483
|
-
end
|
|
484
|
-
total = meminfo['MemTotal'] || 1
|
|
485
|
-
free = (meminfo['MemAvailable'] || meminfo['MemFree'] || 0)
|
|
486
|
-
used = total - free
|
|
487
|
-
(used.to_f / total.to_f * 100).round(1)
|
|
488
|
-
else
|
|
489
|
-
0.0
|
|
490
|
-
end
|
|
491
|
-
when :mac
|
|
492
|
-
total = `sysctl -n hw.memsize 2>/dev/null`.to_i
|
|
493
|
-
if total <= 0
|
|
494
|
-
0.0
|
|
495
|
-
else
|
|
496
|
-
vm = `vm_stat 2>/dev/null`
|
|
497
|
-
page_size = vm[/page size of (\d+) bytes/, 1].to_i
|
|
498
|
-
page_size = 4096 if page_size <= 0
|
|
499
|
-
|
|
500
|
-
stats = {}
|
|
501
|
-
vm.each_line do |line|
|
|
502
|
-
if line =~ /^(.+):\s+(\d+)\./
|
|
503
|
-
stats[$1] = $2.to_i
|
|
504
|
-
end
|
|
505
|
-
end
|
|
506
|
-
|
|
507
|
-
used_pages = 0
|
|
508
|
-
%w[Pages active Pages wired down Pages occupied by compressor].each do |k|
|
|
509
|
-
used_pages += stats[k].to_i
|
|
510
|
-
end
|
|
511
|
-
used = used_pages * page_size
|
|
512
|
-
((used.to_f / total.to_f) * 100).round(1)
|
|
513
|
-
end
|
|
514
|
-
else
|
|
515
|
-
0.0
|
|
516
|
-
end
|
|
517
|
-
rescue
|
|
518
|
-
0.0
|
|
519
|
-
end
|
|
520
|
-
|
|
521
|
-
puts color('=' * 60, "1;35")
|
|
522
|
-
puts color("srsh System Information", "1;33")
|
|
523
|
-
puts color("User: ", "1;36") + color("#{user}@#{host}", "0;37")
|
|
524
|
-
puts color("OS: ", "1;36") + color(os, "0;37")
|
|
525
|
-
puts color("Shell: ", "1;36") + color("srsh v#{SRSH_VERSION}", "0;37")
|
|
526
|
-
puts color("Ruby: ", "1;36") + color(ruby_ver, "0;37")
|
|
527
|
-
puts color("CPU Usage: ", "1;36") + nice_bar(cpu_percent / 100.0, 30, 32)
|
|
528
|
-
puts color("RAM Usage: ", "1;36") + nice_bar(mem_percent / 100.0, 30, 35)
|
|
529
|
-
puts color('=' * 60, "1;35")
|
|
530
|
-
end
|
|
531
|
-
|
|
532
|
-
def builtin_jobs
|
|
533
|
-
if $child_pids.empty?
|
|
534
|
-
puts color("No tracked child jobs.", 36)
|
|
535
|
-
return
|
|
536
|
-
end
|
|
537
|
-
$child_pids.each do |pid|
|
|
538
|
-
status = begin
|
|
539
|
-
Process.kill(0, pid)
|
|
540
|
-
'running'
|
|
541
|
-
rescue Errno::ESRCH
|
|
542
|
-
'done'
|
|
543
|
-
rescue Errno::EPERM
|
|
544
|
-
'running'
|
|
545
|
-
end
|
|
546
|
-
puts "[#{pid}] #{status}"
|
|
547
|
-
end
|
|
548
|
-
end
|
|
549
|
-
|
|
550
|
-
def builtin_hist
|
|
551
|
-
HISTORY.each_with_index do |h, i|
|
|
552
|
-
printf "%5d %s\n", i + 1, h
|
|
553
|
-
end
|
|
554
|
-
end
|
|
555
|
-
|
|
556
|
-
def builtin_clearhist
|
|
557
|
-
HISTORY.clear
|
|
558
|
-
if File.exist?(HISTORY_FILE)
|
|
559
|
-
begin
|
|
560
|
-
File.delete(HISTORY_FILE)
|
|
561
|
-
rescue
|
|
562
|
-
end
|
|
563
|
-
end
|
|
564
|
-
puts color("History cleared (memory + file).", 32)
|
|
565
|
-
end
|
|
566
|
-
|
|
567
|
-
# -------- Pretty column printer for colored text (used by ls) --------
|
|
568
|
-
def print_columns_colored(labels)
|
|
569
|
-
return if labels.nil? || labels.empty?
|
|
570
|
-
|
|
571
|
-
width = terminal_width
|
|
572
|
-
visible_lengths = labels.map { |s| strip_ansi(s).length }
|
|
573
|
-
max_len = visible_lengths.max || 0
|
|
574
|
-
col_width = [max_len + 2, 4].max
|
|
575
|
-
cols = [width / col_width, 1].max
|
|
576
|
-
rows = (labels.length.to_f / cols).ceil
|
|
577
|
-
|
|
578
|
-
rows.times do |r|
|
|
579
|
-
line = ""
|
|
580
|
-
cols.times do |c|
|
|
581
|
-
idx = c * rows + r
|
|
582
|
-
break if idx >= labels.length
|
|
583
|
-
label = labels[idx]
|
|
584
|
-
visible = strip_ansi(label).length
|
|
585
|
-
padding = col_width - visible
|
|
586
|
-
line << label << (" " * padding)
|
|
587
|
-
end
|
|
588
|
-
STDOUT.print("\r")
|
|
589
|
-
STDOUT.print(line.rstrip)
|
|
590
|
-
STDOUT.print("\n")
|
|
591
|
-
end
|
|
592
|
-
end
|
|
593
|
-
|
|
594
|
-
def builtin_ls(path = ".")
|
|
595
|
-
begin
|
|
596
|
-
entries = Dir.children(path).sort
|
|
597
|
-
rescue => e
|
|
598
|
-
puts color("ls: #{e.message}", 31)
|
|
599
|
-
return
|
|
600
|
-
end
|
|
601
|
-
|
|
602
|
-
labels = entries.map do |name|
|
|
603
|
-
full = File.join(path, name)
|
|
604
|
-
begin
|
|
605
|
-
if File.directory?(full)
|
|
606
|
-
color("#{name}/", 36)
|
|
607
|
-
elsif File.executable?(full)
|
|
608
|
-
color("#{name}*", 32)
|
|
609
|
-
else
|
|
610
|
-
color(name, 37)
|
|
611
|
-
end
|
|
612
|
-
rescue
|
|
613
|
-
name
|
|
614
|
-
end
|
|
615
|
-
end
|
|
616
|
-
|
|
617
|
-
print_columns_colored(labels)
|
|
618
|
-
end
|
|
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
|
-
|
|
801
|
-
# ---------------- External Execution Helper ----------------
|
|
802
|
-
def exec_external(args, stdin_file, stdout_file, append)
|
|
803
|
-
command_path = args[0]
|
|
804
|
-
if command_path && (command_path.include?('/') || command_path.start_with?('.'))
|
|
805
|
-
begin
|
|
806
|
-
if File.directory?(command_path)
|
|
807
|
-
puts color("srsh: #{command_path}: is a directory", 31)
|
|
808
|
-
return
|
|
809
|
-
end
|
|
810
|
-
rescue
|
|
811
|
-
end
|
|
812
|
-
end
|
|
813
|
-
|
|
814
|
-
pid = fork do
|
|
815
|
-
Signal.trap("INT","DEFAULT")
|
|
816
|
-
if stdin_file
|
|
817
|
-
begin
|
|
818
|
-
STDIN.reopen(File.open(stdin_file,'r'))
|
|
819
|
-
rescue
|
|
820
|
-
end
|
|
821
|
-
end
|
|
822
|
-
if stdout_file
|
|
823
|
-
begin
|
|
824
|
-
STDOUT.reopen(File.open(stdout_file, append ? 'a' : 'w'))
|
|
825
|
-
rescue
|
|
826
|
-
end
|
|
827
|
-
end
|
|
828
|
-
begin
|
|
829
|
-
exec(*args)
|
|
830
|
-
rescue Errno::ENOENT
|
|
831
|
-
puts color("Command not found: #{args[0]}", rainbow_codes.sample)
|
|
832
|
-
exit 127
|
|
833
|
-
rescue Errno::EACCES
|
|
834
|
-
puts color("Permission denied: #{args[0]}", 31)
|
|
835
|
-
exit 126
|
|
836
|
-
end
|
|
837
|
-
end
|
|
838
|
-
|
|
839
|
-
$child_pids << pid
|
|
840
|
-
begin
|
|
841
|
-
Process.wait(pid)
|
|
842
|
-
$last_status = $?.exitstatus || 0
|
|
843
|
-
rescue Interrupt
|
|
844
|
-
ensure
|
|
845
|
-
$child_pids.delete(pid)
|
|
846
|
-
end
|
|
847
|
-
end
|
|
848
|
-
|
|
849
|
-
# ---------------- Command Execution ----------------
|
|
850
|
-
def run_command(cmd)
|
|
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 ----------------
|
|
882
|
-
cmd, stdin_file, stdout_file, append = parse_redirection(cmd)
|
|
883
|
-
args = Shellwords.shellsplit(cmd) rescue []
|
|
884
|
-
return if args.empty?
|
|
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
|
-
|
|
893
|
-
case args[0]
|
|
894
|
-
when 'ls'
|
|
895
|
-
if args.length == 1
|
|
896
|
-
builtin_ls(".")
|
|
897
|
-
elsif args.length == 2 && !args[1].start_with?("-")
|
|
898
|
-
builtin_ls(args[1])
|
|
899
|
-
else
|
|
900
|
-
exec_external(args, stdin_file, stdout_file, append)
|
|
901
|
-
return
|
|
902
|
-
end
|
|
903
|
-
$last_status = 0
|
|
904
|
-
return
|
|
905
|
-
|
|
906
|
-
when 'cd'
|
|
907
|
-
path = args[1] ? File.expand_path(args[1]) : ENV['HOME']
|
|
908
|
-
if !File.exist?(path)
|
|
909
|
-
puts color("cd: no such file or directory: #{args[1]}", 31)
|
|
910
|
-
$last_status = 1
|
|
911
|
-
elsif !File.directory?(path)
|
|
912
|
-
puts color("cd: not a directory: #{args[1]}", 31)
|
|
913
|
-
$last_status = 1
|
|
914
|
-
else
|
|
915
|
-
Dir.chdir(path)
|
|
916
|
-
$last_status = 0
|
|
917
|
-
end
|
|
918
|
-
return
|
|
919
|
-
|
|
920
|
-
when 'exit','quit'
|
|
921
|
-
$child_pids.each { |pid| Process.kill("TERM", pid) rescue nil }
|
|
922
|
-
exit 0
|
|
923
|
-
|
|
924
|
-
when 'alias'
|
|
925
|
-
if args[1].nil?
|
|
926
|
-
$aliases.each { |k,v| puts "#{k}='#{v}'" }
|
|
927
|
-
else
|
|
928
|
-
arg = args[1..].join(' ')
|
|
929
|
-
if arg =~ /^(\w+)=([\"']?)(.+?)\2$/
|
|
930
|
-
$aliases[$1] = $3
|
|
931
|
-
else
|
|
932
|
-
puts color("Invalid alias format", 31)
|
|
933
|
-
end
|
|
934
|
-
end
|
|
935
|
-
$last_status = 0
|
|
936
|
-
return
|
|
937
|
-
|
|
938
|
-
when 'unalias'
|
|
939
|
-
if args[1]
|
|
940
|
-
$aliases.delete(args[1])
|
|
941
|
-
$last_status = 0
|
|
942
|
-
else
|
|
943
|
-
puts color("unalias: usage: unalias name", 31)
|
|
944
|
-
$last_status = 1
|
|
945
|
-
end
|
|
946
|
-
return
|
|
947
|
-
|
|
948
|
-
when 'help'
|
|
949
|
-
builtin_help
|
|
950
|
-
$last_status = 0
|
|
951
|
-
return
|
|
952
|
-
|
|
953
|
-
when 'systemfetch'
|
|
954
|
-
builtin_systemfetch
|
|
955
|
-
$last_status = 0
|
|
956
|
-
return
|
|
957
|
-
|
|
958
|
-
when 'jobs'
|
|
959
|
-
builtin_jobs
|
|
960
|
-
$last_status = 0
|
|
961
|
-
return
|
|
962
|
-
|
|
963
|
-
when 'pwd'
|
|
964
|
-
puts color(Dir.pwd, 36)
|
|
965
|
-
$last_status = 0
|
|
966
|
-
return
|
|
967
|
-
|
|
968
|
-
when 'hist'
|
|
969
|
-
builtin_hist
|
|
970
|
-
$last_status = 0
|
|
971
|
-
return
|
|
972
|
-
|
|
973
|
-
when 'clearhist'
|
|
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
|
|
1061
|
-
return
|
|
1062
|
-
end
|
|
1063
|
-
|
|
1064
|
-
# Fallback to external command
|
|
1065
|
-
exec_external(args, stdin_file, stdout_file, append)
|
|
1066
|
-
end
|
|
1067
|
-
|
|
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
|
-
|
|
1115
|
-
def run_input_line(input)
|
|
1116
|
-
split_commands(input).each do |cmd|
|
|
1117
|
-
run_command(cmd)
|
|
1118
|
-
end
|
|
1119
|
-
end
|
|
1120
|
-
|
|
1121
|
-
# ---------------- Prompt ----------------
|
|
1122
|
-
hostname = Socket.gethostname
|
|
1123
|
-
prompt_color = random_color
|
|
1124
|
-
|
|
1125
|
-
def prompt(hostname, prompt_color)
|
|
1126
|
-
"#{color(Dir.pwd,33)} #{color(hostname,36)}#{color(' > ', prompt_color)}"
|
|
1127
|
-
end
|
|
1128
|
-
|
|
1129
|
-
# ---------------- Ghost + Completion Helpers ----------------
|
|
1130
|
-
def history_ghost_for(line)
|
|
1131
|
-
return nil if line.nil? || line.empty?
|
|
1132
|
-
HISTORY.reverse_each do |h|
|
|
1133
|
-
next if h.nil? || h.empty?
|
|
1134
|
-
next if h.start_with?("[completions:")
|
|
1135
|
-
next unless h.start_with?(line)
|
|
1136
|
-
next if h == line
|
|
1137
|
-
return h
|
|
1138
|
-
end
|
|
1139
|
-
nil
|
|
1140
|
-
end
|
|
1141
|
-
|
|
1142
|
-
def tab_completions_for(prefix, first_word, at_first_word)
|
|
1143
|
-
prefix ||= ""
|
|
1144
|
-
|
|
1145
|
-
dir = "."
|
|
1146
|
-
base = prefix
|
|
1147
|
-
|
|
1148
|
-
if prefix.include?('/')
|
|
1149
|
-
if prefix.end_with?('/')
|
|
1150
|
-
dir = prefix.chomp('/')
|
|
1151
|
-
base = ""
|
|
1152
|
-
else
|
|
1153
|
-
dir = File.dirname(prefix)
|
|
1154
|
-
base = File.basename(prefix)
|
|
1155
|
-
end
|
|
1156
|
-
dir = "." if dir.nil? || dir.empty?
|
|
1157
|
-
end
|
|
1158
|
-
|
|
1159
|
-
file_completions = []
|
|
1160
|
-
if Dir.exist?(dir)
|
|
1161
|
-
Dir.children(dir).each do |entry|
|
|
1162
|
-
next unless entry.start_with?(base)
|
|
1163
|
-
full = File.join(dir, entry)
|
|
1164
|
-
|
|
1165
|
-
rel =
|
|
1166
|
-
if dir == "."
|
|
1167
|
-
entry
|
|
1168
|
-
else
|
|
1169
|
-
File.join(File.dirname(prefix), entry)
|
|
1170
|
-
end
|
|
1171
|
-
|
|
1172
|
-
case first_word
|
|
1173
|
-
when "cd"
|
|
1174
|
-
next unless File.directory?(full)
|
|
1175
|
-
rel = rel + "/" unless rel.end_with?("/")
|
|
1176
|
-
file_completions << rel
|
|
1177
|
-
when "cat"
|
|
1178
|
-
next unless File.file?(full)
|
|
1179
|
-
file_completions << rel
|
|
1180
|
-
else
|
|
1181
|
-
rel = rel + "/" if File.directory?(full) && !rel.end_with?("/")
|
|
1182
|
-
file_completions << rel
|
|
1183
|
-
end
|
|
1184
|
-
end
|
|
1185
|
-
end
|
|
1186
|
-
|
|
1187
|
-
exec_completions = []
|
|
1188
|
-
if first_word != "cat" && first_word != "cd" && at_first_word && !prefix.include?('/')
|
|
1189
|
-
path_entries = (ENV['PATH'] || "").split(':')
|
|
1190
|
-
execs = path_entries.flat_map do |p|
|
|
1191
|
-
Dir.glob("#{p}/*").map { |f|
|
|
1192
|
-
File.basename(f) if File.executable?(f) && !File.directory?(f)
|
|
1193
|
-
}.compact rescue []
|
|
1194
|
-
end
|
|
1195
|
-
exec_completions = execs.grep(/^#{Regexp.escape(prefix)}/)
|
|
1196
|
-
end
|
|
1197
|
-
|
|
1198
|
-
(file_completions + exec_completions).uniq
|
|
1199
|
-
end
|
|
1200
|
-
|
|
1201
|
-
def longest_common_prefix(strings)
|
|
1202
|
-
return "" if strings.empty?
|
|
1203
|
-
shortest = strings.min_by(&:length)
|
|
1204
|
-
shortest.length.times do |i|
|
|
1205
|
-
c = shortest[i]
|
|
1206
|
-
strings.each do |s|
|
|
1207
|
-
return shortest[0...i] if s[i] != c
|
|
1208
|
-
end
|
|
1209
|
-
end
|
|
1210
|
-
shortest
|
|
1211
|
-
end
|
|
1212
|
-
|
|
1213
|
-
def render_line(prompt_str, buffer, cursor, show_ghost = true)
|
|
1214
|
-
buffer = buffer || ""
|
|
1215
|
-
cursor = [[cursor, 0].max, buffer.length].min
|
|
1216
|
-
|
|
1217
|
-
ghost_tail = ""
|
|
1218
|
-
if show_ghost && cursor == buffer.length
|
|
1219
|
-
suggestion = history_ghost_for(buffer)
|
|
1220
|
-
ghost_tail = suggestion ? suggestion[buffer.length..-1].to_s : ""
|
|
1221
|
-
end
|
|
1222
|
-
|
|
1223
|
-
width = terminal_width
|
|
1224
|
-
prompt_vis = strip_ansi(prompt_str).length
|
|
1225
|
-
total_vis = prompt_vis + buffer.length + ghost_tail.length
|
|
1226
|
-
rows = [(total_vis.to_f / width).ceil, 1].max
|
|
1227
|
-
|
|
1228
|
-
# Clear previous render block (only what we drew last time)
|
|
1229
|
-
if $last_render_rows && $last_render_rows > 0
|
|
1230
|
-
STDOUT.print("\r")
|
|
1231
|
-
($last_render_rows - 1).times do
|
|
1232
|
-
STDOUT.print("\e[1A\r") # move up a line, to column 0
|
|
1233
|
-
end
|
|
1234
|
-
$last_render_rows.times do |i|
|
|
1235
|
-
STDOUT.print("\e[0K") # clear this line
|
|
1236
|
-
STDOUT.print("\n") if i < $last_render_rows - 1
|
|
1237
|
-
end
|
|
1238
|
-
($last_render_rows - 1).times do
|
|
1239
|
-
STDOUT.print("\e[1A\r") # move back up to first line of block
|
|
1240
|
-
end
|
|
1241
|
-
end
|
|
1242
|
-
|
|
1243
|
-
STDOUT.print("\r")
|
|
1244
|
-
STDOUT.print(prompt_str)
|
|
1245
|
-
STDOUT.print(buffer)
|
|
1246
|
-
STDOUT.print(color(ghost_tail, "2")) unless ghost_tail.empty?
|
|
1247
|
-
|
|
1248
|
-
move_left = ghost_tail.length + (buffer.length - cursor)
|
|
1249
|
-
STDOUT.print("\e[#{move_left}D") if move_left > 0
|
|
1250
|
-
STDOUT.flush
|
|
1251
|
-
|
|
1252
|
-
$last_render_rows = rows
|
|
1253
|
-
end
|
|
1254
|
-
|
|
1255
|
-
# --------- NEAT MULTI-COLUMN TAB LIST (bash-style) ----------
|
|
1256
|
-
def print_tab_list(comps)
|
|
1257
|
-
return if comps.empty?
|
|
1258
|
-
|
|
1259
|
-
width = terminal_width
|
|
1260
|
-
max_len = comps.map { |s| s.length }.max || 0
|
|
1261
|
-
col_width = [max_len + 2, 4].max
|
|
1262
|
-
cols = [width / col_width, 1].max
|
|
1263
|
-
rows = (comps.length.to_f / cols).ceil
|
|
1264
|
-
|
|
1265
|
-
STDOUT.print("\r\n")
|
|
1266
|
-
rows.times do |r|
|
|
1267
|
-
line = ""
|
|
1268
|
-
cols.times do |c|
|
|
1269
|
-
idx = c * rows + r
|
|
1270
|
-
break if idx >= comps.length
|
|
1271
|
-
item = comps[idx]
|
|
1272
|
-
padding = col_width - item.length
|
|
1273
|
-
line << item << (" " * padding)
|
|
1274
|
-
end
|
|
1275
|
-
STDOUT.print("\r")
|
|
1276
|
-
STDOUT.print(line.rstrip)
|
|
1277
|
-
STDOUT.print("\n")
|
|
1278
|
-
end
|
|
1279
|
-
STDOUT.print("\r\n")
|
|
1280
|
-
STDOUT.flush
|
|
1281
|
-
end
|
|
1282
|
-
|
|
1283
|
-
def handle_tab_completion(prompt_str, buffer, cursor, last_tab_prefix, tab_cycle)
|
|
1284
|
-
buffer = buffer || ""
|
|
1285
|
-
cursor = [[cursor, 0].max, buffer.length].min
|
|
1286
|
-
|
|
1287
|
-
wstart = buffer.rindex(/[ \t]/, cursor - 1) || -1
|
|
1288
|
-
wstart += 1
|
|
1289
|
-
prefix = buffer[wstart...cursor] || ""
|
|
1290
|
-
|
|
1291
|
-
before_word = buffer[0...wstart]
|
|
1292
|
-
at_first_word = before_word.strip.empty?
|
|
1293
|
-
first_word = buffer.strip.split(/\s+/, 2)[0] || ""
|
|
1294
|
-
|
|
1295
|
-
comps = tab_completions_for(prefix, first_word, at_first_word)
|
|
1296
|
-
return [buffer, cursor, nil, 0, false] if comps.empty?
|
|
1297
|
-
|
|
1298
|
-
if comps.size == 1
|
|
1299
|
-
new_word = comps.first
|
|
1300
|
-
buffer = buffer[0...wstart] + new_word + buffer[cursor..-1].to_s
|
|
1301
|
-
cursor = wstart + new_word.length
|
|
1302
|
-
return [buffer, cursor, nil, 0, true]
|
|
1303
|
-
end
|
|
1304
|
-
|
|
1305
|
-
if prefix != last_tab_prefix
|
|
1306
|
-
lcp = longest_common_prefix(comps)
|
|
1307
|
-
if lcp && lcp.length > prefix.length
|
|
1308
|
-
buffer = buffer[0...wstart] + lcp + buffer[cursor..-1].to_s
|
|
1309
|
-
cursor = wstart + lcp.length
|
|
1310
|
-
else
|
|
1311
|
-
STDOUT.print("\a")
|
|
1312
|
-
end
|
|
1313
|
-
last_tab_prefix = prefix
|
|
1314
|
-
tab_cycle = 1
|
|
1315
|
-
return [buffer, cursor, last_tab_prefix, tab_cycle, false]
|
|
1316
|
-
else
|
|
1317
|
-
# Second tab on same prefix: show list
|
|
1318
|
-
render_line(prompt_str, buffer, cursor, false)
|
|
1319
|
-
print_tab_list(comps)
|
|
1320
|
-
last_tab_prefix = prefix
|
|
1321
|
-
tab_cycle += 1
|
|
1322
|
-
return [buffer, cursor, last_tab_prefix, tab_cycle, true]
|
|
1323
|
-
end
|
|
1324
|
-
end
|
|
1325
|
-
|
|
1326
|
-
def read_line_with_ghost(prompt_str)
|
|
1327
|
-
buffer = ""
|
|
1328
|
-
cursor = 0
|
|
1329
|
-
hist_index = HISTORY.length
|
|
1330
|
-
saved_line_for_history = ""
|
|
1331
|
-
last_tab_prefix = nil
|
|
1332
|
-
tab_cycle = 0
|
|
1333
|
-
|
|
1334
|
-
render_line(prompt_str, buffer, cursor)
|
|
1335
|
-
|
|
1336
|
-
status = :ok
|
|
1337
|
-
|
|
1338
|
-
IO.console.raw do |io|
|
|
1339
|
-
loop do
|
|
1340
|
-
ch = io.getch
|
|
1341
|
-
|
|
1342
|
-
case ch
|
|
1343
|
-
when "\r", "\n"
|
|
1344
|
-
cursor = buffer.length
|
|
1345
|
-
render_line(prompt_str, buffer, cursor, false)
|
|
1346
|
-
STDOUT.print("\r\n")
|
|
1347
|
-
STDOUT.flush
|
|
1348
|
-
break
|
|
1349
|
-
|
|
1350
|
-
when "\u0003" # Ctrl-C
|
|
1351
|
-
STDOUT.print("^C\r\n")
|
|
1352
|
-
STDOUT.flush
|
|
1353
|
-
status = :interrupt
|
|
1354
|
-
buffer = ""
|
|
1355
|
-
break
|
|
1356
|
-
|
|
1357
|
-
when "\u0004" # Ctrl-D
|
|
1358
|
-
if buffer.empty?
|
|
1359
|
-
status = :eof
|
|
1360
|
-
buffer = nil
|
|
1361
|
-
STDOUT.print("\r\n")
|
|
1362
|
-
STDOUT.flush
|
|
1363
|
-
break
|
|
1364
|
-
else
|
|
1365
|
-
# ignore when line not empty
|
|
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
|
-
|
|
1373
|
-
when "\u007F", "\b" # Backspace
|
|
1374
|
-
if cursor > 0
|
|
1375
|
-
buffer.slice!(cursor - 1)
|
|
1376
|
-
cursor -= 1
|
|
1377
|
-
end
|
|
1378
|
-
last_tab_prefix = nil
|
|
1379
|
-
tab_cycle = 0
|
|
1380
|
-
|
|
1381
|
-
when "\t" # Tab completion
|
|
1382
|
-
buffer, cursor, last_tab_prefix, tab_cycle, printed =
|
|
1383
|
-
handle_tab_completion(prompt_str, buffer, cursor, last_tab_prefix, tab_cycle)
|
|
1384
|
-
$last_render_rows = 1 if printed
|
|
1385
|
-
|
|
1386
|
-
when "\e" # Escape sequences (arrows, home/end)
|
|
1387
|
-
seq1 = io.getch
|
|
1388
|
-
seq2 = io.getch
|
|
1389
|
-
if seq1 == "[" && seq2
|
|
1390
|
-
case seq2
|
|
1391
|
-
when "A" # Up
|
|
1392
|
-
if hist_index == HISTORY.length
|
|
1393
|
-
saved_line_for_history = buffer.dup
|
|
1394
|
-
end
|
|
1395
|
-
if hist_index > 0
|
|
1396
|
-
hist_index -= 1
|
|
1397
|
-
buffer = HISTORY[hist_index] || ""
|
|
1398
|
-
cursor = buffer.length
|
|
1399
|
-
end
|
|
1400
|
-
when "B" # Down
|
|
1401
|
-
if hist_index < HISTORY.length - 1
|
|
1402
|
-
hist_index += 1
|
|
1403
|
-
buffer = HISTORY[hist_index] || ""
|
|
1404
|
-
cursor = buffer.length
|
|
1405
|
-
elsif hist_index == HISTORY.length - 1
|
|
1406
|
-
hist_index = HISTORY.length
|
|
1407
|
-
buffer = saved_line_for_history || ""
|
|
1408
|
-
cursor = buffer.length
|
|
1409
|
-
end
|
|
1410
|
-
when "C" # Right
|
|
1411
|
-
if cursor < buffer.length
|
|
1412
|
-
cursor += 1
|
|
1413
|
-
else
|
|
1414
|
-
suggestion = history_ghost_for(buffer)
|
|
1415
|
-
if suggestion
|
|
1416
|
-
buffer = suggestion
|
|
1417
|
-
cursor = buffer.length
|
|
1418
|
-
end
|
|
1419
|
-
end
|
|
1420
|
-
when "D" # Left
|
|
1421
|
-
cursor -= 1 if cursor > 0
|
|
1422
|
-
when "H" # Home
|
|
1423
|
-
cursor = 0
|
|
1424
|
-
when "F" # End
|
|
1425
|
-
cursor = buffer.length
|
|
1426
|
-
end
|
|
1427
|
-
end
|
|
1428
|
-
last_tab_prefix = nil
|
|
1429
|
-
tab_cycle = 0
|
|
1430
|
-
|
|
1431
|
-
else
|
|
1432
|
-
if ch.ord >= 32 && ch.ord != 127
|
|
1433
|
-
buffer.insert(cursor, ch)
|
|
1434
|
-
cursor += 1
|
|
1435
|
-
hist_index = HISTORY.length
|
|
1436
|
-
last_tab_prefix = nil
|
|
1437
|
-
tab_cycle = 0
|
|
1438
|
-
end
|
|
1439
|
-
end
|
|
1440
|
-
|
|
1441
|
-
render_line(prompt_str, buffer, cursor) if status == :ok
|
|
1442
|
-
end
|
|
1443
|
-
end
|
|
1444
|
-
|
|
1445
|
-
[status, buffer]
|
|
1446
|
-
end
|
|
1447
|
-
|
|
1448
|
-
# ---------------- Welcome ----------------
|
|
1449
|
-
def print_welcome
|
|
1450
|
-
puts color("Welcome to srsh #{SRSH_VERSION} - your simple Ruby shell!",36)
|
|
1451
|
-
puts color("Current Time:",36) + " " + color(current_time,34)
|
|
1452
|
-
puts cpu_info
|
|
1453
|
-
puts ram_info
|
|
1454
|
-
puts storage_info
|
|
1455
|
-
puts dynamic_quote
|
|
1456
|
-
puts
|
|
1457
|
-
puts color("Coded with love by https://github.com/RobertFlexx",90)
|
|
1458
|
-
puts
|
|
1459
|
-
end
|
|
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
|
-
|
|
1472
|
-
print_welcome
|
|
1473
|
-
|
|
1474
|
-
# ---------------- Main Loop ----------------
|
|
1475
|
-
loop do
|
|
1476
|
-
print "\033]0;srsh-#{SRSH_VERSION}\007"
|
|
1477
|
-
prompt_str = prompt(hostname, prompt_color)
|
|
1478
|
-
|
|
1479
|
-
status, input = read_line_with_ghost(prompt_str)
|
|
1480
|
-
|
|
1481
|
-
break if status == :eof
|
|
1482
|
-
next if status == :interrupt
|
|
1483
|
-
|
|
1484
|
-
next if input.nil?
|
|
1485
|
-
input = input.strip
|
|
1486
|
-
next if input.empty?
|
|
1487
|
-
|
|
1488
|
-
HISTORY << input
|
|
1489
|
-
|
|
1490
|
-
run_input_line(input)
|
|
1491
|
-
end
|