srsh 0.8.0 → 1.0.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 +12 -3
- data/README.md +446 -8
- data/bin/srsh +71 -0
- data/docs/assets/slut.txt +4 -0
- data/docs/assets/srsh-mark.svg +12 -0
- data/docs/css/style.css +696 -0
- data/docs/index.html +703 -0
- data/docs/js/app.js +203 -0
- data/examples/bridge.rsh +8 -0
- data/examples/calculator.rsh +253 -0
- data/examples/defer.rsh +14 -0
- data/examples/hot.rsh +14 -0
- data/examples/meta.rsh +20 -0
- data/examples/modules/text.rsh +6 -0
- data/examples/modules.rsh +6 -0
- data/examples/paste.rsh +15 -0
- data/examples/plugin.rb +8 -0
- data/examples/power.rsh +65 -0
- data/examples/tour.rsh +38 -0
- data/ext/srsh_native/extconf.rb +3 -0
- data/ext/srsh_native/srsh_native.c +48 -0
- data/language-docs/LANGUAGE.md +670 -0
- data/language-docs/MIGRATION.md +44 -0
- data/language-docs/SECURITY.md +44 -0
- data/lib/srsh/app.rb +261 -0
- data/lib/srsh/builtins.rb +492 -0
- data/lib/srsh/editor.rb +530 -0
- data/lib/srsh/errors.rb +23 -0
- data/lib/srsh/history.rb +74 -0
- data/lib/srsh/language/evaluator.rb +1175 -0
- data/lib/srsh/language/lexer.rb +316 -0
- data/lib/srsh/language/parser.rb +997 -0
- data/lib/srsh/language/token.rb +5 -0
- data/lib/srsh/language/values.rb +392 -0
- data/lib/srsh/paths.rb +29 -0
- data/lib/srsh/plugins.rb +59 -0
- data/lib/srsh/process_identity.rb +38 -0
- data/lib/srsh/security.rb +38 -0
- data/lib/srsh/shell/executor.rb +1182 -0
- data/lib/srsh/shell/job.rb +101 -0
- data/lib/srsh/shell/lexer.rb +114 -0
- data/lib/srsh/shell/terminal.rb +26 -0
- data/lib/srsh/state.rb +136 -0
- data/lib/srsh/theme.rb +108 -0
- data/lib/srsh/version.rb +3 -0
- data/lib/srsh.rb +11 -5
- metadata +61 -14
- data/exe/srsh +0 -6
- data/lib/srsh/runner.rb +0 -2416
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
require 'fileutils'
|
|
2
|
+
require 'socket'
|
|
3
|
+
require 'etc'
|
|
4
|
+
require 'rbconfig'
|
|
5
|
+
require 'io/console'
|
|
6
|
+
|
|
7
|
+
module Srsh
|
|
8
|
+
class Builtins
|
|
9
|
+
def initialize(app)
|
|
10
|
+
@app = app
|
|
11
|
+
@table = {}
|
|
12
|
+
@dir_stack = []
|
|
13
|
+
install
|
|
14
|
+
@core_table = @table.dup.freeze
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def register(name, &block) = @table[name.to_s] = block
|
|
18
|
+
def key?(name) = @table.key?(name.to_s)
|
|
19
|
+
def names = @table.keys
|
|
20
|
+
def reset_dynamic! = @table = @core_table.dup
|
|
21
|
+
def call(name, args) = @table.fetch(name).call(args)
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def install
|
|
26
|
+
register('cd') { |a| cd(a) }
|
|
27
|
+
register('pwd') { |_a| @app.out.puts @app.theme.paint(Dir.pwd, :key, io: @app.out); 0 }
|
|
28
|
+
register('put') { |a| @app.out.puts(a[1..].join(' ')); 0 }
|
|
29
|
+
register('ls') { |a| ls_builtin(a) }
|
|
30
|
+
register('echo') { |a| @app.out.puts(a[1..].join(' ')); 0 }
|
|
31
|
+
register('printf') { |a| printf_builtin(a) }
|
|
32
|
+
register('alias') { |a| alias_builtin(a) }
|
|
33
|
+
register('unalias') { |a| unalias_builtin(a) }
|
|
34
|
+
register('set') { |a| set_builtin(a) }
|
|
35
|
+
register('export') { |a| export_builtin(a) }
|
|
36
|
+
register('option') { |a| option_builtin(a) }
|
|
37
|
+
register('unset') { |a| ENV.delete(a[1].to_s); a[1] ? 0 : 2 }
|
|
38
|
+
register('read') { |a| read_builtin(a) }
|
|
39
|
+
register('true') { |_a| 0 }
|
|
40
|
+
register('false') { |_a| 1 }
|
|
41
|
+
register('sleep') { |a| Kernel.sleep(Float(a[1] || 1)); 0 }
|
|
42
|
+
register('source') { |a| source_builtin(a) }
|
|
43
|
+
register('.') { |a| source_builtin(a) }
|
|
44
|
+
register('exit') { |a| exit(Integer(a[1] || 0)) }
|
|
45
|
+
register('quit') { |a| exit(Integer(a[1] || 0)) }
|
|
46
|
+
register('help') { |_a| help; 0 }
|
|
47
|
+
register('hist') { |_a| history; 0 }
|
|
48
|
+
register('clearhist') { |_a| @app.history.clear; 0 }
|
|
49
|
+
register('scheme') { |a| scheme(a) }
|
|
50
|
+
register('theme') { |a| scheme(a) }
|
|
51
|
+
register('themes') { |_a| scheme(['scheme', '--list']) }
|
|
52
|
+
register('plugins') { |_a| plugins; 0 }
|
|
53
|
+
register('reload') { |_a| @app.reload!; 0 }
|
|
54
|
+
register('jobs') { |_a| jobs; 0 }
|
|
55
|
+
register('wait') { |a| @app.executor.wait_job(a[1]) }
|
|
56
|
+
register('fg') { |a| @app.executor.foreground_job(a[1]) }
|
|
57
|
+
register('bg') { |a| @app.executor.background_job(a[1]) }
|
|
58
|
+
register('exec') { |a| exec_builtin(a) }
|
|
59
|
+
register('systemfetch') { |_a| systemfetch; 0 }
|
|
60
|
+
register('which') { |a| which(a) }
|
|
61
|
+
register('type') { |a| which(a) }
|
|
62
|
+
register('dirs') { |_a| dirs_builtin }
|
|
63
|
+
register('pushd') { |a| pushd_builtin(a) }
|
|
64
|
+
register('popd') { |_a| popd_builtin }
|
|
65
|
+
register('umask') { |a| umask_builtin(a) }
|
|
66
|
+
register('kill') { |a| kill_builtin(a) }
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def exec_builtin(args)
|
|
71
|
+
if args.length < 2
|
|
72
|
+
@app.err.puts 'exec: usage: exec COMMAND [ARG ...]'
|
|
73
|
+
return 2
|
|
74
|
+
end
|
|
75
|
+
path = @app.executor.find_executable(args[1])
|
|
76
|
+
unless path
|
|
77
|
+
@app.err.puts "exec: command not found: #{args[1]}"
|
|
78
|
+
return 127
|
|
79
|
+
end
|
|
80
|
+
Kernel.exec(path, *args[2..])
|
|
81
|
+
rescue SystemCallError => e
|
|
82
|
+
@app.err.puts "exec: #{e.message}"
|
|
83
|
+
126
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def ls_builtin(args)
|
|
87
|
+
# Preserve the original SRSH pretty `ls` for the simple form, but hand
|
|
88
|
+
# option-heavy invocations to the system ls so normal Unix muscle memory
|
|
89
|
+
# still works.
|
|
90
|
+
if args.length > 2 || args[1].to_s.start_with?('-')
|
|
91
|
+
@app.err.puts 'ls: external ls not found'
|
|
92
|
+
return 127
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
dir = args[1] || '.'
|
|
96
|
+
entries = Dir.children(dir).sort
|
|
97
|
+
labels = entries.map do |name|
|
|
98
|
+
full = File.join(dir, name)
|
|
99
|
+
if File.directory?(full)
|
|
100
|
+
@app.theme.paint("#{name}/", :key, io: @app.out)
|
|
101
|
+
elsif File.executable?(full)
|
|
102
|
+
@app.theme.paint("#{name}*", :ok, io: @app.out)
|
|
103
|
+
else
|
|
104
|
+
name
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
print_columns(labels)
|
|
108
|
+
0
|
|
109
|
+
rescue SystemCallError => e
|
|
110
|
+
@app.err.puts "ls: #{e.message}"
|
|
111
|
+
1
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def print_columns(labels)
|
|
115
|
+
return if labels.empty?
|
|
116
|
+
width = begin
|
|
117
|
+
IO.console&.winsize&.[](1)
|
|
118
|
+
rescue IOError, SystemCallError
|
|
119
|
+
nil
|
|
120
|
+
end
|
|
121
|
+
width = 80 unless width && width.positive?
|
|
122
|
+
plain = ->(x) { x.gsub(/\e\[[0-9;]*m/, '') }
|
|
123
|
+
max = labels.map { |x| plain.call(x).length }.max || 0
|
|
124
|
+
col_width = [max + 2, 4].max
|
|
125
|
+
cols = [width / col_width, 1].max
|
|
126
|
+
rows = (labels.length.to_f / cols).ceil
|
|
127
|
+
rows.times do |row|
|
|
128
|
+
line = +''
|
|
129
|
+
cols.times do |col|
|
|
130
|
+
idx = col * rows + row
|
|
131
|
+
break if idx >= labels.length
|
|
132
|
+
label = labels[idx]
|
|
133
|
+
line << label << (' ' * [col_width - plain.call(label).length, 0].max)
|
|
134
|
+
end
|
|
135
|
+
@app.out.puts line.rstrip
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def cd(args)
|
|
140
|
+
target = args[1] || ENV['HOME'] || Dir.home
|
|
141
|
+
old = Dir.pwd
|
|
142
|
+
Dir.chdir(File.expand_path(target))
|
|
143
|
+
ENV['OLDPWD'] = old
|
|
144
|
+
ENV['PWD'] = Dir.pwd
|
|
145
|
+
0
|
|
146
|
+
rescue SystemCallError => e
|
|
147
|
+
@app.err.puts "cd: #{e.message}"
|
|
148
|
+
1
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def alias_builtin(args)
|
|
152
|
+
if args.length == 1
|
|
153
|
+
@app.state.aliases.sort.each { |k, v| @app.out.puts "#{k}=#{v.inspect}" }
|
|
154
|
+
return 0
|
|
155
|
+
end
|
|
156
|
+
text = args[1..].join(' ')
|
|
157
|
+
name, value = text.split('=', 2)
|
|
158
|
+
if name.nil? || value.nil? || !name.match?(/\A[A-Za-z_][A-Za-z0-9_-]*\z/)
|
|
159
|
+
@app.err.puts 'alias: use alias name=command'
|
|
160
|
+
return 2
|
|
161
|
+
end
|
|
162
|
+
@app.state.aliases[name] = value
|
|
163
|
+
0
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def unalias_builtin(args)
|
|
167
|
+
return 2 unless args[1]
|
|
168
|
+
@app.state.aliases.delete(args[1])
|
|
169
|
+
0
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def set_builtin(args)
|
|
173
|
+
if args.length == 1
|
|
174
|
+
ENV.keys.sort.each { |k| @app.out.puts "#{k}=#{ENV[k]}" }
|
|
175
|
+
else
|
|
176
|
+
ENV[args[1]] = args[2..].join(' ')
|
|
177
|
+
end
|
|
178
|
+
0
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def printf_builtin(args)
|
|
182
|
+
return 0 if args.length == 1
|
|
183
|
+
format = args[1].to_s
|
|
184
|
+
values = args[2..] || []
|
|
185
|
+
# Shell printf is intentionally permissive. Convert obvious numerics but
|
|
186
|
+
# leave everything else alone so %s never surprises you.
|
|
187
|
+
cooked = values.map do |value|
|
|
188
|
+
if value.match?(/\A[-+]?\d+\z/)
|
|
189
|
+
value.to_i
|
|
190
|
+
elsif value.match?(/\A[-+]?(?:\d+\.\d*|\d*\.\d+)(?:[eE][-+]?\d+)?\z/)
|
|
191
|
+
value.to_f
|
|
192
|
+
else
|
|
193
|
+
value
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
@app.out.print(format % cooked)
|
|
197
|
+
0
|
|
198
|
+
rescue ArgumentError => e
|
|
199
|
+
@app.err.puts "printf: #{e.message}"
|
|
200
|
+
2
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def export_builtin(args)
|
|
204
|
+
if args.length == 1
|
|
205
|
+
ENV.keys.sort.each { |key| @app.out.puts "export #{key}=#{ENV[key].inspect}" }
|
|
206
|
+
return 0
|
|
207
|
+
end
|
|
208
|
+
status = 0
|
|
209
|
+
args[1..].each do |item|
|
|
210
|
+
name, value = item.split('=', 2)
|
|
211
|
+
unless name.match?(/\A[A-Za-z_][A-Za-z0-9_]*\z/)
|
|
212
|
+
@app.err.puts "export: bad name #{name.inspect}"
|
|
213
|
+
status = 2
|
|
214
|
+
next
|
|
215
|
+
end
|
|
216
|
+
ENV[name] = value.nil? ? ENV[name].to_s : value
|
|
217
|
+
end
|
|
218
|
+
status
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def option_builtin(args)
|
|
222
|
+
opts = @app.state.options
|
|
223
|
+
if args.length == 1
|
|
224
|
+
opts.keys.sort.each { |name| @app.out.puts "#{name}=#{opts[name] ? 'yes' : 'no'}" }
|
|
225
|
+
return 0
|
|
226
|
+
end
|
|
227
|
+
name = args[1].to_s
|
|
228
|
+
value = args[2]
|
|
229
|
+
if name == 'strict'
|
|
230
|
+
enabled = parse_toggle(value.nil? ? 'yes' : value)
|
|
231
|
+
opts['pipefail'] = enabled
|
|
232
|
+
opts['nounset'] = enabled
|
|
233
|
+
return 0
|
|
234
|
+
end
|
|
235
|
+
unless opts.key?(name)
|
|
236
|
+
@app.err.puts "option: unknown option #{name.inspect}"
|
|
237
|
+
return 2
|
|
238
|
+
end
|
|
239
|
+
if value.nil?
|
|
240
|
+
@app.out.puts "#{name}=#{opts[name] ? 'yes' : 'no'}"
|
|
241
|
+
else
|
|
242
|
+
opts[name] = parse_toggle(value)
|
|
243
|
+
end
|
|
244
|
+
0
|
|
245
|
+
rescue ArgumentError => e
|
|
246
|
+
@app.err.puts "option: #{e.message}"
|
|
247
|
+
2
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def parse_toggle(value)
|
|
251
|
+
case value.to_s.downcase
|
|
252
|
+
when 'yes', 'on', 'true', '1' then true
|
|
253
|
+
when 'no', 'off', 'false', '0' then false
|
|
254
|
+
else raise ArgumentError, "expected yes/no, got #{value.inspect}"
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def dirs_builtin
|
|
259
|
+
@app.out.puts ([Dir.pwd] + @dir_stack.reverse).join(' ')
|
|
260
|
+
0
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def pushd_builtin(args)
|
|
264
|
+
target = args[1] || @dir_stack.last
|
|
265
|
+
unless target
|
|
266
|
+
@app.err.puts 'pushd: no other directory'
|
|
267
|
+
return 1
|
|
268
|
+
end
|
|
269
|
+
old = Dir.pwd
|
|
270
|
+
Dir.chdir(File.expand_path(target))
|
|
271
|
+
@dir_stack << old
|
|
272
|
+
ENV['OLDPWD'] = old
|
|
273
|
+
ENV['PWD'] = Dir.pwd
|
|
274
|
+
dirs_builtin
|
|
275
|
+
rescue SystemCallError => e
|
|
276
|
+
@app.err.puts "pushd: #{e.message}"
|
|
277
|
+
1
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def popd_builtin
|
|
281
|
+
target = @dir_stack.pop
|
|
282
|
+
unless target
|
|
283
|
+
@app.err.puts 'popd: directory stack empty'
|
|
284
|
+
return 1
|
|
285
|
+
end
|
|
286
|
+
old = Dir.pwd
|
|
287
|
+
Dir.chdir(target)
|
|
288
|
+
ENV['OLDPWD'] = old
|
|
289
|
+
ENV['PWD'] = Dir.pwd
|
|
290
|
+
dirs_builtin
|
|
291
|
+
rescue SystemCallError => e
|
|
292
|
+
@app.err.puts "popd: #{e.message}"
|
|
293
|
+
1
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def umask_builtin(args)
|
|
297
|
+
if args[1].nil?
|
|
298
|
+
old = Process.umask
|
|
299
|
+
Process.umask(old)
|
|
300
|
+
@app.out.printf "%04o\n", old
|
|
301
|
+
return 0
|
|
302
|
+
end
|
|
303
|
+
text = args[1].to_s
|
|
304
|
+
raise ArgumentError, 'mask must be octal' unless text.match?(/\A[0-7]{1,4}\z/)
|
|
305
|
+
Process.umask(text.to_i(8))
|
|
306
|
+
0
|
|
307
|
+
rescue ArgumentError => e
|
|
308
|
+
@app.err.puts "umask: #{e.message}"
|
|
309
|
+
2
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def kill_builtin(args)
|
|
313
|
+
return 2 if args.length < 2
|
|
314
|
+
signal = 'TERM'
|
|
315
|
+
rest = args[1..]
|
|
316
|
+
if rest[0].start_with?('-')
|
|
317
|
+
signal = rest.shift.delete_prefix('-')
|
|
318
|
+
signal = signal.to_i if signal.match?(/\A\d+\z/)
|
|
319
|
+
end
|
|
320
|
+
status = 0
|
|
321
|
+
rest.each do |pid_text|
|
|
322
|
+
begin
|
|
323
|
+
Process.kill(signal, Integer(pid_text))
|
|
324
|
+
rescue SystemCallError, ArgumentError => e
|
|
325
|
+
@app.err.puts "kill: #{pid_text}: #{e.message}"
|
|
326
|
+
status = 1
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
status
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
def read_builtin(args)
|
|
333
|
+
return 2 unless args[1]
|
|
334
|
+
ENV[args[1]] = ($stdin.gets || '').chomp
|
|
335
|
+
0
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
def source_builtin(args)
|
|
339
|
+
return 2 unless args[1]
|
|
340
|
+
@app.run_script(args[1], args[2..] || [])
|
|
341
|
+
@app.state.last_status
|
|
342
|
+
rescue StandardError => e
|
|
343
|
+
@app.err.puts "source: #{e.message}"
|
|
344
|
+
1
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
def help
|
|
348
|
+
@app.out.puts @app.theme.paint("srsh #{Srsh::VERSION}: Simple Ruby Shell", :title, io: @app.out)
|
|
349
|
+
@app.out.puts <<~TXT
|
|
350
|
+
|
|
351
|
+
shell: cd pwd ls printf alias unalias set export unset read source
|
|
352
|
+
jobs wait fg bg exec which/type pushd popd dirs umask kill
|
|
353
|
+
shell opts: option pipefail|nounset|noclobber yes|no
|
|
354
|
+
option strict yes|no
|
|
355
|
+
info: systemfetch hist clearhist scheme/theme themes plugins reload help
|
|
356
|
+
|
|
357
|
+
RSH values: name := EXPR local binding
|
|
358
|
+
$NAME := EXPR process environment
|
|
359
|
+
= EXPR print a value
|
|
360
|
+
value |> fn value pipeline
|
|
361
|
+
$(command) process output -> value
|
|
362
|
+
|
|
363
|
+
flow: if / each / while / match / try
|
|
364
|
+
? / @ / @? / ?? short forms
|
|
365
|
+
fn name(args) function
|
|
366
|
+
::x => EXPR lambda
|
|
367
|
+
return / break / continue
|
|
368
|
+
|
|
369
|
+
structure: space name ... end namespace
|
|
370
|
+
use "file.rsh" as name module namespace
|
|
371
|
+
defer statement LIFO cleanup
|
|
372
|
+
proto / trait / slot objects + composition
|
|
373
|
+
code name ... end parsed code value
|
|
374
|
+
|
|
375
|
+
concurrency: task work(args) async function
|
|
376
|
+
&:: => EXPR spawn now
|
|
377
|
+
await / await_all / race
|
|
378
|
+
chan / atom / parallel / pmap
|
|
379
|
+
|
|
380
|
+
processes: cmd("prog", arg...) argv-safe command value
|
|
381
|
+
.result .capture .check .task
|
|
382
|
+
|
|
383
|
+
native C: bridge c from "lib.so"
|
|
384
|
+
symbol(cstr, usize) -> i32
|
|
385
|
+
end
|
|
386
|
+
cbuf(size) bounded writable memory
|
|
387
|
+
|
|
388
|
+
functional: map filter reject fold find any all count sum sort uniq
|
|
389
|
+
flat zip enumerate take drop chunk group each tap
|
|
390
|
+
partial compose
|
|
391
|
+
|
|
392
|
+
Values include lists, %[maps], ranges, interpolated "\#{...}", yes/no/void,
|
|
393
|
+
safe ?. / ?[] access, first-class functions/tasks/code/prototypes, and C
|
|
394
|
+
bridge values. `|` is a Unix process pipe; `|>` is an RSH value pipe.
|
|
395
|
+
TXT
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
def history
|
|
399
|
+
@app.history.each.with_index(1) { |line, i| @app.out.printf("%5d %s\n", i, line) }
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
def scheme(args)
|
|
403
|
+
if args[1].nil?
|
|
404
|
+
@app.out.puts @app.theme.name
|
|
405
|
+
return 0
|
|
406
|
+
end
|
|
407
|
+
if %w[-l --list].include?(args[1])
|
|
408
|
+
@app.out.puts @app.theme.names.join("\n")
|
|
409
|
+
return 0
|
|
410
|
+
end
|
|
411
|
+
return 0 if @app.theme.use(args[1])
|
|
412
|
+
@app.err.puts "scheme: unknown theme #{args[1].inspect}"
|
|
413
|
+
1
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
def plugins
|
|
417
|
+
@app.plugins.loaded.each { |p| @app.out.puts File.basename(p) }
|
|
418
|
+
0
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
def jobs
|
|
422
|
+
@app.state.prune_jobs!
|
|
423
|
+
@app.state.jobs.each do |job|
|
|
424
|
+
@app.out.puts "[#{job.id}] #{job.status.to_s.ljust(8)} #{job.command}"
|
|
425
|
+
job.notified = true if job.done?
|
|
426
|
+
end
|
|
427
|
+
0
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
def which(args)
|
|
431
|
+
if args.length < 2
|
|
432
|
+
@app.err.puts 'which: usage: which NAME [...]'
|
|
433
|
+
return 2
|
|
434
|
+
end
|
|
435
|
+
status = 0
|
|
436
|
+
args[1..].each do |name|
|
|
437
|
+
if key?(name)
|
|
438
|
+
@app.out.puts "#{name}: srsh builtin"
|
|
439
|
+
elsif @app.state.functions.key?(name)
|
|
440
|
+
kind = @app.state.functions[name].is_a?(Srsh::Language::TaskFunctionNode) ? 'task function' : 'function'
|
|
441
|
+
@app.out.puts "#{name}: srsh #{kind}"
|
|
442
|
+
elsif @app.state.prototypes.key?(name)
|
|
443
|
+
@app.out.puts "#{name}: srsh prototype"
|
|
444
|
+
elsif @app.state.traits.key?(name)
|
|
445
|
+
@app.out.puts "#{name}: srsh trait"
|
|
446
|
+
elsif (path = @app.executor.find_executable(name))
|
|
447
|
+
@app.out.puts path
|
|
448
|
+
else
|
|
449
|
+
@app.err.puts "#{name}: not found"
|
|
450
|
+
status = 1
|
|
451
|
+
end
|
|
452
|
+
end
|
|
453
|
+
status
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
def systemfetch
|
|
457
|
+
host = Socket.gethostname
|
|
458
|
+
user = ENV['USER'] || Etc.getlogin || Etc.getpwuid.name rescue 'unknown'
|
|
459
|
+
os = if File.file?('/etc/os-release')
|
|
460
|
+
match = File.read('/etc/os-release').match(/^PRETTY_NAME=(?:"(.*)"|(.*))$/)
|
|
461
|
+
match ? (match[1] || match[2]) : RbConfig::CONFIG['host_os']
|
|
462
|
+
else
|
|
463
|
+
RbConfig::CONFIG['host_os']
|
|
464
|
+
end
|
|
465
|
+
mem = if File.file?('/proc/meminfo')
|
|
466
|
+
info = File.read('/proc/meminfo').scan(/^(\w+):\s+(\d+)/).to_h.transform_values { |v| v.to_i * 1024 }
|
|
467
|
+
total = info['MemTotal'].to_i
|
|
468
|
+
avail = info['MemAvailable'].to_i
|
|
469
|
+
total.positive? ? "#{human(total - avail)} / #{human(total)}" : 'n/a'
|
|
470
|
+
else
|
|
471
|
+
'n/a'
|
|
472
|
+
end
|
|
473
|
+
@app.out.puts @app.theme.paint("#{user}@#{host}", :title, io: @app.out)
|
|
474
|
+
@app.out.puts "OS: #{os}"
|
|
475
|
+
@app.out.puts "Shell: srsh #{Srsh::VERSION}"
|
|
476
|
+
@app.out.puts "Ruby: #{RUBY_ENGINE} #{RUBY_VERSION}"
|
|
477
|
+
@app.out.puts "RAM: #{mem}"
|
|
478
|
+
0
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
def human(bytes)
|
|
482
|
+
units = %w[B KiB MiB GiB TiB]
|
|
483
|
+
value = bytes.to_f
|
|
484
|
+
unit = units.shift
|
|
485
|
+
while value >= 1024 && !units.empty?
|
|
486
|
+
value /= 1024
|
|
487
|
+
unit = units.shift
|
|
488
|
+
end
|
|
489
|
+
format('%.1f %s', value, unit)
|
|
490
|
+
end
|
|
491
|
+
end
|
|
492
|
+
end
|