srsh 0.7.0 → 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 -1224
- data/lib/srsh/version.rb +0 -4
data/lib/srsh/runner.rb
ADDED
|
@@ -0,0 +1,2416 @@
|
|
|
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
|
+
require 'fileutils'
|
|
9
|
+
require 'json'
|
|
10
|
+
|
|
11
|
+
# ---------------- Version ----------------
|
|
12
|
+
SRSH_VERSION = "0.8.0"
|
|
13
|
+
|
|
14
|
+
$0 = "srsh-#{SRSH_VERSION}"
|
|
15
|
+
ENV['SHELL'] = "srsh-#{SRSH_VERSION}"
|
|
16
|
+
print "\033]0;srsh-#{SRSH_VERSION}\007"
|
|
17
|
+
|
|
18
|
+
Dir.chdir(ENV['HOME']) if ENV['HOME']
|
|
19
|
+
|
|
20
|
+
# ---------------- Paths ----------------
|
|
21
|
+
SRSH_DIR = File.join(Dir.home, ".srsh")
|
|
22
|
+
SRSH_PLUGINS_DIR= File.join(SRSH_DIR, "plugins")
|
|
23
|
+
SRSH_THEMES_DIR = File.join(SRSH_DIR, "themes")
|
|
24
|
+
SRSH_CONFIG = File.join(SRSH_DIR, "config")
|
|
25
|
+
HISTORY_FILE = File.join(Dir.home, ".srsh_history")
|
|
26
|
+
RC_FILE = File.join(Dir.home, ".srshrc")
|
|
27
|
+
THEME_STATE_FILE= File.join(SRSH_DIR, "theme")
|
|
28
|
+
|
|
29
|
+
begin
|
|
30
|
+
FileUtils.mkdir_p(SRSH_PLUGINS_DIR)
|
|
31
|
+
FileUtils.mkdir_p(SRSH_THEMES_DIR)
|
|
32
|
+
rescue
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# ---------------- Globals ----------------
|
|
36
|
+
$child_pids = []
|
|
37
|
+
$aliases = {}
|
|
38
|
+
$last_render_rows = 0
|
|
39
|
+
$last_status = 0
|
|
40
|
+
|
|
41
|
+
$rsh_functions = {} # name => { args:[...], body:[nodes] }
|
|
42
|
+
$rsh_positional = {} # 0..n => string
|
|
43
|
+
$rsh_call_depth = 0
|
|
44
|
+
|
|
45
|
+
$builtins = {} # name => proc(args)
|
|
46
|
+
$hooks = Hash.new { |h,k| h[k] = [] } # :pre_cmd, :post_cmd, :prompt
|
|
47
|
+
|
|
48
|
+
Signal.trap("INT", "IGNORE")
|
|
49
|
+
|
|
50
|
+
# Control-flow exceptions for the scripting engine
|
|
51
|
+
class RshBreak < StandardError; end
|
|
52
|
+
class RshContinue < StandardError; end
|
|
53
|
+
class RshReturn < StandardError; end
|
|
54
|
+
|
|
55
|
+
# ---------------- Config helpers ----------------
|
|
56
|
+
|
|
57
|
+
def read_kv_file(path)
|
|
58
|
+
h = {}
|
|
59
|
+
return h unless File.exist?(path)
|
|
60
|
+
File.foreach(path) do |line|
|
|
61
|
+
line = line.to_s.strip
|
|
62
|
+
next if line.empty? || line.start_with?("#")
|
|
63
|
+
k, v = line.split("=", 2)
|
|
64
|
+
next if k.nil? || v.nil?
|
|
65
|
+
h[k.strip] = v.strip
|
|
66
|
+
end
|
|
67
|
+
h
|
|
68
|
+
rescue
|
|
69
|
+
{}
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def write_kv_file(path, h)
|
|
73
|
+
dir = File.dirname(path)
|
|
74
|
+
FileUtils.mkdir_p(dir) rescue nil
|
|
75
|
+
tmp = path + ".tmp"
|
|
76
|
+
File.open(tmp, "w") do |f|
|
|
77
|
+
h.keys.sort.each { |k| f.puts("#{k}=#{h[k]}") }
|
|
78
|
+
end
|
|
79
|
+
File.rename(tmp, path)
|
|
80
|
+
rescue
|
|
81
|
+
nil
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# ---------------- Theme system ----------------
|
|
85
|
+
|
|
86
|
+
def supports_truecolor?
|
|
87
|
+
ct = (ENV['COLORTERM'] || "").downcase
|
|
88
|
+
return true if ct.include?("truecolor") || ct.include?("24bit")
|
|
89
|
+
false
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def term_colors
|
|
93
|
+
@term_colors ||= begin
|
|
94
|
+
out = `tput colors 2>/dev/null`.to_i
|
|
95
|
+
out > 0 ? out : 8
|
|
96
|
+
rescue
|
|
97
|
+
8
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def fg_rgb(r,g,b)
|
|
102
|
+
"38;2;#{r};#{g};#{b}"
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def fg_256(n)
|
|
106
|
+
"38;5;#{n}"
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
DEFAULT_THEMES = begin
|
|
110
|
+
ghost = if supports_truecolor?
|
|
111
|
+
fg_rgb(140,140,140)
|
|
112
|
+
elsif term_colors >= 256
|
|
113
|
+
fg_256(244)
|
|
114
|
+
else
|
|
115
|
+
"90" # bright black / gray
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
{
|
|
119
|
+
"classic" => {
|
|
120
|
+
name: "classic",
|
|
121
|
+
ui_border: "1;35",
|
|
122
|
+
ui_title: "1;33",
|
|
123
|
+
ui_hdr: "1;36",
|
|
124
|
+
ui_key: "1;36",
|
|
125
|
+
ui_val: "0;37",
|
|
126
|
+
ok: "32",
|
|
127
|
+
warn: "33",
|
|
128
|
+
err: "31",
|
|
129
|
+
dim: ghost,
|
|
130
|
+
prompt_path: "33",
|
|
131
|
+
prompt_host: "36",
|
|
132
|
+
prompt_mark: "35",
|
|
133
|
+
quote_rainbow: true,
|
|
134
|
+
},
|
|
135
|
+
|
|
136
|
+
"mono" => {
|
|
137
|
+
name: "mono",
|
|
138
|
+
ui_border: "1;37",
|
|
139
|
+
ui_title: "1;37",
|
|
140
|
+
ui_hdr: "0;37",
|
|
141
|
+
ui_key: "0;37",
|
|
142
|
+
ui_val: "0;37",
|
|
143
|
+
ok: "0;37",
|
|
144
|
+
warn: "0;37",
|
|
145
|
+
err: "0;37",
|
|
146
|
+
dim: ghost,
|
|
147
|
+
prompt_path: "0;37",
|
|
148
|
+
prompt_host: "0;37",
|
|
149
|
+
prompt_mark: "0;37",
|
|
150
|
+
quote_rainbow: false,
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
"neon" => {
|
|
154
|
+
name: "neon",
|
|
155
|
+
ui_border: "1;35",
|
|
156
|
+
ui_title: "1;92",
|
|
157
|
+
ui_hdr: "1;96",
|
|
158
|
+
ui_key: "1;95",
|
|
159
|
+
ui_val: "0;37",
|
|
160
|
+
ok: "1;92",
|
|
161
|
+
warn: "1;93",
|
|
162
|
+
err: "1;91",
|
|
163
|
+
dim: ghost,
|
|
164
|
+
prompt_path: "1;93",
|
|
165
|
+
prompt_host: "1;96",
|
|
166
|
+
prompt_mark: "1;95",
|
|
167
|
+
quote_rainbow: true,
|
|
168
|
+
},
|
|
169
|
+
|
|
170
|
+
# special request: light blue + deep ocean blue vibe
|
|
171
|
+
"ocean" => begin
|
|
172
|
+
deep = supports_truecolor? ? fg_rgb(0, 86, 180) : (term_colors >= 256 ? fg_256(25) : "34")
|
|
173
|
+
light = supports_truecolor? ? fg_rgb(120, 210, 255) : (term_colors >= 256 ? fg_256(81) : "36")
|
|
174
|
+
{
|
|
175
|
+
name: "ocean",
|
|
176
|
+
ui_border: deep,
|
|
177
|
+
ui_title: light,
|
|
178
|
+
ui_hdr: light,
|
|
179
|
+
ui_key: deep,
|
|
180
|
+
ui_val: "0;37",
|
|
181
|
+
ok: light,
|
|
182
|
+
warn: "33",
|
|
183
|
+
err: "31",
|
|
184
|
+
dim: ghost,
|
|
185
|
+
prompt_path: deep,
|
|
186
|
+
prompt_host: light,
|
|
187
|
+
prompt_mark: light,
|
|
188
|
+
quote_rainbow: false,
|
|
189
|
+
}
|
|
190
|
+
end,
|
|
191
|
+
}
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
$themes = DEFAULT_THEMES.dup
|
|
195
|
+
$theme_name = nil
|
|
196
|
+
$theme = nil
|
|
197
|
+
|
|
198
|
+
def color(text, code)
|
|
199
|
+
text = text.to_s
|
|
200
|
+
return text if code.nil? || code.to_s.empty?
|
|
201
|
+
"\e[#{code}m#{text}\e[0m"
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def t(key)
|
|
205
|
+
($theme && $theme[key])
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def ui(text, key)
|
|
209
|
+
color(text, t(key))
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def load_user_themes!
|
|
213
|
+
# .theme format: key=value
|
|
214
|
+
Dir.glob(File.join(SRSH_THEMES_DIR, "*.theme")).each do |path|
|
|
215
|
+
name = File.basename(path, ".theme")
|
|
216
|
+
data = read_kv_file(path)
|
|
217
|
+
next if data.empty?
|
|
218
|
+
theme = { name: name.to_s }
|
|
219
|
+
data.each do |k, v|
|
|
220
|
+
theme[k.to_sym] = v
|
|
221
|
+
end
|
|
222
|
+
$themes[name] = theme
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# .json format
|
|
226
|
+
Dir.glob(File.join(SRSH_THEMES_DIR, "*.json")).each do |path|
|
|
227
|
+
name = File.basename(path, ".json")
|
|
228
|
+
begin
|
|
229
|
+
obj = JSON.parse(File.read(path))
|
|
230
|
+
next unless obj.is_a?(Hash)
|
|
231
|
+
theme = { name: name.to_s }
|
|
232
|
+
obj.each { |k,v| theme[k.to_sym] = v.to_s }
|
|
233
|
+
$themes[name] = theme
|
|
234
|
+
rescue
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
rescue
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def set_theme!(name)
|
|
241
|
+
name = name.to_s
|
|
242
|
+
th = $themes[name]
|
|
243
|
+
return false unless th.is_a?(Hash)
|
|
244
|
+
$theme_name = name
|
|
245
|
+
$theme = th
|
|
246
|
+
begin
|
|
247
|
+
File.write(THEME_STATE_FILE, name + "\n")
|
|
248
|
+
rescue
|
|
249
|
+
end
|
|
250
|
+
true
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def load_theme_state!
|
|
254
|
+
load_user_themes!
|
|
255
|
+
|
|
256
|
+
# precedence: SRSH_THEME env, theme state file, config file
|
|
257
|
+
wanted = (ENV['SRSH_THEME'] || "").strip
|
|
258
|
+
if wanted.empty? && File.exist?(THEME_STATE_FILE)
|
|
259
|
+
wanted = File.read(THEME_STATE_FILE).to_s.strip
|
|
260
|
+
end
|
|
261
|
+
if wanted.empty?
|
|
262
|
+
cfg = read_kv_file(SRSH_CONFIG)
|
|
263
|
+
wanted = cfg['theme'].to_s.strip
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
wanted = "classic" if wanted.empty?
|
|
267
|
+
set_theme!(wanted) || set_theme!("classic")
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
load_theme_state!
|
|
271
|
+
|
|
272
|
+
# ---------------- History ----------------
|
|
273
|
+
HISTORY_MAX = begin
|
|
274
|
+
v = (ENV['SRSH_HISTORY_MAX'] || "5000").to_i
|
|
275
|
+
v = 5000 if v <= 0
|
|
276
|
+
v
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
HISTORY = if File.exist?(HISTORY_FILE)
|
|
280
|
+
File.readlines(HISTORY_FILE, chomp: true).first(HISTORY_MAX)
|
|
281
|
+
else
|
|
282
|
+
[]
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
at_exit do
|
|
286
|
+
begin
|
|
287
|
+
trimmed = HISTORY.last(HISTORY_MAX)
|
|
288
|
+
File.open(HISTORY_FILE, "w") { |f| trimmed.each { |line| f.puts(line) } }
|
|
289
|
+
rescue
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
# ---------------- RC file (create if missing) ----------------
|
|
294
|
+
begin
|
|
295
|
+
unless File.exist?(RC_FILE)
|
|
296
|
+
File.write(RC_FILE, <<~RC)
|
|
297
|
+
# ~/.srshrc — srsh configuration (RSH)
|
|
298
|
+
# Created automatically by srsh v#{SRSH_VERSION}
|
|
299
|
+
#
|
|
300
|
+
# Examples:
|
|
301
|
+
# alias ll='ls'
|
|
302
|
+
# scheme ocean
|
|
303
|
+
# set EDITOR nano
|
|
304
|
+
#
|
|
305
|
+
# Plugins:
|
|
306
|
+
# drop .rsh files into ~/.srsh/plugins/ to auto-load
|
|
307
|
+
# Themes:
|
|
308
|
+
# drop .theme files into ~/.srsh/themes/ to add schemes
|
|
309
|
+
RC
|
|
310
|
+
end
|
|
311
|
+
rescue
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
# ---------------- Utilities ----------------
|
|
315
|
+
|
|
316
|
+
def rainbow_codes
|
|
317
|
+
[31, 33, 32, 36, 34, 35, 91, 93, 92, 96, 94, 95]
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
def human_bytes(bytes)
|
|
321
|
+
units = ['B', 'KB', 'MB', 'GB', 'TB']
|
|
322
|
+
size = bytes.to_f
|
|
323
|
+
unit = units.shift
|
|
324
|
+
while size > 1024 && !units.empty?
|
|
325
|
+
size /= 1024
|
|
326
|
+
unit = units.shift
|
|
327
|
+
end
|
|
328
|
+
"#{format('%.2f', size)} #{unit}"
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
def nice_bar(p, w = 30, code = nil)
|
|
332
|
+
p = [[p, 0.0].max, 1.0].min
|
|
333
|
+
f = (p * w).round
|
|
334
|
+
b = "█" * f + "░" * (w - f)
|
|
335
|
+
pct = (p * 100).to_i
|
|
336
|
+
bar = "[#{b}]"
|
|
337
|
+
code ||= t(:ok)
|
|
338
|
+
"#{color(bar, code)} #{color(sprintf("%3d%%", pct), t(:ui_val))}"
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
def terminal_width
|
|
342
|
+
IO.console.winsize[1]
|
|
343
|
+
rescue
|
|
344
|
+
80
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
def strip_ansi(str)
|
|
348
|
+
str.to_s.gsub(/\e\[[0-9;]*m/, '')
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
# limit for command substitution output to avoid accidental memory nukes
|
|
352
|
+
CMD_SUBST_MAX = 256 * 1024
|
|
353
|
+
|
|
354
|
+
# Simple $(...) command substitution (no nesting)
|
|
355
|
+
def expand_command_substitutions(str)
|
|
356
|
+
return "" if str.nil?
|
|
357
|
+
s = str.to_s.dup
|
|
358
|
+
|
|
359
|
+
s.gsub(/\$\(([^()]*)\)/) do
|
|
360
|
+
inner = $1.to_s.strip
|
|
361
|
+
next "" if inner.empty?
|
|
362
|
+
begin
|
|
363
|
+
out = `#{inner} 2>/dev/null`
|
|
364
|
+
out = out.to_s
|
|
365
|
+
out = out.byteslice(0, CMD_SUBST_MAX) if out.bytesize > CMD_SUBST_MAX
|
|
366
|
+
out.strip
|
|
367
|
+
rescue
|
|
368
|
+
""
|
|
369
|
+
end
|
|
370
|
+
end
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
# variable expansion: $VAR, $1, $2, $0, $? (and $(...))
|
|
374
|
+
def expand_vars(str)
|
|
375
|
+
return "" if str.nil?
|
|
376
|
+
|
|
377
|
+
s = expand_command_substitutions(str.to_s)
|
|
378
|
+
|
|
379
|
+
# $? -> last exit status
|
|
380
|
+
s = s.gsub(/\$\?/) { $last_status.to_s }
|
|
381
|
+
|
|
382
|
+
# $1, $2, $0 from positional table
|
|
383
|
+
s = s.gsub(/\$(\d+)/) do
|
|
384
|
+
idx = $1.to_i
|
|
385
|
+
($rsh_positional && $rsh_positional[idx]) || ""
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
# $VARNAME from ENV
|
|
389
|
+
s.gsub(/\$([a-zA-Z_][a-zA-Z0-9_]*)/) { ENV[$1] || "" }
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
# ---------------- Redirections ----------------
|
|
393
|
+
# supports: <, >, >>, 2>, 2>> (simple, no quoting)
|
|
394
|
+
def parse_redirection(cmd)
|
|
395
|
+
stdin_file = nil
|
|
396
|
+
stdout_file = nil
|
|
397
|
+
stderr_file = nil
|
|
398
|
+
append_out = false
|
|
399
|
+
append_err = false
|
|
400
|
+
|
|
401
|
+
s = cmd.to_s.dup
|
|
402
|
+
|
|
403
|
+
# stderr redirection first
|
|
404
|
+
if s =~ /(.*)2>>\s*(\S+)\s*\z/
|
|
405
|
+
s = $1.strip
|
|
406
|
+
stderr_file= $2.strip
|
|
407
|
+
append_err = true
|
|
408
|
+
elsif s =~ /(.*)2>\s*(\S+)\s*\z/
|
|
409
|
+
s = $1.strip
|
|
410
|
+
stderr_file= $2.strip
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
if s =~ /(.*)>>\s*(\S+)\s*\z/
|
|
414
|
+
s = $1.strip
|
|
415
|
+
stdout_file= $2.strip
|
|
416
|
+
append_out = true
|
|
417
|
+
elsif s =~ /(.*)>\s*(\S+)\s*\z/
|
|
418
|
+
s = $1.strip
|
|
419
|
+
stdout_file= $2.strip
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
if s =~ /(.*)<\s*(\S+)\s*\z/
|
|
423
|
+
s = $1.strip
|
|
424
|
+
stdin_file= $2.strip
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
[s, stdin_file, stdout_file, append_out, stderr_file, append_err]
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
def with_redirections(stdin_file, stdout_file, append_out, stderr_file, append_err)
|
|
431
|
+
in_dup = STDIN.dup
|
|
432
|
+
out_dup = STDOUT.dup
|
|
433
|
+
err_dup = STDERR.dup
|
|
434
|
+
|
|
435
|
+
if stdin_file
|
|
436
|
+
STDIN.reopen(File.open(stdin_file, 'r')) rescue nil
|
|
437
|
+
end
|
|
438
|
+
if stdout_file
|
|
439
|
+
STDOUT.reopen(File.open(stdout_file, append_out ? 'a' : 'w')) rescue nil
|
|
440
|
+
end
|
|
441
|
+
if stderr_file
|
|
442
|
+
STDERR.reopen(File.open(stderr_file, append_err ? 'a' : 'w')) rescue nil
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
yield
|
|
446
|
+
ensure
|
|
447
|
+
STDIN.reopen(in_dup) rescue nil
|
|
448
|
+
STDOUT.reopen(out_dup) rescue nil
|
|
449
|
+
STDERR.reopen(err_dup) rescue nil
|
|
450
|
+
in_dup.close rescue nil
|
|
451
|
+
out_dup.close rescue nil
|
|
452
|
+
err_dup.close rescue nil
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
# ---------------- Aliases ----------------
|
|
456
|
+
def expand_aliases(cmd, seen = [])
|
|
457
|
+
return cmd if cmd.nil? || cmd.strip.empty?
|
|
458
|
+
first_word, rest = cmd.strip.split(' ', 2)
|
|
459
|
+
return cmd if seen.include?(first_word)
|
|
460
|
+
seen << first_word
|
|
461
|
+
|
|
462
|
+
if $aliases.key?(first_word)
|
|
463
|
+
replacement = $aliases[first_word]
|
|
464
|
+
expanded = expand_aliases(replacement, seen)
|
|
465
|
+
rest ? "#{expanded} #{rest}" : expanded
|
|
466
|
+
else
|
|
467
|
+
cmd
|
|
468
|
+
end
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
# ---------------- System Info ----------------
|
|
472
|
+
def current_time
|
|
473
|
+
Time.now.strftime("%Y-%m-%d %H:%M:%S %Z")
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
def detect_distro
|
|
477
|
+
if File.exist?('/etc/os-release')
|
|
478
|
+
line = File.read('/etc/os-release').lines.find { |l|
|
|
479
|
+
l.start_with?('PRETTY_NAME="') || l.start_with?('PRETTY_NAME=')
|
|
480
|
+
}
|
|
481
|
+
return line.split('=').last.strip.delete('"') if line
|
|
482
|
+
end
|
|
483
|
+
"#{RbConfig::CONFIG['host_os']}"
|
|
484
|
+
rescue
|
|
485
|
+
"#{RbConfig::CONFIG['host_os']}"
|
|
486
|
+
end
|
|
487
|
+
|
|
488
|
+
def os_type
|
|
489
|
+
host = RbConfig::CONFIG['host_os'].to_s
|
|
490
|
+
case host
|
|
491
|
+
when /linux/i then :linux
|
|
492
|
+
when /darwin/i then :mac
|
|
493
|
+
when /bsd/i then :bsd
|
|
494
|
+
else :other
|
|
495
|
+
end
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
# ---------------- Quotes ----------------
|
|
499
|
+
QUOTES = [
|
|
500
|
+
"Listen you flatpaker! - Terry Davis",
|
|
501
|
+
"Btw quotes have made a full rotation, some old ones may not exist (sorry)",
|
|
502
|
+
"Keep calm and ship it.",
|
|
503
|
+
"If it works, don't touch it. (Unless it's legacy, then definitely don't touch it.)",
|
|
504
|
+
"There’s no place like 127.0.0.1.",
|
|
505
|
+
"The computer is never wrong. The user is never right.",
|
|
506
|
+
"Unix is user-friendly. It's just picky about its friends.",
|
|
507
|
+
"A watched process never completes.",
|
|
508
|
+
"Pipes: the original microservices.",
|
|
509
|
+
"If you can read this, your terminal is working. Congrats.",
|
|
510
|
+
"Ctrl+C: the developer's parachute.",
|
|
511
|
+
"Nothing is permanent except the alias you forgot you set.",
|
|
512
|
+
"One does not simply exit vim.",
|
|
513
|
+
"If it compiles, it’s probably fine.",
|
|
514
|
+
"When in doubt, check $PATH.",
|
|
515
|
+
"I/O is lava.",
|
|
516
|
+
"Permissions are a feature, not a bug.",
|
|
517
|
+
"rm -rf is not a personality.",
|
|
518
|
+
"Your shell history knows too much.",
|
|
519
|
+
"If it’s slow, add caching. If it’s still slow, blame DNS.",
|
|
520
|
+
"Kernel panic: the OS's way of saying 'bruh'.",
|
|
521
|
+
"Logs don't lie. They just omit context.",
|
|
522
|
+
"Everything is a file. Including your mistakes.",
|
|
523
|
+
"Segfault: surprise!",
|
|
524
|
+
"If you can't fix it, make it a function.",
|
|
525
|
+
"The quickest optimization is deleting the feature.",
|
|
526
|
+
"Man pages: ancient scrolls of wisdom.",
|
|
527
|
+
"If at first you don’t succeed: read the error message.",
|
|
528
|
+
"The best tool is the one already installed.",
|
|
529
|
+
"A clean build is a suspicious build.",
|
|
530
|
+
"Git is not a backup, but it *tries*.",
|
|
531
|
+
"Sleep: the ultimate debugger.",
|
|
532
|
+
"Bash: because typing 'make it work' was too hard.",
|
|
533
|
+
"In POSIX we trust, in extensions we cope.",
|
|
534
|
+
"How is #{detect_distro}?",
|
|
535
|
+
"If it's on fire: commit, push, walk away.",
|
|
536
|
+
]
|
|
537
|
+
|
|
538
|
+
$current_quote = QUOTES.sample
|
|
539
|
+
|
|
540
|
+
def dynamic_quote
|
|
541
|
+
return $current_quote unless t(:quote_rainbow)
|
|
542
|
+
chars = $current_quote.chars
|
|
543
|
+
rainbow = rainbow_codes.cycle
|
|
544
|
+
chars.map { |c| color(c, rainbow.next) }.join
|
|
545
|
+
end
|
|
546
|
+
|
|
547
|
+
# ---------------- CPU / RAM / Storage ----------------
|
|
548
|
+
def read_cpu_times
|
|
549
|
+
return [] unless File.exist?('/proc/stat')
|
|
550
|
+
cpu_line = File.readlines('/proc/stat').find { |line| line.start_with?('cpu ') }
|
|
551
|
+
return [] unless cpu_line
|
|
552
|
+
cpu_line.split[1..-1].map(&:to_i)
|
|
553
|
+
rescue
|
|
554
|
+
[]
|
|
555
|
+
end
|
|
556
|
+
|
|
557
|
+
def calculate_cpu_usage(prev, current)
|
|
558
|
+
return 0.0 if prev.empty? || current.empty?
|
|
559
|
+
prev_idle = prev[3] + (prev[4] || 0)
|
|
560
|
+
idle = current[3] + (current[4] || 0)
|
|
561
|
+
prev_non_idle = prev[0] + prev[1] + prev[2] + (prev[5] || 0) + (prev[6] || 0) + (prev[7] || 0)
|
|
562
|
+
non_idle = current[0] + current[1] + current[2] + (current[5] || 0) + (current[6] || 0) + (current[7] || 0)
|
|
563
|
+
prev_total = prev_idle + prev_non_idle
|
|
564
|
+
total = idle + non_idle
|
|
565
|
+
totald = total - prev_total
|
|
566
|
+
idled = idle - prev_idle
|
|
567
|
+
return 0.0 if totald <= 0
|
|
568
|
+
((totald - idled).to_f / totald) * 100
|
|
569
|
+
end
|
|
570
|
+
|
|
571
|
+
def cpu_cores_and_freq
|
|
572
|
+
return [0, []] unless File.exist?('/proc/cpuinfo')
|
|
573
|
+
cores = 0
|
|
574
|
+
freqs = []
|
|
575
|
+
File.foreach('/proc/cpuinfo') do |line|
|
|
576
|
+
cores += 1 if line =~ /^processor\s*:\s*\d+/
|
|
577
|
+
if line =~ /^cpu MHz\s*:\s*([\d.]+)/
|
|
578
|
+
freqs << $1.to_f
|
|
579
|
+
end
|
|
580
|
+
end
|
|
581
|
+
[cores, freqs.first(cores)]
|
|
582
|
+
rescue
|
|
583
|
+
[0, []]
|
|
584
|
+
end
|
|
585
|
+
|
|
586
|
+
def cpu_info
|
|
587
|
+
usage = 0.0
|
|
588
|
+
cores = 0
|
|
589
|
+
freq_display = "N/A"
|
|
590
|
+
|
|
591
|
+
case os_type
|
|
592
|
+
when :linux
|
|
593
|
+
prev = read_cpu_times
|
|
594
|
+
sleep 0.05
|
|
595
|
+
current = read_cpu_times
|
|
596
|
+
usage = calculate_cpu_usage(prev, current).round(1)
|
|
597
|
+
cores, freqs = cpu_cores_and_freq
|
|
598
|
+
freq_display = freqs.empty? ? "N/A" : freqs.map { |f| "#{f.round(0)}MHz" }.join(', ')
|
|
599
|
+
else
|
|
600
|
+
cores = (`sysctl -n hw.ncpu 2>/dev/null`.to_i rescue 0)
|
|
601
|
+
raw_freq_hz = (`sysctl -n hw.cpufrequency 2>/dev/null`.to_i rescue 0)
|
|
602
|
+
freq_display = if raw_freq_hz > 0
|
|
603
|
+
mhz = (raw_freq_hz.to_f / 1_000_000.0).round(0)
|
|
604
|
+
"#{mhz.to_i}MHz"
|
|
605
|
+
else
|
|
606
|
+
"N/A"
|
|
607
|
+
end
|
|
608
|
+
usage = begin
|
|
609
|
+
ps_output = `ps -A -o %cpu 2>/dev/null`
|
|
610
|
+
lines = ps_output.lines
|
|
611
|
+
values = lines[1..-1] || []
|
|
612
|
+
sum = values.map { |l| l.to_f }.inject(0.0, :+)
|
|
613
|
+
cores > 0 ? (sum / cores).round(1) : sum.round(1)
|
|
614
|
+
rescue
|
|
615
|
+
0.0
|
|
616
|
+
end
|
|
617
|
+
end
|
|
618
|
+
|
|
619
|
+
"#{ui('CPU',:ui_key)} #{color("#{usage}%", t(:warn))} | " \
|
|
620
|
+
"#{ui('Cores',:ui_key)} #{color(cores.to_s, t(:ok))} | " \
|
|
621
|
+
"#{ui('Freq',:ui_key)} #{color(freq_display, t(:ui_title))}"
|
|
622
|
+
end
|
|
623
|
+
|
|
624
|
+
def ram_info
|
|
625
|
+
case os_type
|
|
626
|
+
when :linux
|
|
627
|
+
if File.exist?('/proc/meminfo')
|
|
628
|
+
meminfo = {}
|
|
629
|
+
File.read('/proc/meminfo').each_line do |line|
|
|
630
|
+
key, val = line.split(':')
|
|
631
|
+
meminfo[key.strip] = val.strip.split.first.to_i * 1024 if key && val
|
|
632
|
+
end
|
|
633
|
+
total = meminfo['MemTotal'] || 0
|
|
634
|
+
free = (meminfo['MemFree'] || 0) + (meminfo['Buffers'] || 0) + (meminfo['Cached'] || 0)
|
|
635
|
+
used = total - free
|
|
636
|
+
"#{ui('RAM',:ui_key)} #{color(human_bytes(used), t(:warn))} / #{color(human_bytes(total), t(:ok))}"
|
|
637
|
+
else
|
|
638
|
+
"#{ui('RAM',:ui_key)} Info not available"
|
|
639
|
+
end
|
|
640
|
+
else
|
|
641
|
+
begin
|
|
642
|
+
if os_type == :mac
|
|
643
|
+
total = `sysctl -n hw.memsize 2>/dev/null`.to_i
|
|
644
|
+
return "#{ui('RAM',:ui_key)} Info not available" if total <= 0
|
|
645
|
+
vm = `vm_stat 2>/dev/null`
|
|
646
|
+
page_size = vm[/page size of (\d+) bytes/, 1].to_i
|
|
647
|
+
page_size = 4096 if page_size <= 0
|
|
648
|
+
stats = {}
|
|
649
|
+
vm.each_line do |line|
|
|
650
|
+
if line =~ /^(.+):\s+(\d+)\./
|
|
651
|
+
stats[$1] = $2.to_i
|
|
652
|
+
end
|
|
653
|
+
end
|
|
654
|
+
used_pages = 0
|
|
655
|
+
%w[Pages active Pages wired down Pages occupied by compressor].each do |k|
|
|
656
|
+
used_pages += stats[k].to_i
|
|
657
|
+
end
|
|
658
|
+
used = used_pages * page_size
|
|
659
|
+
"#{ui('RAM',:ui_key)} #{color(human_bytes(used), t(:warn))} / #{color(human_bytes(total), t(:ok))}"
|
|
660
|
+
else
|
|
661
|
+
total = `sysctl -n hw.physmem 2>/dev/null`.to_i
|
|
662
|
+
total = `sysctl -n hw.realmem 2>/dev/null`.to_i if total <= 0
|
|
663
|
+
return "#{ui('RAM',:ui_key)} Info not available" if total <= 0
|
|
664
|
+
"#{ui('RAM',:ui_key)} #{color('Unknown', t(:warn))} / #{color(human_bytes(total), t(:ok))}"
|
|
665
|
+
end
|
|
666
|
+
rescue
|
|
667
|
+
"#{ui('RAM',:ui_key)} Info not available"
|
|
668
|
+
end
|
|
669
|
+
end
|
|
670
|
+
end
|
|
671
|
+
|
|
672
|
+
def storage_info
|
|
673
|
+
begin
|
|
674
|
+
require 'sys/filesystem'
|
|
675
|
+
stat = Sys::Filesystem.stat(Dir.pwd)
|
|
676
|
+
total = stat.bytes_total
|
|
677
|
+
free = stat.bytes_available
|
|
678
|
+
used = total - free
|
|
679
|
+
"#{ui("Disk(#{Dir.pwd})",:ui_key)} #{color(human_bytes(used), t(:warn))} / #{color(human_bytes(total), t(:ok))}"
|
|
680
|
+
rescue LoadError
|
|
681
|
+
"#{color("Install 'sys-filesystem' gem:", t(:err))} #{color('gem install sys-filesystem', t(:warn))}"
|
|
682
|
+
rescue
|
|
683
|
+
"#{ui('Disk',:ui_key)} Info not available"
|
|
684
|
+
end
|
|
685
|
+
end
|
|
686
|
+
|
|
687
|
+
# ---------------- Pretty column printer for colored text (used by ls) --------
|
|
688
|
+
def print_columns_colored(labels)
|
|
689
|
+
return if labels.nil? || labels.empty?
|
|
690
|
+
width = terminal_width
|
|
691
|
+
visible_lengths = labels.map { |s| strip_ansi(s).length }
|
|
692
|
+
max_len = visible_lengths.max || 0
|
|
693
|
+
col_width = [max_len + 2, 4].max
|
|
694
|
+
cols = [width / col_width, 1].max
|
|
695
|
+
rows = (labels.length.to_f / cols).ceil
|
|
696
|
+
|
|
697
|
+
rows.times do |r|
|
|
698
|
+
line = ""
|
|
699
|
+
cols.times do |c|
|
|
700
|
+
idx = c * rows + r
|
|
701
|
+
break if idx >= labels.length
|
|
702
|
+
label = labels[idx]
|
|
703
|
+
visible = strip_ansi(label).length
|
|
704
|
+
padding = col_width - visible
|
|
705
|
+
line << label << (" " * padding)
|
|
706
|
+
end
|
|
707
|
+
STDOUT.print("\r")
|
|
708
|
+
STDOUT.print(line.rstrip)
|
|
709
|
+
STDOUT.print("\n")
|
|
710
|
+
end
|
|
711
|
+
end
|
|
712
|
+
|
|
713
|
+
def builtin_ls(path = ".")
|
|
714
|
+
begin
|
|
715
|
+
entries = Dir.children(path).sort
|
|
716
|
+
rescue => e
|
|
717
|
+
puts color("ls: #{e.message}", t(:err))
|
|
718
|
+
return
|
|
719
|
+
end
|
|
720
|
+
|
|
721
|
+
labels = entries.map do |name|
|
|
722
|
+
full = File.join(path, name)
|
|
723
|
+
begin
|
|
724
|
+
if File.directory?(full)
|
|
725
|
+
color("#{name}/", t(:ui_hdr))
|
|
726
|
+
elsif File.executable?(full)
|
|
727
|
+
color("#{name}*", t(:ok))
|
|
728
|
+
else
|
|
729
|
+
color(name, t(:ui_val))
|
|
730
|
+
end
|
|
731
|
+
rescue
|
|
732
|
+
name
|
|
733
|
+
end
|
|
734
|
+
end
|
|
735
|
+
|
|
736
|
+
print_columns_colored(labels)
|
|
737
|
+
end
|
|
738
|
+
|
|
739
|
+
# ---------------- RSH ----------------
|
|
740
|
+
# Design: Ruby-ish feel (end blocks, keywords), but separate expr engine (no Ruby eval)
|
|
741
|
+
|
|
742
|
+
module Rsh
|
|
743
|
+
Token = Struct.new(:type, :value)
|
|
744
|
+
|
|
745
|
+
class Lexer
|
|
746
|
+
def initialize(src)
|
|
747
|
+
@s = src.to_s
|
|
748
|
+
@i = 0
|
|
749
|
+
@n = @s.length
|
|
750
|
+
end
|
|
751
|
+
|
|
752
|
+
def next_token
|
|
753
|
+
skip_ws
|
|
754
|
+
return Token.new(:eof, nil) if eof?
|
|
755
|
+
ch = peek
|
|
756
|
+
|
|
757
|
+
# numbers
|
|
758
|
+
if ch =~ /[0-9]/
|
|
759
|
+
return read_number
|
|
760
|
+
end
|
|
761
|
+
|
|
762
|
+
# strings
|
|
763
|
+
if ch == '"' || ch == "'"
|
|
764
|
+
return read_string
|
|
765
|
+
end
|
|
766
|
+
|
|
767
|
+
# vars: $?, $1, $NAME
|
|
768
|
+
if ch == '$'
|
|
769
|
+
advance
|
|
770
|
+
return Token.new(:var, '?') if peek == '?'
|
|
771
|
+
if peek =~ /[0-9]/
|
|
772
|
+
num = read_digits
|
|
773
|
+
return Token.new(:pos, num.to_i)
|
|
774
|
+
end
|
|
775
|
+
ident = read_ident
|
|
776
|
+
return Token.new(:var, ident)
|
|
777
|
+
end
|
|
778
|
+
|
|
779
|
+
# punctuation
|
|
780
|
+
if ch == '('
|
|
781
|
+
advance
|
|
782
|
+
return Token.new(:lparen, '(')
|
|
783
|
+
end
|
|
784
|
+
if ch == ')'
|
|
785
|
+
advance
|
|
786
|
+
return Token.new(:rparen, ')')
|
|
787
|
+
end
|
|
788
|
+
if ch == ','
|
|
789
|
+
advance
|
|
790
|
+
return Token.new(:comma, ',')
|
|
791
|
+
end
|
|
792
|
+
|
|
793
|
+
# operators (multi-char first)
|
|
794
|
+
two = @s[@i, 2]
|
|
795
|
+
three = @s[@i, 3]
|
|
796
|
+
|
|
797
|
+
if %w[== != <= >= && || ..].include?(two)
|
|
798
|
+
@i += 2
|
|
799
|
+
return Token.new(:op, two)
|
|
800
|
+
end
|
|
801
|
+
|
|
802
|
+
if %w[===].include?(three)
|
|
803
|
+
@i += 3
|
|
804
|
+
return Token.new(:op, three)
|
|
805
|
+
end
|
|
806
|
+
|
|
807
|
+
if %w[+ - * / % < > !].include?(ch)
|
|
808
|
+
advance
|
|
809
|
+
return Token.new(:op, ch)
|
|
810
|
+
end
|
|
811
|
+
|
|
812
|
+
# identifiers / keywords
|
|
813
|
+
if ch =~ /[A-Za-z_]/
|
|
814
|
+
ident = read_ident
|
|
815
|
+
type = case ident
|
|
816
|
+
when 'and', 'or', 'not' then :op
|
|
817
|
+
when 'true' then :bool
|
|
818
|
+
when 'false' then :bool
|
|
819
|
+
when 'nil' then :nil
|
|
820
|
+
else :ident
|
|
821
|
+
end
|
|
822
|
+
return Token.new(type, ident)
|
|
823
|
+
end
|
|
824
|
+
|
|
825
|
+
# unknown: treat as operator-ish single
|
|
826
|
+
advance
|
|
827
|
+
Token.new(:op, ch)
|
|
828
|
+
end
|
|
829
|
+
|
|
830
|
+
private
|
|
831
|
+
|
|
832
|
+
def eof?
|
|
833
|
+
@i >= @n
|
|
834
|
+
end
|
|
835
|
+
|
|
836
|
+
def peek
|
|
837
|
+
@s[@i]
|
|
838
|
+
end
|
|
839
|
+
|
|
840
|
+
def advance
|
|
841
|
+
@i += 1
|
|
842
|
+
end
|
|
843
|
+
|
|
844
|
+
def skip_ws
|
|
845
|
+
while !eof? && @s[@i] =~ /\s/
|
|
846
|
+
@i += 1
|
|
847
|
+
end
|
|
848
|
+
end
|
|
849
|
+
|
|
850
|
+
def read_digits
|
|
851
|
+
start = @i
|
|
852
|
+
while !eof? && @s[@i] =~ /[0-9]/
|
|
853
|
+
@i += 1
|
|
854
|
+
end
|
|
855
|
+
@s[start...@i]
|
|
856
|
+
end
|
|
857
|
+
|
|
858
|
+
def read_number
|
|
859
|
+
start = @i
|
|
860
|
+
read_digits
|
|
861
|
+
if !eof? && @s[@i] == '.' && @s[@i+1] =~ /[0-9]/
|
|
862
|
+
@i += 1
|
|
863
|
+
read_digits
|
|
864
|
+
end
|
|
865
|
+
Token.new(:num, @s[start...@i])
|
|
866
|
+
end
|
|
867
|
+
|
|
868
|
+
def read_ident
|
|
869
|
+
start = @i
|
|
870
|
+
while !eof? && @s[@i] =~ /[A-Za-z0-9_]/
|
|
871
|
+
@i += 1
|
|
872
|
+
end
|
|
873
|
+
@s[start...@i]
|
|
874
|
+
end
|
|
875
|
+
|
|
876
|
+
def read_string
|
|
877
|
+
quote = peek
|
|
878
|
+
advance
|
|
879
|
+
out = +""
|
|
880
|
+
while !eof?
|
|
881
|
+
ch = peek
|
|
882
|
+
advance
|
|
883
|
+
break if ch == quote
|
|
884
|
+
if ch == '\\' && !eof?
|
|
885
|
+
nxt = peek
|
|
886
|
+
advance
|
|
887
|
+
out << case nxt
|
|
888
|
+
when 'n' then "\n"
|
|
889
|
+
when 't' then "\t"
|
|
890
|
+
when 'r' then "\r"
|
|
891
|
+
when '"' then '"'
|
|
892
|
+
when "'" then "'"
|
|
893
|
+
when '\\' then '\\'
|
|
894
|
+
else nxt
|
|
895
|
+
end
|
|
896
|
+
else
|
|
897
|
+
out << ch
|
|
898
|
+
end
|
|
899
|
+
end
|
|
900
|
+
Token.new(:str, out)
|
|
901
|
+
end
|
|
902
|
+
end
|
|
903
|
+
|
|
904
|
+
class Parser
|
|
905
|
+
def initialize(src)
|
|
906
|
+
@lex = Lexer.new(src)
|
|
907
|
+
@tok = @lex.next_token
|
|
908
|
+
end
|
|
909
|
+
|
|
910
|
+
def parse
|
|
911
|
+
expr(0)
|
|
912
|
+
end
|
|
913
|
+
|
|
914
|
+
private
|
|
915
|
+
|
|
916
|
+
PRECEDENCE = {
|
|
917
|
+
'or' => 1, '||' => 1,
|
|
918
|
+
'and' => 2, '&&' => 2,
|
|
919
|
+
'==' => 3, '!=' => 3, '<' => 3, '<=' => 3, '>' => 3, '>=' => 3,
|
|
920
|
+
'..' => 4,
|
|
921
|
+
'+' => 5, '-' => 5,
|
|
922
|
+
'*' => 6, '/' => 6, '%' => 6,
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
def lbp(op)
|
|
926
|
+
PRECEDENCE[op] || 0
|
|
927
|
+
end
|
|
928
|
+
|
|
929
|
+
def advance
|
|
930
|
+
@tok = @lex.next_token
|
|
931
|
+
end
|
|
932
|
+
|
|
933
|
+
def expect(type)
|
|
934
|
+
t = @tok
|
|
935
|
+
raise "Expected #{type}, got #{t.type}" unless t.type == type
|
|
936
|
+
advance
|
|
937
|
+
t
|
|
938
|
+
end
|
|
939
|
+
|
|
940
|
+
def expr(rbp)
|
|
941
|
+
t = @tok
|
|
942
|
+
advance
|
|
943
|
+
left = nud(t)
|
|
944
|
+
while @tok.type == :op && lbp(@tok.value) > rbp
|
|
945
|
+
op = @tok.value
|
|
946
|
+
advance
|
|
947
|
+
left = [:bin, op, left, expr(lbp(op))]
|
|
948
|
+
end
|
|
949
|
+
left
|
|
950
|
+
end
|
|
951
|
+
|
|
952
|
+
def nud(t)
|
|
953
|
+
case t.type
|
|
954
|
+
when :num
|
|
955
|
+
if t.value.include?('.')
|
|
956
|
+
[:num, t.value.to_f]
|
|
957
|
+
else
|
|
958
|
+
[:num, t.value.to_i]
|
|
959
|
+
end
|
|
960
|
+
when :str
|
|
961
|
+
[:str, t.value]
|
|
962
|
+
when :bool
|
|
963
|
+
[:bool, t.value == 'true']
|
|
964
|
+
when :nil
|
|
965
|
+
[:nil, nil]
|
|
966
|
+
when :var
|
|
967
|
+
[:var, t.value]
|
|
968
|
+
when :pos
|
|
969
|
+
[:pos, t.value]
|
|
970
|
+
when :ident
|
|
971
|
+
# function call or identifier value
|
|
972
|
+
if @tok.type == :lparen
|
|
973
|
+
advance
|
|
974
|
+
args = []
|
|
975
|
+
if @tok.type != :rparen
|
|
976
|
+
loop do
|
|
977
|
+
args << expr(0)
|
|
978
|
+
break if @tok.type == :rparen
|
|
979
|
+
expect(:comma)
|
|
980
|
+
end
|
|
981
|
+
end
|
|
982
|
+
expect(:rparen)
|
|
983
|
+
[:call, t.value, args]
|
|
984
|
+
else
|
|
985
|
+
[:ident, t.value]
|
|
986
|
+
end
|
|
987
|
+
when :op
|
|
988
|
+
if t.value == '-' || t.value == '!' || t.value == 'not'
|
|
989
|
+
[:un, t.value, expr(7)]
|
|
990
|
+
else
|
|
991
|
+
raise "Unexpected operator #{t.value}"
|
|
992
|
+
end
|
|
993
|
+
when :lparen
|
|
994
|
+
e = expr(0)
|
|
995
|
+
expect(:rparen)
|
|
996
|
+
e
|
|
997
|
+
else
|
|
998
|
+
raise "Unexpected token #{t.type}"
|
|
999
|
+
end
|
|
1000
|
+
end
|
|
1001
|
+
end
|
|
1002
|
+
|
|
1003
|
+
module Eval
|
|
1004
|
+
module_function
|
|
1005
|
+
|
|
1006
|
+
def truthy?(v)
|
|
1007
|
+
!(v.nil? || v == false)
|
|
1008
|
+
end
|
|
1009
|
+
|
|
1010
|
+
def to_num(v)
|
|
1011
|
+
return v if v.is_a?(Integer) || v.is_a?(Float)
|
|
1012
|
+
s = v.to_s.strip
|
|
1013
|
+
return 0 if s.empty?
|
|
1014
|
+
return s.to_i if s =~ /\A-?\d+\z/
|
|
1015
|
+
return s.to_f if s =~ /\A-?\d+(\.\d+)?\z/
|
|
1016
|
+
0
|
|
1017
|
+
end
|
|
1018
|
+
|
|
1019
|
+
def to_s(v)
|
|
1020
|
+
v.nil? ? "" : v.to_s
|
|
1021
|
+
end
|
|
1022
|
+
|
|
1023
|
+
def cmp(a,b)
|
|
1024
|
+
if (a.is_a?(Integer) || a.is_a?(Float) || a.to_s =~ /\A-?\d+(\.\d+)?\z/) &&
|
|
1025
|
+
(b.is_a?(Integer) || b.is_a?(Float) || b.to_s =~ /\A-?\d+(\.\d+)?\z/)
|
|
1026
|
+
to_num(a) <=> to_num(b)
|
|
1027
|
+
else
|
|
1028
|
+
to_s(a) <=> to_s(b)
|
|
1029
|
+
end
|
|
1030
|
+
end
|
|
1031
|
+
|
|
1032
|
+
def env_lookup(name, positional, last_status)
|
|
1033
|
+
case name
|
|
1034
|
+
when '?' then last_status
|
|
1035
|
+
else
|
|
1036
|
+
ENV[name] || ""
|
|
1037
|
+
end
|
|
1038
|
+
end
|
|
1039
|
+
|
|
1040
|
+
def eval_ast(ast, positional:, last_status:)
|
|
1041
|
+
t = ast[0]
|
|
1042
|
+
case t
|
|
1043
|
+
when :num then ast[1]
|
|
1044
|
+
when :str then ast[1]
|
|
1045
|
+
when :bool then ast[1]
|
|
1046
|
+
when :nil then nil
|
|
1047
|
+
when :var
|
|
1048
|
+
env_lookup(ast[1].to_s, positional, last_status)
|
|
1049
|
+
when :pos
|
|
1050
|
+
(positional[ast[1].to_i] || "")
|
|
1051
|
+
when :ident
|
|
1052
|
+
ENV[ast[1].to_s] || ""
|
|
1053
|
+
when :un
|
|
1054
|
+
op, rhs = ast[1], eval_ast(ast[2], positional: positional, last_status: last_status)
|
|
1055
|
+
case op
|
|
1056
|
+
when '-' then -to_num(rhs)
|
|
1057
|
+
when '!', 'not'
|
|
1058
|
+
!truthy?(rhs)
|
|
1059
|
+
else
|
|
1060
|
+
nil
|
|
1061
|
+
end
|
|
1062
|
+
when :bin
|
|
1063
|
+
op = ast[1]
|
|
1064
|
+
if op == 'and' || op == '&&'
|
|
1065
|
+
l = eval_ast(ast[2], positional: positional, last_status: last_status)
|
|
1066
|
+
return false unless truthy?(l)
|
|
1067
|
+
r = eval_ast(ast[3], positional: positional, last_status: last_status)
|
|
1068
|
+
return truthy?(r)
|
|
1069
|
+
elsif op == 'or' || op == '||'
|
|
1070
|
+
l = eval_ast(ast[2], positional: positional, last_status: last_status)
|
|
1071
|
+
return true if truthy?(l)
|
|
1072
|
+
r = eval_ast(ast[3], positional: positional, last_status: last_status)
|
|
1073
|
+
return truthy?(r)
|
|
1074
|
+
end
|
|
1075
|
+
|
|
1076
|
+
a = eval_ast(ast[2], positional: positional, last_status: last_status)
|
|
1077
|
+
b = eval_ast(ast[3], positional: positional, last_status: last_status)
|
|
1078
|
+
|
|
1079
|
+
case op
|
|
1080
|
+
when '+'
|
|
1081
|
+
if (a.is_a?(Integer) || a.is_a?(Float)) && (b.is_a?(Integer) || b.is_a?(Float))
|
|
1082
|
+
a + b
|
|
1083
|
+
elsif a.to_s =~ /\A-?\d+(\.\d+)?\z/ && b.to_s =~ /\A-?\d+(\.\d+)?\z/
|
|
1084
|
+
to_num(a) + to_num(b)
|
|
1085
|
+
else
|
|
1086
|
+
to_s(a) + to_s(b)
|
|
1087
|
+
end
|
|
1088
|
+
when '-'
|
|
1089
|
+
to_num(a) - to_num(b)
|
|
1090
|
+
when '*'
|
|
1091
|
+
to_num(a) * to_num(b)
|
|
1092
|
+
when '/'
|
|
1093
|
+
den = to_num(b)
|
|
1094
|
+
den == 0 ? 0 : (to_num(a).to_f / den.to_f)
|
|
1095
|
+
when '%'
|
|
1096
|
+
den = to_num(b)
|
|
1097
|
+
den == 0 ? 0 : (to_num(a).to_i % den.to_i)
|
|
1098
|
+
when '..'
|
|
1099
|
+
to_s(a) + to_s(b)
|
|
1100
|
+
when '==' then cmp(a,b) == 0
|
|
1101
|
+
when '!=' then cmp(a,b) != 0
|
|
1102
|
+
when '<' then cmp(a,b) < 0
|
|
1103
|
+
when '<=' then cmp(a,b) <= 0
|
|
1104
|
+
when '>' then cmp(a,b) > 0
|
|
1105
|
+
when '>=' then cmp(a,b) >= 0
|
|
1106
|
+
else
|
|
1107
|
+
nil
|
|
1108
|
+
end
|
|
1109
|
+
when :call
|
|
1110
|
+
name = ast[1].to_s
|
|
1111
|
+
args = ast[2].map { |x| eval_ast(x, positional: positional, last_status: last_status) }
|
|
1112
|
+
call_fn(name, args, positional: positional, last_status: last_status)
|
|
1113
|
+
else
|
|
1114
|
+
nil
|
|
1115
|
+
end
|
|
1116
|
+
end
|
|
1117
|
+
|
|
1118
|
+
def call_fn(name, args, positional:, last_status:)
|
|
1119
|
+
case name
|
|
1120
|
+
when 'int' then to_num(args[0]).to_i
|
|
1121
|
+
when 'float' then to_num(args[0]).to_f
|
|
1122
|
+
when 'str' then to_s(args[0])
|
|
1123
|
+
when 'len' then to_s(args[0]).length
|
|
1124
|
+
when 'empty' then to_s(args[0]).empty?
|
|
1125
|
+
when 'contains' then to_s(args[0]).include?(to_s(args[1]))
|
|
1126
|
+
when 'starts' then to_s(args[0]).start_with?(to_s(args[1]))
|
|
1127
|
+
when 'ends' then to_s(args[0]).end_with?(to_s(args[1]))
|
|
1128
|
+
when 'env'
|
|
1129
|
+
key = to_s(args[0])
|
|
1130
|
+
ENV[key] || ""
|
|
1131
|
+
when 'rand'
|
|
1132
|
+
n = to_num(args[0]).to_i
|
|
1133
|
+
n = 1 if n <= 0
|
|
1134
|
+
Kernel.rand(n)
|
|
1135
|
+
when 'pick'
|
|
1136
|
+
return "" if args.empty?
|
|
1137
|
+
args[Kernel.rand(args.length)]
|
|
1138
|
+
when 'status'
|
|
1139
|
+
last_status
|
|
1140
|
+
else
|
|
1141
|
+
# unknown function: empty
|
|
1142
|
+
""
|
|
1143
|
+
end
|
|
1144
|
+
rescue
|
|
1145
|
+
""
|
|
1146
|
+
end
|
|
1147
|
+
end
|
|
1148
|
+
|
|
1149
|
+
# ---- Script parsing ----
|
|
1150
|
+
NodeCmd = Struct.new(:line)
|
|
1151
|
+
NodeIf = Struct.new(:cond, :then_nodes, :else_nodes)
|
|
1152
|
+
NodeWhile = Struct.new(:cond, :body)
|
|
1153
|
+
NodeTimes = Struct.new(:count, :body)
|
|
1154
|
+
NodeFn = Struct.new(:name, :args, :body)
|
|
1155
|
+
|
|
1156
|
+
def self.strip_comment(line)
|
|
1157
|
+
in_single = false
|
|
1158
|
+
in_double = false
|
|
1159
|
+
escaped = false
|
|
1160
|
+
i = 0
|
|
1161
|
+
while i < line.length
|
|
1162
|
+
ch = line[i]
|
|
1163
|
+
if escaped
|
|
1164
|
+
escaped = false
|
|
1165
|
+
elsif ch == '\\'
|
|
1166
|
+
escaped = true
|
|
1167
|
+
elsif ch == "'" && !in_double
|
|
1168
|
+
in_single = !in_single
|
|
1169
|
+
elsif ch == '"' && !in_single
|
|
1170
|
+
in_double = !in_double
|
|
1171
|
+
elsif ch == '#' && !in_single && !in_double
|
|
1172
|
+
return line[0...i]
|
|
1173
|
+
end
|
|
1174
|
+
i += 1
|
|
1175
|
+
end
|
|
1176
|
+
line
|
|
1177
|
+
end
|
|
1178
|
+
|
|
1179
|
+
def self.parse_program(lines)
|
|
1180
|
+
clean = lines.map { |l| strip_comment(l.to_s).rstrip }
|
|
1181
|
+
nodes, _, stop = parse_nodes(clean, 0, [])
|
|
1182
|
+
raise "Unexpected #{stop}" if stop
|
|
1183
|
+
nodes
|
|
1184
|
+
end
|
|
1185
|
+
|
|
1186
|
+
def self.parse_nodes(lines, idx, stop_words)
|
|
1187
|
+
nodes = []
|
|
1188
|
+
while idx < lines.length
|
|
1189
|
+
raw = lines[idx]
|
|
1190
|
+
idx += 1
|
|
1191
|
+
line = raw.to_s.strip
|
|
1192
|
+
next if line.empty?
|
|
1193
|
+
|
|
1194
|
+
if stop_words.include?(line)
|
|
1195
|
+
return [nodes, idx - 1, line]
|
|
1196
|
+
end
|
|
1197
|
+
|
|
1198
|
+
if line.start_with?("if ")
|
|
1199
|
+
cond = line[3..-1].to_s.strip
|
|
1200
|
+
then_nodes, idx2, stop = parse_nodes(lines, idx, ['else', 'end'])
|
|
1201
|
+
idx = idx2
|
|
1202
|
+
else_nodes = []
|
|
1203
|
+
if stop == 'else'
|
|
1204
|
+
else_nodes, idx3, stop2 = parse_nodes(lines, idx + 1, ['end'])
|
|
1205
|
+
idx = idx3
|
|
1206
|
+
raise "Unmatched if" unless stop2 == 'end'
|
|
1207
|
+
idx += 1
|
|
1208
|
+
elsif stop == 'end'
|
|
1209
|
+
idx += 1
|
|
1210
|
+
else
|
|
1211
|
+
raise "Unmatched if"
|
|
1212
|
+
end
|
|
1213
|
+
nodes << NodeIf.new(cond, then_nodes, else_nodes)
|
|
1214
|
+
next
|
|
1215
|
+
end
|
|
1216
|
+
|
|
1217
|
+
if line.start_with?("while ")
|
|
1218
|
+
cond = line[6..-1].to_s.strip
|
|
1219
|
+
body, idx2, stop = parse_nodes(lines, idx, ['end'])
|
|
1220
|
+
raise "Unmatched while" unless stop == 'end'
|
|
1221
|
+
idx = idx2 + 1
|
|
1222
|
+
nodes << NodeWhile.new(cond, body)
|
|
1223
|
+
next
|
|
1224
|
+
end
|
|
1225
|
+
|
|
1226
|
+
if line.start_with?("times ")
|
|
1227
|
+
count = line[6..-1].to_s.strip
|
|
1228
|
+
body, idx2, stop = parse_nodes(lines, idx, ['end'])
|
|
1229
|
+
raise "Unmatched times" unless stop == 'end'
|
|
1230
|
+
idx = idx2 + 1
|
|
1231
|
+
nodes << NodeTimes.new(count, body)
|
|
1232
|
+
next
|
|
1233
|
+
end
|
|
1234
|
+
|
|
1235
|
+
if line.start_with?("fn ")
|
|
1236
|
+
parts = line.split(/\s+/)
|
|
1237
|
+
name = parts[1]
|
|
1238
|
+
args = parts[2..-1] || []
|
|
1239
|
+
body, idx2, stop = parse_nodes(lines, idx, ['end'])
|
|
1240
|
+
raise "Unmatched fn" unless stop == 'end'
|
|
1241
|
+
idx = idx2 + 1
|
|
1242
|
+
nodes << NodeFn.new(name, args, body)
|
|
1243
|
+
next
|
|
1244
|
+
end
|
|
1245
|
+
|
|
1246
|
+
nodes << NodeCmd.new(line)
|
|
1247
|
+
end
|
|
1248
|
+
|
|
1249
|
+
[nodes, idx, nil]
|
|
1250
|
+
end
|
|
1251
|
+
end
|
|
1252
|
+
|
|
1253
|
+
def eval_rsh_expr(expr)
|
|
1254
|
+
return false if expr.nil? || expr.to_s.strip.empty?
|
|
1255
|
+
ast = Rsh::Parser.new(expr).parse
|
|
1256
|
+
!!Rsh::Eval.eval_ast(ast, positional: $rsh_positional, last_status: $last_status)
|
|
1257
|
+
rescue
|
|
1258
|
+
false
|
|
1259
|
+
end
|
|
1260
|
+
|
|
1261
|
+
def run_rsh_nodes(nodes)
|
|
1262
|
+
nodes.each do |node|
|
|
1263
|
+
case node
|
|
1264
|
+
when Rsh::NodeCmd
|
|
1265
|
+
run_input_line(node.line)
|
|
1266
|
+
when Rsh::NodeIf
|
|
1267
|
+
if eval_rsh_expr(node.cond)
|
|
1268
|
+
run_rsh_nodes(node.then_nodes)
|
|
1269
|
+
else
|
|
1270
|
+
run_rsh_nodes(node.else_nodes)
|
|
1271
|
+
end
|
|
1272
|
+
when Rsh::NodeWhile
|
|
1273
|
+
while eval_rsh_expr(node.cond)
|
|
1274
|
+
begin
|
|
1275
|
+
run_rsh_nodes(node.body)
|
|
1276
|
+
rescue RshBreak
|
|
1277
|
+
break
|
|
1278
|
+
rescue RshContinue
|
|
1279
|
+
next
|
|
1280
|
+
end
|
|
1281
|
+
end
|
|
1282
|
+
when Rsh::NodeTimes
|
|
1283
|
+
count_ast = Rsh::Parser.new(node.count).parse
|
|
1284
|
+
n = Rsh::Eval.eval_ast(count_ast, positional: $rsh_positional, last_status: $last_status)
|
|
1285
|
+
times = n.to_i
|
|
1286
|
+
times = 0 if times < 0
|
|
1287
|
+
times.times do |i|
|
|
1288
|
+
ENV['it'] = i.to_s
|
|
1289
|
+
begin
|
|
1290
|
+
run_rsh_nodes(node.body)
|
|
1291
|
+
rescue RshBreak
|
|
1292
|
+
break
|
|
1293
|
+
rescue RshContinue
|
|
1294
|
+
next
|
|
1295
|
+
end
|
|
1296
|
+
end
|
|
1297
|
+
when Rsh::NodeFn
|
|
1298
|
+
# define function (stored as AST body)
|
|
1299
|
+
$rsh_functions[node.name] = { args: node.args, body: node.body }
|
|
1300
|
+
else
|
|
1301
|
+
# ignore
|
|
1302
|
+
end
|
|
1303
|
+
end
|
|
1304
|
+
end
|
|
1305
|
+
|
|
1306
|
+
def rsh_run_script(script_path, argv)
|
|
1307
|
+
$rsh_call_depth += 1
|
|
1308
|
+
raise "RSH call depth exceeded" if $rsh_call_depth > 200
|
|
1309
|
+
|
|
1310
|
+
saved_pos = $rsh_positional
|
|
1311
|
+
$rsh_positional = {}
|
|
1312
|
+
$rsh_positional[0] = File.basename(script_path)
|
|
1313
|
+
argv.each_with_index { |val, idx| $rsh_positional[idx + 1] = val.to_s }
|
|
1314
|
+
|
|
1315
|
+
raise "Script too large" if File.exist?(script_path) && File.size(script_path) > 2_000_000
|
|
1316
|
+
|
|
1317
|
+
lines = File.readlines(script_path, chomp: true)
|
|
1318
|
+
lines = lines[1..-1] || [] if lines[0] && lines[0].start_with?("#!")
|
|
1319
|
+
nodes = Rsh.parse_program(lines)
|
|
1320
|
+
run_rsh_nodes(nodes)
|
|
1321
|
+
ensure
|
|
1322
|
+
$rsh_positional = saved_pos
|
|
1323
|
+
$rsh_call_depth -= 1
|
|
1324
|
+
end
|
|
1325
|
+
|
|
1326
|
+
def rsh_call_function(name, argv)
|
|
1327
|
+
fn = $rsh_functions[name]
|
|
1328
|
+
return false unless fn
|
|
1329
|
+
|
|
1330
|
+
$rsh_call_depth += 1
|
|
1331
|
+
raise "RSH call depth exceeded" if $rsh_call_depth > 200
|
|
1332
|
+
|
|
1333
|
+
saved_positional = $rsh_positional
|
|
1334
|
+
$rsh_positional = {}
|
|
1335
|
+
$rsh_positional[0] = name
|
|
1336
|
+
|
|
1337
|
+
saved_env = {}
|
|
1338
|
+
fn[:args].each_with_index do |argname, idx|
|
|
1339
|
+
val = (argv[idx] || "").to_s
|
|
1340
|
+
saved_env[argname] = ENV.key?(argname) ? ENV[argname] : :__unset__
|
|
1341
|
+
ENV[argname] = val
|
|
1342
|
+
$rsh_positional[idx + 1] = val
|
|
1343
|
+
end
|
|
1344
|
+
|
|
1345
|
+
begin
|
|
1346
|
+
run_rsh_nodes(fn[:body])
|
|
1347
|
+
rescue RshReturn
|
|
1348
|
+
# swallow
|
|
1349
|
+
ensure
|
|
1350
|
+
saved_env.each do |k, v|
|
|
1351
|
+
if v == :__unset__
|
|
1352
|
+
ENV.delete(k)
|
|
1353
|
+
else
|
|
1354
|
+
ENV[k] = v
|
|
1355
|
+
end
|
|
1356
|
+
end
|
|
1357
|
+
$rsh_positional = saved_positional
|
|
1358
|
+
$rsh_call_depth -= 1
|
|
1359
|
+
end
|
|
1360
|
+
true
|
|
1361
|
+
end
|
|
1362
|
+
|
|
1363
|
+
# ---------------- Builtin registration + plugins ----------------
|
|
1364
|
+
|
|
1365
|
+
def register_builtin(name, &blk)
|
|
1366
|
+
$builtins[name.to_s] = blk
|
|
1367
|
+
end
|
|
1368
|
+
|
|
1369
|
+
def register_hook(type, &blk)
|
|
1370
|
+
$hooks[type.to_sym] << blk
|
|
1371
|
+
end
|
|
1372
|
+
|
|
1373
|
+
def run_hooks(type, *args)
|
|
1374
|
+
$hooks[type.to_sym].each do |blk|
|
|
1375
|
+
blk.call(*args)
|
|
1376
|
+
rescue
|
|
1377
|
+
end
|
|
1378
|
+
end
|
|
1379
|
+
|
|
1380
|
+
# Ruby plugin API object
|
|
1381
|
+
class SrshAPI
|
|
1382
|
+
def builtin(name, &blk) = register_builtin(name, &blk)
|
|
1383
|
+
def hook(type, &blk) = register_hook(type, &blk)
|
|
1384
|
+
def theme(name, hash) = ($themes[name.to_s] = hash.merge(name: name.to_s))
|
|
1385
|
+
def scheme(name) = set_theme!(name)
|
|
1386
|
+
def aliases = $aliases
|
|
1387
|
+
end
|
|
1388
|
+
|
|
1389
|
+
SRSH = SrshAPI.new
|
|
1390
|
+
|
|
1391
|
+
def load_plugins!
|
|
1392
|
+
# RSH plugins first
|
|
1393
|
+
Dir.glob(File.join(SRSH_PLUGINS_DIR, "*.rsh")).sort.each do |path|
|
|
1394
|
+
begin
|
|
1395
|
+
rsh_run_script(path, [])
|
|
1396
|
+
rescue => e
|
|
1397
|
+
STDERR.puts(color("plugin(rsh) #{File.basename(path)}: #{e.class}: #{e.message}", t(:err)))
|
|
1398
|
+
end
|
|
1399
|
+
end
|
|
1400
|
+
|
|
1401
|
+
# Ruby plugins (trusted)
|
|
1402
|
+
Dir.glob(File.join(SRSH_PLUGINS_DIR, "*.rb")).sort.each do |path|
|
|
1403
|
+
begin
|
|
1404
|
+
Kernel.load(path)
|
|
1405
|
+
rescue => e
|
|
1406
|
+
STDERR.puts(color("plugin(rb) #{File.basename(path)}: #{e.class}: #{e.message}", t(:err)))
|
|
1407
|
+
end
|
|
1408
|
+
end
|
|
1409
|
+
rescue
|
|
1410
|
+
end
|
|
1411
|
+
|
|
1412
|
+
# ---------------- External Execution Helper ----------------
|
|
1413
|
+
|
|
1414
|
+
def exec_external(args, stdin_file, stdout_file, append_out, stderr_file, append_err)
|
|
1415
|
+
command_path = args[0]
|
|
1416
|
+
if command_path && (command_path.include?('/') || command_path.start_with?('.'))
|
|
1417
|
+
begin
|
|
1418
|
+
if File.directory?(command_path)
|
|
1419
|
+
puts color("srsh: #{command_path}: is a directory", t(:err))
|
|
1420
|
+
$last_status = 126
|
|
1421
|
+
return
|
|
1422
|
+
end
|
|
1423
|
+
rescue
|
|
1424
|
+
end
|
|
1425
|
+
end
|
|
1426
|
+
|
|
1427
|
+
pid = fork do
|
|
1428
|
+
Signal.trap("INT","DEFAULT")
|
|
1429
|
+
if stdin_file
|
|
1430
|
+
STDIN.reopen(File.open(stdin_file,'r')) rescue nil
|
|
1431
|
+
end
|
|
1432
|
+
if stdout_file
|
|
1433
|
+
STDOUT.reopen(File.open(stdout_file, append_out ? 'a' : 'w')) rescue nil
|
|
1434
|
+
end
|
|
1435
|
+
if stderr_file
|
|
1436
|
+
STDERR.reopen(File.open(stderr_file, append_err ? 'a' : 'w')) rescue nil
|
|
1437
|
+
end
|
|
1438
|
+
|
|
1439
|
+
begin
|
|
1440
|
+
exec(*args)
|
|
1441
|
+
rescue Errno::ENOENT
|
|
1442
|
+
STDERR.puts color("Command not found: #{args[0]}", t(:warn))
|
|
1443
|
+
exit 127
|
|
1444
|
+
rescue Errno::EACCES
|
|
1445
|
+
STDERR.puts color("Permission denied: #{args[0]}", t(:err))
|
|
1446
|
+
exit 126
|
|
1447
|
+
end
|
|
1448
|
+
end
|
|
1449
|
+
|
|
1450
|
+
$child_pids << pid
|
|
1451
|
+
begin
|
|
1452
|
+
Process.wait(pid)
|
|
1453
|
+
$last_status = $?.exitstatus || 0
|
|
1454
|
+
rescue Interrupt
|
|
1455
|
+
ensure
|
|
1456
|
+
$child_pids.delete(pid)
|
|
1457
|
+
end
|
|
1458
|
+
end
|
|
1459
|
+
|
|
1460
|
+
# ---------------- Builtins ----------------
|
|
1461
|
+
|
|
1462
|
+
def builtin_help
|
|
1463
|
+
border = ui('=' * 66, :ui_border)
|
|
1464
|
+
puts border
|
|
1465
|
+
puts ui("srsh #{SRSH_VERSION}", :ui_title) + " " + ui("Builtins", :ui_hdr)
|
|
1466
|
+
puts ui("Theme:", :ui_key) + " #{color($theme_name, t(:ui_title))}"
|
|
1467
|
+
puts ui("Tip:", :ui_key) + " use #{color('scheme --list', t(:warn))} then #{color('scheme NAME', t(:warn))}"
|
|
1468
|
+
puts ui('-' * 66, :ui_border)
|
|
1469
|
+
|
|
1470
|
+
groups = {
|
|
1471
|
+
"Core" => {
|
|
1472
|
+
"cd [dir]" => "Change directory",
|
|
1473
|
+
"pwd" => "Print working directory",
|
|
1474
|
+
"ls [dir]" => "List directory (pretty columns)",
|
|
1475
|
+
"exit | quit" => "Exit srsh",
|
|
1476
|
+
"put TEXT" => "Print text",
|
|
1477
|
+
},
|
|
1478
|
+
"Config" => {
|
|
1479
|
+
"alias [name='cmd']" => "List or set aliases",
|
|
1480
|
+
"unalias NAME" => "Remove alias",
|
|
1481
|
+
"set [VAR [value...]]" => "List or set variables",
|
|
1482
|
+
"unset VAR" => "Unset a variable",
|
|
1483
|
+
"read VAR" => "Read a line into a variable",
|
|
1484
|
+
"source FILE [args...]" => "Run an RSH script",
|
|
1485
|
+
"scheme [--list|NAME]" => "List/set color scheme",
|
|
1486
|
+
"plugins" => "List loaded plugin files",
|
|
1487
|
+
"reload" => "Reload rc + plugins",
|
|
1488
|
+
},
|
|
1489
|
+
"Info" => {
|
|
1490
|
+
"systemfetch" => "Display system information",
|
|
1491
|
+
"jobs" => "Show tracked child jobs",
|
|
1492
|
+
"hist" => "Show history",
|
|
1493
|
+
"clearhist" => "Clear history (memory + file)",
|
|
1494
|
+
"help" => "Show this help",
|
|
1495
|
+
},
|
|
1496
|
+
"RSH control" => {
|
|
1497
|
+
"if EXPR ... [else ...] end" => "Conditional block",
|
|
1498
|
+
"while EXPR ... end" => "Loop",
|
|
1499
|
+
"times EXPR ... end" => "Loop N times (ENV['it']=index)",
|
|
1500
|
+
"fn NAME [args...] ... end" => "Define function",
|
|
1501
|
+
"break | continue | return" => "Control flow (scripts)",
|
|
1502
|
+
"true | false" => "Always succeed / fail",
|
|
1503
|
+
"sleep N" => "Sleep for N seconds",
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
col1 = 26
|
|
1508
|
+
groups.each do |g, cmds|
|
|
1509
|
+
puts ui("\n#{g}:", :ui_hdr)
|
|
1510
|
+
cmds.each do |k, v|
|
|
1511
|
+
left = k.ljust(col1)
|
|
1512
|
+
puts color(left, t(:ui_key)) + color(v, t(:ui_val))
|
|
1513
|
+
end
|
|
1514
|
+
end
|
|
1515
|
+
|
|
1516
|
+
puts "\n" + border
|
|
1517
|
+
end
|
|
1518
|
+
|
|
1519
|
+
def builtin_systemfetch
|
|
1520
|
+
user = (ENV['USER'] || Etc.getlogin || Etc.getpwuid.name rescue ENV['USER'] || Etc.getlogin)
|
|
1521
|
+
host = Socket.gethostname
|
|
1522
|
+
os = detect_distro
|
|
1523
|
+
ruby_ver = RUBY_VERSION
|
|
1524
|
+
|
|
1525
|
+
cpu_percent = begin
|
|
1526
|
+
case os_type
|
|
1527
|
+
when :linux
|
|
1528
|
+
prev = read_cpu_times
|
|
1529
|
+
sleep 0.05
|
|
1530
|
+
cur = read_cpu_times
|
|
1531
|
+
calculate_cpu_usage(prev, cur).round(1)
|
|
1532
|
+
else
|
|
1533
|
+
cores = `sysctl -n hw.ncpu 2>/dev/null`.to_i rescue 0
|
|
1534
|
+
ps_output = `ps -A -o %cpu 2>/dev/null`
|
|
1535
|
+
lines = ps_output.lines
|
|
1536
|
+
values = lines[1..-1] || []
|
|
1537
|
+
sum = values.map { |l| l.to_f }.inject(0.0, :+)
|
|
1538
|
+
cores > 0 ? (sum / cores).round(1) : sum.round(1)
|
|
1539
|
+
end
|
|
1540
|
+
rescue
|
|
1541
|
+
0.0
|
|
1542
|
+
end
|
|
1543
|
+
|
|
1544
|
+
mem_percent = begin
|
|
1545
|
+
case os_type
|
|
1546
|
+
when :linux
|
|
1547
|
+
if File.exist?('/proc/meminfo')
|
|
1548
|
+
meminfo = {}
|
|
1549
|
+
File.read('/proc/meminfo').each_line do |line|
|
|
1550
|
+
k, v = line.split(':')
|
|
1551
|
+
meminfo[k.strip] = v.strip.split.first.to_i * 1024 if k && v
|
|
1552
|
+
end
|
|
1553
|
+
total = meminfo['MemTotal'] || 1
|
|
1554
|
+
free = (meminfo['MemAvailable'] || meminfo['MemFree'] || 0)
|
|
1555
|
+
used = total - free
|
|
1556
|
+
(used.to_f / total.to_f * 100).round(1)
|
|
1557
|
+
else
|
|
1558
|
+
0.0
|
|
1559
|
+
end
|
|
1560
|
+
when :mac
|
|
1561
|
+
total = `sysctl -n hw.memsize 2>/dev/null`.to_i
|
|
1562
|
+
if total <= 0
|
|
1563
|
+
0.0
|
|
1564
|
+
else
|
|
1565
|
+
vm = `vm_stat 2>/dev/null`
|
|
1566
|
+
page_size = vm[/page size of (\d+) bytes/, 1].to_i
|
|
1567
|
+
page_size = 4096 if page_size <= 0
|
|
1568
|
+
stats = {}
|
|
1569
|
+
vm.each_line do |line|
|
|
1570
|
+
if line =~ /^(.+):\s+(\d+)\./
|
|
1571
|
+
stats[$1] = $2.to_i
|
|
1572
|
+
end
|
|
1573
|
+
end
|
|
1574
|
+
used_pages = 0
|
|
1575
|
+
%w[Pages active Pages wired down Pages occupied by compressor].each { |k| used_pages += stats[k].to_i }
|
|
1576
|
+
used = used_pages * page_size
|
|
1577
|
+
((used.to_f / total.to_f) * 100).round(1)
|
|
1578
|
+
end
|
|
1579
|
+
else
|
|
1580
|
+
0.0
|
|
1581
|
+
end
|
|
1582
|
+
rescue
|
|
1583
|
+
0.0
|
|
1584
|
+
end
|
|
1585
|
+
|
|
1586
|
+
border = ui('=' * 60, :ui_border)
|
|
1587
|
+
puts border
|
|
1588
|
+
puts ui("srsh System Information", :ui_title)
|
|
1589
|
+
puts ui("User: ", :ui_key) + color("#{user}@#{host}", t(:ui_val))
|
|
1590
|
+
puts ui("OS: ", :ui_key) + color(os, t(:ui_val))
|
|
1591
|
+
puts ui("Shell: ", :ui_key) + color("srsh v#{SRSH_VERSION}", t(:ui_val))
|
|
1592
|
+
puts ui("Ruby: ", :ui_key) + color(ruby_ver, t(:ui_val))
|
|
1593
|
+
puts ui("CPU: ", :ui_key) + nice_bar(cpu_percent / 100.0, 30, t(:ok))
|
|
1594
|
+
puts ui("RAM: ", :ui_key) + nice_bar(mem_percent / 100.0, 30, t(:prompt_mark))
|
|
1595
|
+
puts border
|
|
1596
|
+
end
|
|
1597
|
+
|
|
1598
|
+
def builtin_jobs
|
|
1599
|
+
if $child_pids.empty?
|
|
1600
|
+
puts color("No tracked child jobs.", t(:ui_hdr))
|
|
1601
|
+
return
|
|
1602
|
+
end
|
|
1603
|
+
$child_pids.each do |pid|
|
|
1604
|
+
status = begin
|
|
1605
|
+
Process.kill(0, pid)
|
|
1606
|
+
'running'
|
|
1607
|
+
rescue Errno::ESRCH
|
|
1608
|
+
'done'
|
|
1609
|
+
rescue Errno::EPERM
|
|
1610
|
+
'running'
|
|
1611
|
+
end
|
|
1612
|
+
puts "[#{pid}] #{status}"
|
|
1613
|
+
end
|
|
1614
|
+
end
|
|
1615
|
+
|
|
1616
|
+
def builtin_hist
|
|
1617
|
+
HISTORY.each_with_index { |h, i| printf "%5d %s\n", i + 1, h }
|
|
1618
|
+
end
|
|
1619
|
+
|
|
1620
|
+
def builtin_clearhist
|
|
1621
|
+
HISTORY.clear
|
|
1622
|
+
File.delete(HISTORY_FILE) rescue nil
|
|
1623
|
+
puts color("History cleared (memory + file).", t(:ok))
|
|
1624
|
+
end
|
|
1625
|
+
|
|
1626
|
+
def builtin_scheme(args)
|
|
1627
|
+
a = args[1..-1] || []
|
|
1628
|
+
if a.empty?
|
|
1629
|
+
puts ui("Theme:", :ui_key) + " #{color($theme_name, t(:ui_title))}"
|
|
1630
|
+
return
|
|
1631
|
+
end
|
|
1632
|
+
|
|
1633
|
+
if a[0] == "--list" || a[0] == "-l"
|
|
1634
|
+
puts ui("Available schemes:", :ui_hdr)
|
|
1635
|
+
$themes.keys.sort.each do |k|
|
|
1636
|
+
mark = (k == $theme_name) ? color("*", t(:ok)) : " "
|
|
1637
|
+
puts " #{mark} #{k}"
|
|
1638
|
+
end
|
|
1639
|
+
puts ui("User themes dir:", :ui_key) + " #{SRSH_THEMES_DIR}"
|
|
1640
|
+
return
|
|
1641
|
+
end
|
|
1642
|
+
|
|
1643
|
+
name = a[0].to_s
|
|
1644
|
+
if set_theme!(name)
|
|
1645
|
+
puts color("scheme: now using '#{name}'", t(:ok))
|
|
1646
|
+
else
|
|
1647
|
+
puts color("scheme: unknown '#{name}' (try: scheme --list)", t(:err))
|
|
1648
|
+
$last_status = 1
|
|
1649
|
+
end
|
|
1650
|
+
end
|
|
1651
|
+
|
|
1652
|
+
def builtin_plugins
|
|
1653
|
+
rsh = Dir.glob(File.join(SRSH_PLUGINS_DIR, "*.rsh")).sort
|
|
1654
|
+
rb = Dir.glob(File.join(SRSH_PLUGINS_DIR, "*.rb")).sort
|
|
1655
|
+
puts ui("Plugins:", :ui_hdr)
|
|
1656
|
+
(rsh + rb).each { |p| puts " - #{File.basename(p)}" }
|
|
1657
|
+
puts ui("Dir:", :ui_key) + " #{SRSH_PLUGINS_DIR}"
|
|
1658
|
+
end
|
|
1659
|
+
|
|
1660
|
+
def builtin_reload
|
|
1661
|
+
begin
|
|
1662
|
+
rsh_run_script(RC_FILE, []) if File.exist?(RC_FILE)
|
|
1663
|
+
load_plugins!
|
|
1664
|
+
puts color("reloaded rc + plugins", t(:ok))
|
|
1665
|
+
rescue => e
|
|
1666
|
+
puts color("reload: #{e.class}: #{e.message}", t(:err))
|
|
1667
|
+
$last_status = 1
|
|
1668
|
+
end
|
|
1669
|
+
end
|
|
1670
|
+
|
|
1671
|
+
# register builtins
|
|
1672
|
+
register_builtin('help') { |args| builtin_help; $last_status = 0 }
|
|
1673
|
+
register_builtin('systemfetch') { |args| builtin_systemfetch; $last_status = 0 }
|
|
1674
|
+
register_builtin('jobs') { |args| builtin_jobs; $last_status = 0 }
|
|
1675
|
+
register_builtin('hist') { |args| builtin_hist; $last_status = 0 }
|
|
1676
|
+
register_builtin('clearhist') { |args| builtin_clearhist; $last_status = 0 }
|
|
1677
|
+
register_builtin('scheme') { |args| builtin_scheme(args); $last_status ||= 0 }
|
|
1678
|
+
register_builtin('plugins') { |args| builtin_plugins; $last_status = 0 }
|
|
1679
|
+
register_builtin('reload') { |args| builtin_reload; $last_status ||= 0 }
|
|
1680
|
+
|
|
1681
|
+
# ---------------- Command Execution ----------------
|
|
1682
|
+
|
|
1683
|
+
def run_command(cmd)
|
|
1684
|
+
cmd = cmd.to_s.strip
|
|
1685
|
+
return if cmd.empty?
|
|
1686
|
+
|
|
1687
|
+
cmd = expand_aliases(cmd)
|
|
1688
|
+
cmd = expand_vars(cmd)
|
|
1689
|
+
|
|
1690
|
+
run_hooks(:pre_cmd, cmd)
|
|
1691
|
+
|
|
1692
|
+
# ---- assignments ----
|
|
1693
|
+
# Shell-style: VAR=value (no spaces)
|
|
1694
|
+
if (m = cmd.match(/\A([A-Za-z_][A-Za-z0-9_]*)=(.*)\z/))
|
|
1695
|
+
var = m[1]
|
|
1696
|
+
val = m[2] || ""
|
|
1697
|
+
ENV[var] = val
|
|
1698
|
+
$last_status = 0
|
|
1699
|
+
run_hooks(:post_cmd, cmd, $last_status)
|
|
1700
|
+
return
|
|
1701
|
+
end
|
|
1702
|
+
|
|
1703
|
+
# Expr-style: VAR = expr (RSH expr, safe)
|
|
1704
|
+
if (m = cmd.match(/\A([A-Za-z_][A-Za-z0-9_]*)\s+=\s+(.+)\z/))
|
|
1705
|
+
var = m[1]
|
|
1706
|
+
rhs = m[2]
|
|
1707
|
+
begin
|
|
1708
|
+
ast = Rsh::Parser.new(rhs).parse
|
|
1709
|
+
val = Rsh::Eval.eval_ast(ast, positional: $rsh_positional, last_status: $last_status)
|
|
1710
|
+
ENV[var] = val.nil? ? "" : val.to_s
|
|
1711
|
+
$last_status = 0
|
|
1712
|
+
rescue
|
|
1713
|
+
ENV[var] = rhs
|
|
1714
|
+
$last_status = 0
|
|
1715
|
+
end
|
|
1716
|
+
run_hooks(:post_cmd, cmd, $last_status)
|
|
1717
|
+
return
|
|
1718
|
+
end
|
|
1719
|
+
|
|
1720
|
+
# ---- expression output ----
|
|
1721
|
+
# emit EXPR -> evaluate RSH expr and print it (keeps quotes intact)
|
|
1722
|
+
if (m = cmd.match(/\Aemit\s+(.+)\z/))
|
|
1723
|
+
expr = m[1]
|
|
1724
|
+
begin
|
|
1725
|
+
ast = Rsh::Parser.new(expr).parse
|
|
1726
|
+
val = Rsh::Eval.eval_ast(ast, positional: $rsh_positional, last_status: $last_status)
|
|
1727
|
+
puts(val.nil? ? "" : val.to_s)
|
|
1728
|
+
$last_status = 0
|
|
1729
|
+
rescue => e
|
|
1730
|
+
STDERR.puts color("emit: #{e.class}: #{e.message}", t('error'))
|
|
1731
|
+
$last_status = 1
|
|
1732
|
+
end
|
|
1733
|
+
run_hooks(:post_cmd, cmd, $last_status)
|
|
1734
|
+
return
|
|
1735
|
+
end
|
|
1736
|
+
|
|
1737
|
+
|
|
1738
|
+
# ---- redirections + args ----
|
|
1739
|
+
cmd2, stdin_file, stdout_file, append_out, stderr_file, append_err = parse_redirection(cmd)
|
|
1740
|
+
args = Shellwords.shellsplit(cmd2) rescue []
|
|
1741
|
+
return if args.empty?
|
|
1742
|
+
|
|
1743
|
+
# RSH functions
|
|
1744
|
+
if $rsh_functions.key?(args[0])
|
|
1745
|
+
ok = rsh_call_function(args[0], args[1..-1] || [])
|
|
1746
|
+
$last_status = ok ? 0 : 1
|
|
1747
|
+
run_hooks(:post_cmd, cmd, $last_status)
|
|
1748
|
+
return
|
|
1749
|
+
end
|
|
1750
|
+
|
|
1751
|
+
# Builtins
|
|
1752
|
+
if $builtins.key?(args[0])
|
|
1753
|
+
with_redirections(stdin_file, stdout_file, append_out, stderr_file, append_err) do
|
|
1754
|
+
begin
|
|
1755
|
+
$builtins[args[0]].call(args)
|
|
1756
|
+
rescue RshBreak
|
|
1757
|
+
raise
|
|
1758
|
+
rescue RshContinue
|
|
1759
|
+
raise
|
|
1760
|
+
rescue RshReturn
|
|
1761
|
+
raise
|
|
1762
|
+
rescue => e
|
|
1763
|
+
STDERR.puts color("#{args[0]}: #{e.class}: #{e.message}", t(:err))
|
|
1764
|
+
$last_status = 1
|
|
1765
|
+
end
|
|
1766
|
+
end
|
|
1767
|
+
run_hooks(:post_cmd, cmd, $last_status)
|
|
1768
|
+
return
|
|
1769
|
+
end
|
|
1770
|
+
|
|
1771
|
+
# Special-cased builtins that need args parsing
|
|
1772
|
+
case args[0]
|
|
1773
|
+
when 'ls'
|
|
1774
|
+
with_redirections(stdin_file, stdout_file, append_out, stderr_file, append_err) do
|
|
1775
|
+
if args.length == 1
|
|
1776
|
+
builtin_ls(".")
|
|
1777
|
+
$last_status = 0
|
|
1778
|
+
elsif args.length == 2 && !args[1].start_with?("-")
|
|
1779
|
+
builtin_ls(args[1])
|
|
1780
|
+
$last_status = 0
|
|
1781
|
+
else
|
|
1782
|
+
exec_external(args, stdin_file, stdout_file, append_out, stderr_file, append_err)
|
|
1783
|
+
end
|
|
1784
|
+
end
|
|
1785
|
+
run_hooks(:post_cmd, cmd, $last_status)
|
|
1786
|
+
return
|
|
1787
|
+
|
|
1788
|
+
when 'cd'
|
|
1789
|
+
path = args[1] ? File.expand_path(args[1]) : ENV['HOME']
|
|
1790
|
+
if !path || !File.exist?(path)
|
|
1791
|
+
puts color("cd: no such file or directory: #{args[1]}", t(:err))
|
|
1792
|
+
$last_status = 1
|
|
1793
|
+
elsif !File.directory?(path)
|
|
1794
|
+
puts color("cd: not a directory: #{args[1]}", t(:err))
|
|
1795
|
+
$last_status = 1
|
|
1796
|
+
else
|
|
1797
|
+
Dir.chdir(path)
|
|
1798
|
+
$last_status = 0
|
|
1799
|
+
end
|
|
1800
|
+
run_hooks(:post_cmd, cmd, $last_status)
|
|
1801
|
+
return
|
|
1802
|
+
|
|
1803
|
+
when 'pwd'
|
|
1804
|
+
puts color(Dir.pwd, t(:ui_hdr))
|
|
1805
|
+
$last_status = 0
|
|
1806
|
+
run_hooks(:post_cmd, cmd, $last_status)
|
|
1807
|
+
return
|
|
1808
|
+
|
|
1809
|
+
when 'exit', 'quit'
|
|
1810
|
+
$child_pids.each { |pid| Process.kill("TERM", pid) rescue nil }
|
|
1811
|
+
exit 0
|
|
1812
|
+
|
|
1813
|
+
when 'alias'
|
|
1814
|
+
if args[1].nil?
|
|
1815
|
+
$aliases.each { |k,v| puts "#{k}='#{v}'" }
|
|
1816
|
+
else
|
|
1817
|
+
arg = args[1..].join(' ')
|
|
1818
|
+
if arg =~ /^(\w+)=([\"']?)(.+?)\2$/
|
|
1819
|
+
$aliases[$1] = $3
|
|
1820
|
+
else
|
|
1821
|
+
puts color("Invalid alias format", t(:err))
|
|
1822
|
+
$last_status = 1
|
|
1823
|
+
run_hooks(:post_cmd, cmd, $last_status)
|
|
1824
|
+
return
|
|
1825
|
+
end
|
|
1826
|
+
end
|
|
1827
|
+
$last_status = 0
|
|
1828
|
+
run_hooks(:post_cmd, cmd, $last_status)
|
|
1829
|
+
return
|
|
1830
|
+
|
|
1831
|
+
when 'unalias'
|
|
1832
|
+
if args[1]
|
|
1833
|
+
$aliases.delete(args[1])
|
|
1834
|
+
$last_status = 0
|
|
1835
|
+
else
|
|
1836
|
+
puts color("unalias: usage: unalias name", t(:err))
|
|
1837
|
+
$last_status = 1
|
|
1838
|
+
end
|
|
1839
|
+
run_hooks(:post_cmd, cmd, $last_status)
|
|
1840
|
+
return
|
|
1841
|
+
|
|
1842
|
+
when 'put'
|
|
1843
|
+
msg = args[1..-1].join(' ')
|
|
1844
|
+
puts msg
|
|
1845
|
+
$last_status = 0
|
|
1846
|
+
run_hooks(:post_cmd, cmd, $last_status)
|
|
1847
|
+
return
|
|
1848
|
+
|
|
1849
|
+
when 'break'
|
|
1850
|
+
raise RshBreak
|
|
1851
|
+
when 'continue'
|
|
1852
|
+
raise RshContinue
|
|
1853
|
+
when 'return'
|
|
1854
|
+
raise RshReturn
|
|
1855
|
+
|
|
1856
|
+
when 'set'
|
|
1857
|
+
if args.length == 1
|
|
1858
|
+
ENV.keys.sort.each { |k| puts "#{k}=#{ENV[k]}" }
|
|
1859
|
+
else
|
|
1860
|
+
var = args[1]
|
|
1861
|
+
val = args[2..-1].join(' ')
|
|
1862
|
+
ENV[var] = val
|
|
1863
|
+
end
|
|
1864
|
+
$last_status = 0
|
|
1865
|
+
run_hooks(:post_cmd, cmd, $last_status)
|
|
1866
|
+
return
|
|
1867
|
+
|
|
1868
|
+
when 'unset'
|
|
1869
|
+
if args[1]
|
|
1870
|
+
ENV.delete(args[1])
|
|
1871
|
+
$last_status = 0
|
|
1872
|
+
else
|
|
1873
|
+
puts color("unset: usage: unset VAR", t(:err))
|
|
1874
|
+
$last_status = 1
|
|
1875
|
+
end
|
|
1876
|
+
run_hooks(:post_cmd, cmd, $last_status)
|
|
1877
|
+
return
|
|
1878
|
+
|
|
1879
|
+
when 'read'
|
|
1880
|
+
var = args[1]
|
|
1881
|
+
unless var
|
|
1882
|
+
puts color("read: usage: read VAR", t(:err))
|
|
1883
|
+
$last_status = 1
|
|
1884
|
+
run_hooks(:post_cmd, cmd, $last_status)
|
|
1885
|
+
return
|
|
1886
|
+
end
|
|
1887
|
+
line = STDIN.gets
|
|
1888
|
+
ENV[var] = (line ? line.chomp : "")
|
|
1889
|
+
$last_status = 0
|
|
1890
|
+
run_hooks(:post_cmd, cmd, $last_status)
|
|
1891
|
+
return
|
|
1892
|
+
|
|
1893
|
+
when 'true'
|
|
1894
|
+
$last_status = 0
|
|
1895
|
+
run_hooks(:post_cmd, cmd, $last_status)
|
|
1896
|
+
return
|
|
1897
|
+
|
|
1898
|
+
when 'false'
|
|
1899
|
+
$last_status = 1
|
|
1900
|
+
run_hooks(:post_cmd, cmd, $last_status)
|
|
1901
|
+
return
|
|
1902
|
+
|
|
1903
|
+
when 'sleep'
|
|
1904
|
+
secs = (args[1] || "1").to_f
|
|
1905
|
+
begin
|
|
1906
|
+
Kernel.sleep(secs)
|
|
1907
|
+
$last_status = 0
|
|
1908
|
+
rescue
|
|
1909
|
+
$last_status = 1
|
|
1910
|
+
end
|
|
1911
|
+
run_hooks(:post_cmd, cmd, $last_status)
|
|
1912
|
+
return
|
|
1913
|
+
|
|
1914
|
+
when 'source', '.'
|
|
1915
|
+
file = args[1]
|
|
1916
|
+
if file.nil?
|
|
1917
|
+
puts color("source: usage: source FILE", t(:err))
|
|
1918
|
+
$last_status = 1
|
|
1919
|
+
run_hooks(:post_cmd, cmd, $last_status)
|
|
1920
|
+
return
|
|
1921
|
+
end
|
|
1922
|
+
begin
|
|
1923
|
+
rsh_run_script(file, args[2..-1] || [])
|
|
1924
|
+
$last_status = 0
|
|
1925
|
+
rescue => e
|
|
1926
|
+
STDERR.puts color("source error: #{e.class}: #{e.message}", t(:err))
|
|
1927
|
+
$last_status = 1
|
|
1928
|
+
end
|
|
1929
|
+
run_hooks(:post_cmd, cmd, $last_status)
|
|
1930
|
+
return
|
|
1931
|
+
end
|
|
1932
|
+
|
|
1933
|
+
# Fallback to external command
|
|
1934
|
+
exec_external(args, stdin_file, stdout_file, append_out, stderr_file, append_err)
|
|
1935
|
+
run_hooks(:post_cmd, cmd, $last_status)
|
|
1936
|
+
end
|
|
1937
|
+
|
|
1938
|
+
# ---------------- Chained Commands ----------------
|
|
1939
|
+
# supports ;, &&, || (real short-circuit)
|
|
1940
|
+
|
|
1941
|
+
def split_commands_ops(input)
|
|
1942
|
+
return [] if input.nil?
|
|
1943
|
+
|
|
1944
|
+
tokens = []
|
|
1945
|
+
buf = +""
|
|
1946
|
+
in_single = false
|
|
1947
|
+
in_double = false
|
|
1948
|
+
escaped = false
|
|
1949
|
+
i = 0
|
|
1950
|
+
|
|
1951
|
+
push = lambda do |op|
|
|
1952
|
+
cmd = buf.strip
|
|
1953
|
+
tokens << [op, cmd] unless cmd.empty?
|
|
1954
|
+
buf = +""
|
|
1955
|
+
end
|
|
1956
|
+
|
|
1957
|
+
current_op = :seq
|
|
1958
|
+
|
|
1959
|
+
while i < input.length
|
|
1960
|
+
ch = input[i]
|
|
1961
|
+
|
|
1962
|
+
if escaped
|
|
1963
|
+
buf << ch
|
|
1964
|
+
escaped = false
|
|
1965
|
+
elsif ch == '\\'
|
|
1966
|
+
escaped = true
|
|
1967
|
+
buf << ch
|
|
1968
|
+
elsif ch == "'" && !in_double
|
|
1969
|
+
in_single = !in_single
|
|
1970
|
+
buf << ch
|
|
1971
|
+
elsif ch == '"' && !in_single
|
|
1972
|
+
in_double = !in_double
|
|
1973
|
+
buf << ch
|
|
1974
|
+
elsif !in_single && !in_double
|
|
1975
|
+
if ch == ';'
|
|
1976
|
+
push.call(current_op)
|
|
1977
|
+
current_op = :seq
|
|
1978
|
+
elsif ch == '&' && input[i+1] == '&'
|
|
1979
|
+
push.call(current_op)
|
|
1980
|
+
current_op = :and
|
|
1981
|
+
i += 1
|
|
1982
|
+
elsif ch == '|' && input[i+1] == '|'
|
|
1983
|
+
push.call(current_op)
|
|
1984
|
+
current_op = :or
|
|
1985
|
+
i += 1
|
|
1986
|
+
else
|
|
1987
|
+
buf << ch
|
|
1988
|
+
end
|
|
1989
|
+
else
|
|
1990
|
+
buf << ch
|
|
1991
|
+
end
|
|
1992
|
+
|
|
1993
|
+
i += 1
|
|
1994
|
+
end
|
|
1995
|
+
|
|
1996
|
+
push.call(current_op)
|
|
1997
|
+
tokens
|
|
1998
|
+
end
|
|
1999
|
+
|
|
2000
|
+
def run_input_line(input)
|
|
2001
|
+
split_commands_ops(input).each do |op, cmd|
|
|
2002
|
+
case op
|
|
2003
|
+
when :seq
|
|
2004
|
+
run_command(cmd)
|
|
2005
|
+
when :and
|
|
2006
|
+
run_command(cmd) if $last_status == 0
|
|
2007
|
+
when :or
|
|
2008
|
+
run_command(cmd) if $last_status != 0
|
|
2009
|
+
end
|
|
2010
|
+
end
|
|
2011
|
+
end
|
|
2012
|
+
|
|
2013
|
+
# ---------------- Prompt ----------------
|
|
2014
|
+
|
|
2015
|
+
def prompt(hostname)
|
|
2016
|
+
"#{color(Dir.pwd, t(:prompt_path))} #{color(hostname, t(:prompt_host))}#{color(' > ', t(:prompt_mark))}"
|
|
2017
|
+
end
|
|
2018
|
+
|
|
2019
|
+
# ---------------- Ghost + Completion Helpers ----------------
|
|
2020
|
+
|
|
2021
|
+
def history_ghost_for(line)
|
|
2022
|
+
return nil if line.nil? || line.empty?
|
|
2023
|
+
HISTORY.reverse_each do |h|
|
|
2024
|
+
next if h.nil? || h.empty?
|
|
2025
|
+
next if h.start_with?("[completions:")
|
|
2026
|
+
next unless h.start_with?(line)
|
|
2027
|
+
next if h == line
|
|
2028
|
+
return h
|
|
2029
|
+
end
|
|
2030
|
+
nil
|
|
2031
|
+
end
|
|
2032
|
+
|
|
2033
|
+
# exec cache to keep tab completion fast
|
|
2034
|
+
class ExecCache
|
|
2035
|
+
def initialize
|
|
2036
|
+
@path = nil
|
|
2037
|
+
@entries = []
|
|
2038
|
+
@built_at = 0.0
|
|
2039
|
+
end
|
|
2040
|
+
|
|
2041
|
+
def list
|
|
2042
|
+
p = ENV['PATH'].to_s
|
|
2043
|
+
now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
2044
|
+
if @path != p || (now - @built_at) > 2.5
|
|
2045
|
+
@path = p
|
|
2046
|
+
@built_at = now
|
|
2047
|
+
@entries = build_execs(p)
|
|
2048
|
+
end
|
|
2049
|
+
@entries
|
|
2050
|
+
end
|
|
2051
|
+
|
|
2052
|
+
private
|
|
2053
|
+
|
|
2054
|
+
def build_execs(path)
|
|
2055
|
+
out = []
|
|
2056
|
+
path.split(':').each do |dir|
|
|
2057
|
+
next if dir.nil? || dir.empty?
|
|
2058
|
+
begin
|
|
2059
|
+
Dir.children(dir).each do |entry|
|
|
2060
|
+
full = File.join(dir, entry)
|
|
2061
|
+
next if File.directory?(full)
|
|
2062
|
+
next unless File.executable?(full)
|
|
2063
|
+
out << entry
|
|
2064
|
+
end
|
|
2065
|
+
rescue
|
|
2066
|
+
end
|
|
2067
|
+
end
|
|
2068
|
+
out.uniq
|
|
2069
|
+
end
|
|
2070
|
+
end
|
|
2071
|
+
|
|
2072
|
+
$exec_cache = ExecCache.new
|
|
2073
|
+
|
|
2074
|
+
def tab_completions_for(prefix, first_word, at_first_word)
|
|
2075
|
+
prefix ||= ""
|
|
2076
|
+
|
|
2077
|
+
dir = "."
|
|
2078
|
+
base = prefix
|
|
2079
|
+
|
|
2080
|
+
if prefix.include?('/')
|
|
2081
|
+
if prefix.end_with?('/')
|
|
2082
|
+
dir = prefix.chomp('/')
|
|
2083
|
+
base = ""
|
|
2084
|
+
else
|
|
2085
|
+
dir = File.dirname(prefix)
|
|
2086
|
+
base = File.basename(prefix)
|
|
2087
|
+
end
|
|
2088
|
+
dir = "." if dir.nil? || dir.empty?
|
|
2089
|
+
end
|
|
2090
|
+
|
|
2091
|
+
file_completions = []
|
|
2092
|
+
if Dir.exist?(dir)
|
|
2093
|
+
Dir.children(dir).each do |entry|
|
|
2094
|
+
next unless entry.start_with?(base)
|
|
2095
|
+
full = File.join(dir, entry)
|
|
2096
|
+
|
|
2097
|
+
rel = if dir == "."
|
|
2098
|
+
entry
|
|
2099
|
+
else
|
|
2100
|
+
File.join(File.dirname(prefix), entry)
|
|
2101
|
+
end
|
|
2102
|
+
|
|
2103
|
+
case first_word
|
|
2104
|
+
when "cd"
|
|
2105
|
+
next unless File.directory?(full)
|
|
2106
|
+
rel = rel + "/" unless rel.end_with?("/")
|
|
2107
|
+
file_completions << rel
|
|
2108
|
+
when "cat"
|
|
2109
|
+
next unless File.file?(full)
|
|
2110
|
+
file_completions << rel
|
|
2111
|
+
else
|
|
2112
|
+
rel = rel + "/" if File.directory?(full) && !rel.end_with?("/")
|
|
2113
|
+
file_completions << rel
|
|
2114
|
+
end
|
|
2115
|
+
end
|
|
2116
|
+
end
|
|
2117
|
+
|
|
2118
|
+
exec_completions = []
|
|
2119
|
+
if first_word != "cat" && first_word != "cd" && at_first_word && !prefix.include?('/')
|
|
2120
|
+
exec_completions = $exec_cache.list.grep(/^#{Regexp.escape(prefix)}/)
|
|
2121
|
+
end
|
|
2122
|
+
|
|
2123
|
+
(file_completions + exec_completions).uniq
|
|
2124
|
+
end
|
|
2125
|
+
|
|
2126
|
+
def longest_common_prefix(strings)
|
|
2127
|
+
return "" if strings.empty?
|
|
2128
|
+
shortest = strings.min_by(&:length)
|
|
2129
|
+
shortest.length.times do |i|
|
|
2130
|
+
c = shortest[i]
|
|
2131
|
+
strings.each { |s| return shortest[0...i] if s[i] != c }
|
|
2132
|
+
end
|
|
2133
|
+
shortest
|
|
2134
|
+
end
|
|
2135
|
+
|
|
2136
|
+
def render_line(prompt_str, buffer, cursor, show_ghost = true)
|
|
2137
|
+
buffer = buffer || ""
|
|
2138
|
+
cursor = [[cursor, 0].max, buffer.length].min
|
|
2139
|
+
|
|
2140
|
+
ghost_tail = ""
|
|
2141
|
+
if show_ghost && cursor == buffer.length
|
|
2142
|
+
suggestion = history_ghost_for(buffer)
|
|
2143
|
+
ghost_tail = suggestion ? suggestion[buffer.length..-1].to_s : ""
|
|
2144
|
+
end
|
|
2145
|
+
|
|
2146
|
+
width = terminal_width
|
|
2147
|
+
prompt_vis = strip_ansi(prompt_str).length
|
|
2148
|
+
total_vis = prompt_vis + buffer.length + ghost_tail.length
|
|
2149
|
+
rows = [(total_vis.to_f / width).ceil, 1].max
|
|
2150
|
+
|
|
2151
|
+
# Clear previous render block (only what we drew last time)
|
|
2152
|
+
if $last_render_rows && $last_render_rows > 0
|
|
2153
|
+
STDOUT.print("\r")
|
|
2154
|
+
($last_render_rows - 1).times { STDOUT.print("\e[1A\r") }
|
|
2155
|
+
$last_render_rows.times do |i|
|
|
2156
|
+
STDOUT.print("\e[0K")
|
|
2157
|
+
STDOUT.print("\n") if i < $last_render_rows - 1
|
|
2158
|
+
end
|
|
2159
|
+
($last_render_rows - 1).times { STDOUT.print("\e[1A\r") }
|
|
2160
|
+
end
|
|
2161
|
+
|
|
2162
|
+
STDOUT.print("\r")
|
|
2163
|
+
STDOUT.print(prompt_str)
|
|
2164
|
+
STDOUT.print(buffer)
|
|
2165
|
+
|
|
2166
|
+
# explicit gray, not "dim" (fixes Konsole/Breeze showing white)
|
|
2167
|
+
STDOUT.print(color(ghost_tail, t(:dim))) unless ghost_tail.empty?
|
|
2168
|
+
|
|
2169
|
+
move_left = ghost_tail.length + (buffer.length - cursor)
|
|
2170
|
+
STDOUT.print("\e[#{move_left}D") if move_left > 0
|
|
2171
|
+
STDOUT.flush
|
|
2172
|
+
|
|
2173
|
+
$last_render_rows = rows
|
|
2174
|
+
end
|
|
2175
|
+
|
|
2176
|
+
# --------- MULTI-COLUMN TAB LIST ----------
|
|
2177
|
+
def print_tab_list(comps)
|
|
2178
|
+
return if comps.empty?
|
|
2179
|
+
|
|
2180
|
+
width = terminal_width
|
|
2181
|
+
max_len = comps.map(&:length).max || 0
|
|
2182
|
+
col_width = [max_len + 2, 4].max
|
|
2183
|
+
cols = [width / col_width, 1].max
|
|
2184
|
+
rows = (comps.length.to_f / cols).ceil
|
|
2185
|
+
|
|
2186
|
+
STDOUT.print("\r\n")
|
|
2187
|
+
rows.times do |r|
|
|
2188
|
+
line = ""
|
|
2189
|
+
cols.times do |c|
|
|
2190
|
+
idx = c * rows + r
|
|
2191
|
+
break if idx >= comps.length
|
|
2192
|
+
item = comps[idx]
|
|
2193
|
+
padding = col_width - item.length
|
|
2194
|
+
line << item << (" " * padding)
|
|
2195
|
+
end
|
|
2196
|
+
STDOUT.print("\r")
|
|
2197
|
+
STDOUT.print(line.rstrip)
|
|
2198
|
+
STDOUT.print("\n")
|
|
2199
|
+
end
|
|
2200
|
+
STDOUT.print("\r\n")
|
|
2201
|
+
STDOUT.flush
|
|
2202
|
+
end
|
|
2203
|
+
|
|
2204
|
+
def handle_tab_completion(prompt_str, buffer, cursor, last_tab_prefix)
|
|
2205
|
+
buffer = buffer || ""
|
|
2206
|
+
cursor = [[cursor, 0].max, buffer.length].min
|
|
2207
|
+
|
|
2208
|
+
wstart = buffer.rindex(/[ \t]/, cursor - 1) || -1
|
|
2209
|
+
wstart += 1
|
|
2210
|
+
prefix = buffer[wstart...cursor] || ""
|
|
2211
|
+
|
|
2212
|
+
before_word = buffer[0...wstart]
|
|
2213
|
+
at_first_word = before_word.strip.empty?
|
|
2214
|
+
first_word = buffer.strip.split(/\s+/, 2)[0] || ""
|
|
2215
|
+
|
|
2216
|
+
comps = tab_completions_for(prefix, first_word, at_first_word)
|
|
2217
|
+
return [buffer, cursor, nil, false] if comps.empty?
|
|
2218
|
+
|
|
2219
|
+
if comps.size == 1
|
|
2220
|
+
new_word = comps.first
|
|
2221
|
+
buffer = buffer[0...wstart] + new_word + buffer[cursor..-1].to_s
|
|
2222
|
+
cursor = wstart + new_word.length
|
|
2223
|
+
return [buffer, cursor, nil, true]
|
|
2224
|
+
end
|
|
2225
|
+
|
|
2226
|
+
if prefix != last_tab_prefix
|
|
2227
|
+
lcp = longest_common_prefix(comps)
|
|
2228
|
+
if lcp && lcp.length > prefix.length
|
|
2229
|
+
buffer = buffer[0...wstart] + lcp + buffer[cursor..-1].to_s
|
|
2230
|
+
cursor = wstart + lcp.length
|
|
2231
|
+
else
|
|
2232
|
+
STDOUT.print("\a")
|
|
2233
|
+
end
|
|
2234
|
+
return [buffer, cursor, prefix, false]
|
|
2235
|
+
end
|
|
2236
|
+
|
|
2237
|
+
render_line(prompt_str, buffer, cursor, false)
|
|
2238
|
+
print_tab_list(comps)
|
|
2239
|
+
[buffer, cursor, prefix, true]
|
|
2240
|
+
end
|
|
2241
|
+
|
|
2242
|
+
def read_line_with_ghost(prompt_str)
|
|
2243
|
+
buffer = ""
|
|
2244
|
+
cursor = 0
|
|
2245
|
+
hist_index = HISTORY.length
|
|
2246
|
+
saved_line_for_history = ""
|
|
2247
|
+
last_tab_prefix = nil
|
|
2248
|
+
|
|
2249
|
+
render_line(prompt_str, buffer, cursor)
|
|
2250
|
+
|
|
2251
|
+
status = :ok
|
|
2252
|
+
|
|
2253
|
+
IO.console.raw do |io|
|
|
2254
|
+
loop do
|
|
2255
|
+
ch = io.getch
|
|
2256
|
+
|
|
2257
|
+
case ch
|
|
2258
|
+
when "\r", "\n"
|
|
2259
|
+
cursor = buffer.length
|
|
2260
|
+
render_line(prompt_str, buffer, cursor, false)
|
|
2261
|
+
STDOUT.print("\r\n")
|
|
2262
|
+
STDOUT.flush
|
|
2263
|
+
break
|
|
2264
|
+
|
|
2265
|
+
when "\u0003" # Ctrl-C
|
|
2266
|
+
STDOUT.print("^C\r\n")
|
|
2267
|
+
STDOUT.flush
|
|
2268
|
+
status = :interrupt
|
|
2269
|
+
buffer = ""
|
|
2270
|
+
break
|
|
2271
|
+
|
|
2272
|
+
when "\u0004" # Ctrl-D
|
|
2273
|
+
if buffer.empty?
|
|
2274
|
+
status = :eof
|
|
2275
|
+
buffer = nil
|
|
2276
|
+
STDOUT.print("\r\n")
|
|
2277
|
+
STDOUT.flush
|
|
2278
|
+
break
|
|
2279
|
+
end
|
|
2280
|
+
|
|
2281
|
+
when "\u0001" # Ctrl-A
|
|
2282
|
+
cursor = 0
|
|
2283
|
+
last_tab_prefix = nil
|
|
2284
|
+
|
|
2285
|
+
when "\u0005" # Ctrl-E
|
|
2286
|
+
cursor = buffer.length
|
|
2287
|
+
last_tab_prefix = nil
|
|
2288
|
+
|
|
2289
|
+
when "\u007F", "\b" # Backspace
|
|
2290
|
+
if cursor > 0
|
|
2291
|
+
buffer.slice!(cursor - 1)
|
|
2292
|
+
cursor -= 1
|
|
2293
|
+
end
|
|
2294
|
+
last_tab_prefix = nil
|
|
2295
|
+
|
|
2296
|
+
when "\t" # Tab completion
|
|
2297
|
+
buffer, cursor, last_tab_prefix, printed =
|
|
2298
|
+
handle_tab_completion(prompt_str, buffer, cursor, last_tab_prefix)
|
|
2299
|
+
$last_render_rows = 1 if printed
|
|
2300
|
+
|
|
2301
|
+
when "\e" # Escape sequences (arrows, home/end)
|
|
2302
|
+
seq1 = io.getch
|
|
2303
|
+
seq2 = io.getch
|
|
2304
|
+
if seq1 == "[" && seq2
|
|
2305
|
+
case seq2
|
|
2306
|
+
when "A" # Up
|
|
2307
|
+
if hist_index == HISTORY.length
|
|
2308
|
+
saved_line_for_history = buffer.dup
|
|
2309
|
+
end
|
|
2310
|
+
if hist_index > 0
|
|
2311
|
+
hist_index -= 1
|
|
2312
|
+
buffer = HISTORY[hist_index] || ""
|
|
2313
|
+
cursor = buffer.length
|
|
2314
|
+
end
|
|
2315
|
+
when "B" # Down
|
|
2316
|
+
if hist_index < HISTORY.length - 1
|
|
2317
|
+
hist_index += 1
|
|
2318
|
+
buffer = HISTORY[hist_index] || ""
|
|
2319
|
+
cursor = buffer.length
|
|
2320
|
+
elsif hist_index == HISTORY.length - 1
|
|
2321
|
+
hist_index = HISTORY.length
|
|
2322
|
+
buffer = saved_line_for_history || ""
|
|
2323
|
+
cursor = buffer.length
|
|
2324
|
+
end
|
|
2325
|
+
when "C" # Right
|
|
2326
|
+
if cursor < buffer.length
|
|
2327
|
+
cursor += 1
|
|
2328
|
+
else
|
|
2329
|
+
suggestion = history_ghost_for(buffer)
|
|
2330
|
+
if suggestion
|
|
2331
|
+
buffer = suggestion
|
|
2332
|
+
cursor = buffer.length
|
|
2333
|
+
end
|
|
2334
|
+
end
|
|
2335
|
+
when "D" # Left
|
|
2336
|
+
cursor -= 1 if cursor > 0
|
|
2337
|
+
when "H" # Home
|
|
2338
|
+
cursor = 0
|
|
2339
|
+
when "F" # End
|
|
2340
|
+
cursor = buffer.length
|
|
2341
|
+
end
|
|
2342
|
+
end
|
|
2343
|
+
last_tab_prefix = nil
|
|
2344
|
+
|
|
2345
|
+
else
|
|
2346
|
+
if ch.ord >= 32 && ch.ord != 127
|
|
2347
|
+
buffer.insert(cursor, ch)
|
|
2348
|
+
cursor += 1
|
|
2349
|
+
hist_index = HISTORY.length
|
|
2350
|
+
last_tab_prefix = nil
|
|
2351
|
+
end
|
|
2352
|
+
end
|
|
2353
|
+
|
|
2354
|
+
render_line(prompt_str, buffer, cursor) if status == :ok
|
|
2355
|
+
end
|
|
2356
|
+
end
|
|
2357
|
+
|
|
2358
|
+
[status, buffer]
|
|
2359
|
+
end
|
|
2360
|
+
|
|
2361
|
+
# ---------------- Welcome ----------------
|
|
2362
|
+
def print_welcome
|
|
2363
|
+
puts color("Welcome to srsh #{SRSH_VERSION}", t(:ui_hdr))
|
|
2364
|
+
puts ui("Time:", :ui_key) + " " + color(current_time, t(:ui_val))
|
|
2365
|
+
puts cpu_info
|
|
2366
|
+
puts ram_info
|
|
2367
|
+
puts storage_info
|
|
2368
|
+
puts dynamic_quote
|
|
2369
|
+
puts
|
|
2370
|
+
puts color("Coded by https://github.com/RobertFlexx", t(:dim))
|
|
2371
|
+
puts
|
|
2372
|
+
end
|
|
2373
|
+
|
|
2374
|
+
# ---------------- Startup: source rc + plugins ----------------
|
|
2375
|
+
begin
|
|
2376
|
+
rsh_run_script(RC_FILE, []) if File.exist?(RC_FILE)
|
|
2377
|
+
rescue => e
|
|
2378
|
+
STDERR.puts color("srshrc: #{e.class}: #{e.message}", t(:err))
|
|
2379
|
+
end
|
|
2380
|
+
|
|
2381
|
+
load_plugins!
|
|
2382
|
+
|
|
2383
|
+
# ---------------- Script vs interactive entry ----------------
|
|
2384
|
+
if ARGV[0]
|
|
2385
|
+
script_path = ARGV.shift
|
|
2386
|
+
begin
|
|
2387
|
+
rsh_run_script(script_path, ARGV)
|
|
2388
|
+
rescue => e
|
|
2389
|
+
STDERR.puts color("rsh script error: #{e.class}: #{e.message}", t(:err))
|
|
2390
|
+
end
|
|
2391
|
+
exit 0
|
|
2392
|
+
end
|
|
2393
|
+
|
|
2394
|
+
print_welcome
|
|
2395
|
+
|
|
2396
|
+
# ---------------- Main Loop ----------------
|
|
2397
|
+
hostname = Socket.gethostname
|
|
2398
|
+
|
|
2399
|
+
loop do
|
|
2400
|
+
print "\033]0;srsh-#{SRSH_VERSION}\007"
|
|
2401
|
+
prompt_str = prompt(hostname)
|
|
2402
|
+
|
|
2403
|
+
status, input = read_line_with_ghost(prompt_str)
|
|
2404
|
+
|
|
2405
|
+
break if status == :eof
|
|
2406
|
+
next if status == :interrupt
|
|
2407
|
+
|
|
2408
|
+
next if input.nil?
|
|
2409
|
+
input = input.strip
|
|
2410
|
+
next if input.empty?
|
|
2411
|
+
|
|
2412
|
+
HISTORY << input
|
|
2413
|
+
HISTORY.shift while HISTORY.length > HISTORY_MAX
|
|
2414
|
+
|
|
2415
|
+
run_input_line(input)
|
|
2416
|
+
end
|