srsh 0.7.0 → 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.
- checksums.yaml +4 -4
- data/bin/srsh +608 -341
- data/lib/srsh/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0c0910382ad05f07b93041cfbecb7d3db98f24e23df1ecd5d23243aab35a7f63
|
|
4
|
+
data.tar.gz: c39ce2184076112bb950035ea2f09939bef9f4105b2c207e6ec73ee152fc3c89
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.7.
|
|
10
|
+
SRSH_VERSION = "0.7.1"
|
|
11
11
|
|
|
12
12
|
$0 = "srsh-#{SRSH_VERSION}"
|
|
13
13
|
ENV['SHELL'] = "srsh-#{SRSH_VERSION}"
|
|
@@ -15,6 +15,7 @@ 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
|
|
@@ -26,345 +27,380 @@ $rsh_script_mode = false
|
|
|
26
27
|
|
|
27
28
|
Signal.trap("INT", "IGNORE")
|
|
28
29
|
|
|
29
|
-
#
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
else
|
|
34
|
-
[]
|
|
35
|
-
end
|
|
30
|
+
# Control-flow exceptions for the scripting engine
|
|
31
|
+
class RshBreak < StandardError; end
|
|
32
|
+
class RshContinue < StandardError; end
|
|
33
|
+
class RshReturn < StandardError; end
|
|
36
34
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
41
|
end
|
|
42
|
-
rescue
|
|
43
|
-
end
|
|
44
|
-
end
|
|
45
42
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
# (Currently not sourced by srsh runtime.)
|
|
55
|
-
RC
|
|
56
|
-
end
|
|
57
|
-
rescue
|
|
58
|
-
end
|
|
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
|
|
59
51
|
|
|
60
|
-
# ----------------
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
|
64
65
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
66
|
+
# ---------------- Utilities ----------------
|
|
67
|
+
def color(text, code)
|
|
68
|
+
"\e[#{code}m#{text}\e[0m"
|
|
69
|
+
end
|
|
68
70
|
|
|
69
|
-
def
|
|
70
|
-
|
|
71
|
-
end
|
|
71
|
+
def random_color
|
|
72
|
+
[31, 32, 33, 34, 35, 36, 37].sample
|
|
73
|
+
end
|
|
72
74
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
|
81
293
|
end
|
|
294
|
+
[cores, freqs.first(cores)]
|
|
82
295
|
end
|
|
83
296
|
|
|
84
|
-
def
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
|
97
316
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
317
|
+
raw_freq_hz = begin
|
|
318
|
+
`sysctl -n hw.cpufrequency 2>/dev/null`.to_i
|
|
319
|
+
rescue
|
|
320
|
+
0
|
|
321
|
+
end
|
|
102
322
|
|
|
103
|
-
|
|
104
|
-
|
|
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
|
|
105
330
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
|
113
344
|
end
|
|
114
|
-
"#{format('%.2f', size)} #{unit}"
|
|
115
|
-
end
|
|
116
345
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
b = "█" * f + "░" * (w - f)
|
|
121
|
-
pct = (p * 100).to_i
|
|
122
|
-
"#{color("[#{b}]", code)} #{color(sprintf("%3d%%", pct), 37)}"
|
|
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)}"
|
|
123
349
|
end
|
|
124
350
|
|
|
125
|
-
def
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
|
129
401
|
end
|
|
130
402
|
|
|
131
|
-
def
|
|
132
|
-
str.to_s.gsub(/\e\[[0-9;]*m/, '')
|
|
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
|
|
403
|
+
def storage_info
|
|
368
404
|
begin
|
|
369
405
|
require 'sys/filesystem'
|
|
370
406
|
stat = Sys::Filesystem.stat(Dir.pwd)
|
|
@@ -395,6 +431,14 @@ def builtin_help
|
|
|
395
431
|
puts color(sprintf("%-15s", "hist"), "1;36") + "Show shell history"
|
|
396
432
|
puts color(sprintf("%-15s", "clearhist"), "1;36") + "Clear saved history (memory + file)"
|
|
397
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"
|
|
398
442
|
puts color(sprintf("%-15s", "help"), "1;36") + "Show this help message"
|
|
399
443
|
puts color('=' * 60, "1;35")
|
|
400
444
|
end
|
|
@@ -602,7 +646,7 @@ def rsh_find_if_bounds(lines, start_idx)
|
|
|
602
646
|
else_idx = nil
|
|
603
647
|
i = start_idx + 1
|
|
604
648
|
while i < lines.length
|
|
605
|
-
line = lines[i].to_s.strip
|
|
649
|
+
line = strip_rsh_comment(lines[i].to_s).strip
|
|
606
650
|
if line.start_with?("if ")
|
|
607
651
|
depth += 1
|
|
608
652
|
elsif line.start_with?("while ")
|
|
@@ -624,7 +668,7 @@ def rsh_find_block_end(lines, start_idx)
|
|
|
624
668
|
depth = 1
|
|
625
669
|
i = start_idx + 1
|
|
626
670
|
while i < lines.length
|
|
627
|
-
line = lines[i].to_s.strip
|
|
671
|
+
line = strip_rsh_comment(lines[i].to_s).strip
|
|
628
672
|
if line.start_with?("if ") || line.start_with?("while ") || line.start_with?("fn ")
|
|
629
673
|
depth += 1
|
|
630
674
|
elsif line == "end"
|
|
@@ -636,14 +680,40 @@ def rsh_find_block_end(lines, start_idx)
|
|
|
636
680
|
raise "Unmatched block in rsh script"
|
|
637
681
|
end
|
|
638
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
|
+
|
|
639
708
|
def run_rsh_block(lines, start_idx, end_idx)
|
|
640
709
|
i = start_idx
|
|
641
710
|
while i < end_idx
|
|
642
|
-
raw
|
|
643
|
-
i
|
|
711
|
+
raw = lines[i]
|
|
712
|
+
i += 1
|
|
644
713
|
next if raw.nil?
|
|
645
|
-
|
|
646
|
-
|
|
714
|
+
|
|
715
|
+
line = strip_rsh_comment(raw).strip
|
|
716
|
+
next if line.empty?
|
|
647
717
|
|
|
648
718
|
if line.start_with?("if ")
|
|
649
719
|
cond_expr = line[3..-1].strip
|
|
@@ -656,14 +726,22 @@ def run_rsh_block(lines, start_idx, end_idx)
|
|
|
656
726
|
end
|
|
657
727
|
i = end_idx_2 + 1
|
|
658
728
|
next
|
|
729
|
+
|
|
659
730
|
elsif line.start_with?("while ")
|
|
660
731
|
cond_expr = line[6..-1].strip
|
|
661
732
|
block_end = rsh_find_block_end(lines, i - 1)
|
|
662
733
|
while eval_rsh_expr(cond_expr)
|
|
663
|
-
|
|
734
|
+
begin
|
|
735
|
+
run_rsh_block(lines, i, block_end)
|
|
736
|
+
rescue RshBreak
|
|
737
|
+
break
|
|
738
|
+
rescue RshContinue
|
|
739
|
+
next
|
|
740
|
+
end
|
|
664
741
|
end
|
|
665
742
|
i = block_end + 1
|
|
666
743
|
next
|
|
744
|
+
|
|
667
745
|
elsif line.start_with?("fn ")
|
|
668
746
|
parts = line.split
|
|
669
747
|
name = parts[1]
|
|
@@ -675,6 +753,7 @@ def run_rsh_block(lines, start_idx, end_idx)
|
|
|
675
753
|
}
|
|
676
754
|
i = block_end + 1
|
|
677
755
|
next
|
|
756
|
+
|
|
678
757
|
else
|
|
679
758
|
run_input_line(line)
|
|
680
759
|
end
|
|
@@ -703,15 +782,20 @@ def rsh_call_function(name, argv)
|
|
|
703
782
|
saved_positional = $rsh_positional
|
|
704
783
|
$rsh_positional = {}
|
|
705
784
|
$rsh_positional[0] = name
|
|
785
|
+
|
|
706
786
|
fn[:args].each_with_index do |argname, idx|
|
|
707
787
|
val = argv[idx] || ""
|
|
708
788
|
ENV[argname] = val
|
|
709
789
|
$rsh_positional[idx + 1] = val
|
|
710
790
|
end
|
|
711
791
|
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
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
|
|
715
799
|
end
|
|
716
800
|
|
|
717
801
|
# ---------------- External Execution Helper ----------------
|
|
@@ -764,9 +848,37 @@ end
|
|
|
764
848
|
|
|
765
849
|
# ---------------- Command Execution ----------------
|
|
766
850
|
def run_command(cmd)
|
|
767
|
-
cmd = cmd.to_s
|
|
768
|
-
|
|
769
|
-
|
|
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 ----------------
|
|
770
882
|
cmd, stdin_file, stdout_file, append = parse_redirection(cmd)
|
|
771
883
|
args = Shellwords.shellsplit(cmd) rescue []
|
|
772
884
|
return if args.empty?
|
|
@@ -774,6 +886,7 @@ def run_command(cmd)
|
|
|
774
886
|
# rsh functions
|
|
775
887
|
if $rsh_functions.key?(args[0])
|
|
776
888
|
rsh_call_function(args[0], args[1..-1] || [])
|
|
889
|
+
$last_status = 0
|
|
777
890
|
return
|
|
778
891
|
end
|
|
779
892
|
|
|
@@ -781,26 +894,33 @@ def run_command(cmd)
|
|
|
781
894
|
when 'ls'
|
|
782
895
|
if args.length == 1
|
|
783
896
|
builtin_ls(".")
|
|
784
|
-
return
|
|
785
897
|
elsif args.length == 2 && !args[1].start_with?("-")
|
|
786
898
|
builtin_ls(args[1])
|
|
899
|
+
else
|
|
900
|
+
exec_external(args, stdin_file, stdout_file, append)
|
|
787
901
|
return
|
|
788
902
|
end
|
|
789
|
-
|
|
903
|
+
$last_status = 0
|
|
790
904
|
return
|
|
905
|
+
|
|
791
906
|
when 'cd'
|
|
792
907
|
path = args[1] ? File.expand_path(args[1]) : ENV['HOME']
|
|
793
908
|
if !File.exist?(path)
|
|
794
909
|
puts color("cd: no such file or directory: #{args[1]}", 31)
|
|
910
|
+
$last_status = 1
|
|
795
911
|
elsif !File.directory?(path)
|
|
796
912
|
puts color("cd: not a directory: #{args[1]}", 31)
|
|
913
|
+
$last_status = 1
|
|
797
914
|
else
|
|
798
915
|
Dir.chdir(path)
|
|
916
|
+
$last_status = 0
|
|
799
917
|
end
|
|
800
918
|
return
|
|
919
|
+
|
|
801
920
|
when 'exit','quit'
|
|
802
921
|
$child_pids.each { |pid| Process.kill("TERM", pid) rescue nil }
|
|
803
922
|
exit 0
|
|
923
|
+
|
|
804
924
|
when 'alias'
|
|
805
925
|
if args[1].nil?
|
|
806
926
|
$aliases.each { |k,v| puts "#{k}='#{v}'" }
|
|
@@ -812,46 +932,188 @@ def run_command(cmd)
|
|
|
812
932
|
puts color("Invalid alias format", 31)
|
|
813
933
|
end
|
|
814
934
|
end
|
|
935
|
+
$last_status = 0
|
|
815
936
|
return
|
|
937
|
+
|
|
816
938
|
when 'unalias'
|
|
817
939
|
if args[1]
|
|
818
940
|
$aliases.delete(args[1])
|
|
941
|
+
$last_status = 0
|
|
819
942
|
else
|
|
820
943
|
puts color("unalias: usage: unalias name", 31)
|
|
944
|
+
$last_status = 1
|
|
821
945
|
end
|
|
822
946
|
return
|
|
947
|
+
|
|
823
948
|
when 'help'
|
|
824
949
|
builtin_help
|
|
950
|
+
$last_status = 0
|
|
825
951
|
return
|
|
952
|
+
|
|
826
953
|
when 'systemfetch'
|
|
827
954
|
builtin_systemfetch
|
|
955
|
+
$last_status = 0
|
|
828
956
|
return
|
|
957
|
+
|
|
829
958
|
when 'jobs'
|
|
830
959
|
builtin_jobs
|
|
960
|
+
$last_status = 0
|
|
831
961
|
return
|
|
962
|
+
|
|
832
963
|
when 'pwd'
|
|
833
964
|
puts color(Dir.pwd, 36)
|
|
965
|
+
$last_status = 0
|
|
834
966
|
return
|
|
967
|
+
|
|
835
968
|
when 'hist'
|
|
836
969
|
builtin_hist
|
|
970
|
+
$last_status = 0
|
|
837
971
|
return
|
|
972
|
+
|
|
838
973
|
when 'clearhist'
|
|
839
974
|
builtin_clearhist
|
|
975
|
+
$last_status = 0
|
|
840
976
|
return
|
|
977
|
+
|
|
841
978
|
when 'put'
|
|
842
979
|
msg = args[1..-1].join(' ')
|
|
843
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
|
|
844
1061
|
return
|
|
845
1062
|
end
|
|
846
1063
|
|
|
1064
|
+
# Fallback to external command
|
|
847
1065
|
exec_external(args, stdin_file, stdout_file, append)
|
|
848
1066
|
end
|
|
849
1067
|
|
|
850
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
|
+
|
|
851
1115
|
def run_input_line(input)
|
|
852
|
-
|
|
853
|
-
commands.each do |cmd|
|
|
854
|
-
next if cmd.empty?
|
|
1116
|
+
split_commands(input).each do |cmd|
|
|
855
1117
|
run_command(cmd)
|
|
856
1118
|
end
|
|
857
1119
|
end
|
|
@@ -1084,12 +1346,14 @@ def read_line_with_ghost(prompt_str)
|
|
|
1084
1346
|
STDOUT.print("\r\n")
|
|
1085
1347
|
STDOUT.flush
|
|
1086
1348
|
break
|
|
1349
|
+
|
|
1087
1350
|
when "\u0003" # Ctrl-C
|
|
1088
1351
|
STDOUT.print("^C\r\n")
|
|
1089
1352
|
STDOUT.flush
|
|
1090
1353
|
status = :interrupt
|
|
1091
1354
|
buffer = ""
|
|
1092
1355
|
break
|
|
1356
|
+
|
|
1093
1357
|
when "\u0004" # Ctrl-D
|
|
1094
1358
|
if buffer.empty?
|
|
1095
1359
|
status = :eof
|
|
@@ -1100,10 +1364,12 @@ def read_line_with_ghost(prompt_str)
|
|
|
1100
1364
|
else
|
|
1101
1365
|
# ignore when line not empty
|
|
1102
1366
|
end
|
|
1367
|
+
|
|
1103
1368
|
when "\u0001" # Ctrl-A - move to beginning of line
|
|
1104
1369
|
cursor = 0
|
|
1105
1370
|
last_tab_prefix = nil
|
|
1106
1371
|
tab_cycle = 0
|
|
1372
|
+
|
|
1107
1373
|
when "\u007F", "\b" # Backspace
|
|
1108
1374
|
if cursor > 0
|
|
1109
1375
|
buffer.slice!(cursor - 1)
|
|
@@ -1111,12 +1377,12 @@ def read_line_with_ghost(prompt_str)
|
|
|
1111
1377
|
end
|
|
1112
1378
|
last_tab_prefix = nil
|
|
1113
1379
|
tab_cycle = 0
|
|
1380
|
+
|
|
1114
1381
|
when "\t" # Tab completion
|
|
1115
1382
|
buffer, cursor, last_tab_prefix, tab_cycle, printed =
|
|
1116
1383
|
handle_tab_completion(prompt_str, buffer, cursor, last_tab_prefix, tab_cycle)
|
|
1117
|
-
# After showing completion list, reset render rows so the next prompt
|
|
1118
|
-
# redraw only clears the current input line, not the completion block.
|
|
1119
1384
|
$last_render_rows = 1 if printed
|
|
1385
|
+
|
|
1120
1386
|
when "\e" # Escape sequences (arrows, home/end)
|
|
1121
1387
|
seq1 = io.getch
|
|
1122
1388
|
seq2 = io.getch
|
|
@@ -1161,6 +1427,7 @@ def read_line_with_ghost(prompt_str)
|
|
|
1161
1427
|
end
|
|
1162
1428
|
last_tab_prefix = nil
|
|
1163
1429
|
tab_cycle = 0
|
|
1430
|
+
|
|
1164
1431
|
else
|
|
1165
1432
|
if ch.ord >= 32 && ch.ord != 127
|
|
1166
1433
|
buffer.insert(cursor, ch)
|
data/lib/srsh/version.rb
CHANGED