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,101 @@
|
|
|
1
|
+
module Srsh
|
|
2
|
+
module Shell
|
|
3
|
+
class Job
|
|
4
|
+
WAIT_FLAGS = begin
|
|
5
|
+
flags = Process::WNOHANG | Process::WUNTRACED
|
|
6
|
+
flags |= Process.const_get(:WCONTINUED) if Process.const_defined?(:WCONTINUED)
|
|
7
|
+
flags
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
attr_accessor :id, :notified
|
|
11
|
+
attr_reader :pgid, :pids, :command, :status
|
|
12
|
+
|
|
13
|
+
def initialize(pgid:, pids:, command:, background:)
|
|
14
|
+
@pgid = pgid
|
|
15
|
+
@pids = pids.freeze
|
|
16
|
+
@command = command
|
|
17
|
+
@background = background
|
|
18
|
+
@pid_states = pids.to_h { |pid| [pid, :running] }
|
|
19
|
+
@status = :running
|
|
20
|
+
@notified = false
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def background? = @background
|
|
24
|
+
def running? = @status == :running
|
|
25
|
+
def stopped? = @status == :stopped
|
|
26
|
+
def done? = @status == :done
|
|
27
|
+
|
|
28
|
+
def mark_background! = @background = true
|
|
29
|
+
def mark_foreground! = @background = false
|
|
30
|
+
|
|
31
|
+
def mark_running!
|
|
32
|
+
@pid_states.each_key do |pid|
|
|
33
|
+
@pid_states[pid] = :running unless @pid_states[pid] == :done
|
|
34
|
+
end
|
|
35
|
+
recalculate!
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def mark_stopped!(pid = nil)
|
|
39
|
+
if pid
|
|
40
|
+
@pid_states[pid] = :stopped unless @pid_states[pid] == :done
|
|
41
|
+
else
|
|
42
|
+
@pid_states.each_key { |p| @pid_states[p] = :stopped unless @pid_states[p] == :done }
|
|
43
|
+
end
|
|
44
|
+
recalculate!
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def observe(pid, process_status)
|
|
48
|
+
return self unless @pid_states.key?(pid)
|
|
49
|
+
if process_status.stopped?
|
|
50
|
+
@pid_states[pid] = :stopped
|
|
51
|
+
elsif process_status.exited? || process_status.signaled?
|
|
52
|
+
@pid_states[pid] = :done
|
|
53
|
+
elsif process_status.respond_to?(:continued?) && process_status.continued?
|
|
54
|
+
@pid_states[pid] = :running
|
|
55
|
+
end
|
|
56
|
+
recalculate!
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def refresh!
|
|
60
|
+
return self if done?
|
|
61
|
+
|
|
62
|
+
@pids.each do |pid|
|
|
63
|
+
next if @pid_states[pid] == :done
|
|
64
|
+
begin
|
|
65
|
+
result = Process.waitpid2(pid, WAIT_FLAGS)
|
|
66
|
+
observe(*result) if result
|
|
67
|
+
rescue Errno::ECHILD
|
|
68
|
+
# No waitable child means it was reaped by the synchronous path or
|
|
69
|
+
# elsewhere in srsh. Treat it as terminal, never as magically live.
|
|
70
|
+
@pid_states[pid] = :done
|
|
71
|
+
rescue Errno::EINVAL
|
|
72
|
+
# Some libc/Ruby combinations expose a wait flag constant but reject
|
|
73
|
+
# the combination at runtime. Retry with the universally portable set.
|
|
74
|
+
begin
|
|
75
|
+
result = Process.waitpid2(pid, Process::WNOHANG | Process::WUNTRACED)
|
|
76
|
+
observe(*result) if result
|
|
77
|
+
rescue Errno::ECHILD
|
|
78
|
+
@pid_states[pid] = :done
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
recalculate!
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
def recalculate!
|
|
89
|
+
states = @pid_states.values
|
|
90
|
+
@status = if states.all? { |s| s == :done }
|
|
91
|
+
:done
|
|
92
|
+
elsif states.none? { |s| s == :running } && states.any? { |s| s == :stopped }
|
|
93
|
+
:stopped
|
|
94
|
+
else
|
|
95
|
+
:running
|
|
96
|
+
end
|
|
97
|
+
self
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
require_relative '../errors'
|
|
2
|
+
|
|
3
|
+
module Srsh
|
|
4
|
+
module Shell
|
|
5
|
+
Lexeme = Data.define(:type, :text)
|
|
6
|
+
|
|
7
|
+
class Lexer
|
|
8
|
+
OPERATORS = %w[2>> 2> >> && || | & ; > <].freeze
|
|
9
|
+
MAX_INPUT = 1024 * 1024
|
|
10
|
+
MAX_LEXEMES = 100_000
|
|
11
|
+
|
|
12
|
+
def self.scan(input)
|
|
13
|
+
new(input).scan
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def initialize(input)
|
|
17
|
+
@s = input.to_s
|
|
18
|
+
raise ParseError, 'command line too large' if @s.bytesize > MAX_INPUT
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def scan
|
|
22
|
+
out = []
|
|
23
|
+
buf = +''
|
|
24
|
+
quote = nil
|
|
25
|
+
escaped = false
|
|
26
|
+
subst_depth = 0
|
|
27
|
+
i = 0
|
|
28
|
+
|
|
29
|
+
flush = lambda do
|
|
30
|
+
unless buf.empty?
|
|
31
|
+
raise ParseError, 'command has too many tokens' if out.length >= MAX_LEXEMES
|
|
32
|
+
out << Lexeme.new(:word, buf)
|
|
33
|
+
buf = +''
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
while i < @s.length
|
|
38
|
+
c = @s[i]
|
|
39
|
+
|
|
40
|
+
if escaped
|
|
41
|
+
buf << c
|
|
42
|
+
escaped = false
|
|
43
|
+
i += 1
|
|
44
|
+
next
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
if c == '\\'
|
|
48
|
+
buf << c
|
|
49
|
+
escaped = true
|
|
50
|
+
i += 1
|
|
51
|
+
next
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
if quote
|
|
55
|
+
buf << c
|
|
56
|
+
quote = nil if c == quote
|
|
57
|
+
i += 1
|
|
58
|
+
next
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
if c == "'" || c == '"'
|
|
62
|
+
quote = c
|
|
63
|
+
buf << c
|
|
64
|
+
i += 1
|
|
65
|
+
next
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
if c == '$' && @s[i + 1] == '('
|
|
69
|
+
subst_depth += 1
|
|
70
|
+
buf << '$('
|
|
71
|
+
i += 2
|
|
72
|
+
next
|
|
73
|
+
elsif c == '(' && subst_depth.positive?
|
|
74
|
+
subst_depth += 1
|
|
75
|
+
buf << c
|
|
76
|
+
i += 1
|
|
77
|
+
next
|
|
78
|
+
elsif c == ')' && subst_depth.positive?
|
|
79
|
+
subst_depth -= 1
|
|
80
|
+
buf << c
|
|
81
|
+
i += 1
|
|
82
|
+
next
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
if subst_depth.zero? && c.match?(/\s/)
|
|
86
|
+
flush.call
|
|
87
|
+
i += 1
|
|
88
|
+
next
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
if subst_depth.zero?
|
|
92
|
+
op = OPERATORS.find { |candidate| @s[i, candidate.length] == candidate }
|
|
93
|
+
if op
|
|
94
|
+
flush.call
|
|
95
|
+
raise ParseError, 'command has too many tokens' if out.length >= MAX_LEXEMES
|
|
96
|
+
out << Lexeme.new(:op, op)
|
|
97
|
+
i += op.length
|
|
98
|
+
next
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
buf << c
|
|
103
|
+
i += 1
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
raise IncompleteInput, 'trailing backslash' if escaped
|
|
107
|
+
raise IncompleteInput, 'unterminated quote' if quote
|
|
108
|
+
raise IncompleteInput, 'unterminated command substitution' unless subst_depth.zero?
|
|
109
|
+
flush.call
|
|
110
|
+
out
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module Srsh
|
|
2
|
+
module Shell
|
|
3
|
+
module Terminal
|
|
4
|
+
module_function
|
|
5
|
+
|
|
6
|
+
begin
|
|
7
|
+
require 'fiddle/import'
|
|
8
|
+
module LibC
|
|
9
|
+
extend Fiddle::Importer
|
|
10
|
+
dlload Fiddle.dlopen(nil)
|
|
11
|
+
extern 'int tcsetpgrp(int, int)'
|
|
12
|
+
end
|
|
13
|
+
AVAILABLE = true
|
|
14
|
+
rescue LoadError, Fiddle::DLError
|
|
15
|
+
AVAILABLE = false
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def foreground(pgid, io = STDIN)
|
|
19
|
+
return false unless AVAILABLE && io.tty?
|
|
20
|
+
LibC.tcsetpgrp(io.fileno, pgid.to_i).zero?
|
|
21
|
+
rescue SystemCallError, Fiddle::DLError
|
|
22
|
+
false
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
data/lib/srsh/state.rb
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
module Srsh
|
|
2
|
+
class State
|
|
3
|
+
attr_accessor :theme_name
|
|
4
|
+
attr_reader :aliases, :functions, :prototypes, :traits, :hooks, :jobs, :owner_thread, :options
|
|
5
|
+
|
|
6
|
+
def initialize
|
|
7
|
+
@owner_thread = Thread.current
|
|
8
|
+
@last_status = 0
|
|
9
|
+
@last_bg_pid = nil
|
|
10
|
+
@theme_name = 'classic'
|
|
11
|
+
@options = { 'pipefail' => false, 'nounset' => false, 'noclobber' => false }
|
|
12
|
+
@aliases = {}
|
|
13
|
+
@functions = {}
|
|
14
|
+
@prototypes = {}
|
|
15
|
+
@traits = {}
|
|
16
|
+
@hooks = Hash.new { |h, k| h[k] = [] }
|
|
17
|
+
@root_locals = {}
|
|
18
|
+
@owner_locals = [@root_locals]
|
|
19
|
+
@scope_key = :"srsh_scopes_#{object_id}"
|
|
20
|
+
@status_key = :"srsh_status_#{object_id}"
|
|
21
|
+
@bg_key = :"srsh_bg_#{object_id}"
|
|
22
|
+
@jobs = []
|
|
23
|
+
@jobs_lock = Mutex.new
|
|
24
|
+
@next_job_id = 1
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# RSH threads share global definitions and the root interactive scope for
|
|
28
|
+
# reads, but each OS thread gets its own scope stack. Function/lambda calls
|
|
29
|
+
# therefore cannot stomp another thread's locals while closures still see
|
|
30
|
+
# the snapshot they captured.
|
|
31
|
+
def locals
|
|
32
|
+
return @owner_locals if Thread.current == @owner_thread
|
|
33
|
+
Thread.current[@scope_key] ||= [@root_locals]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def push_scope(seed = {}) = locals << seed
|
|
37
|
+
|
|
38
|
+
def pop_scope
|
|
39
|
+
stack = locals
|
|
40
|
+
stack.pop if stack.length > 1
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def local_get(name)
|
|
44
|
+
locals.reverse_each { |scope| return scope[name] if scope.key?(name) }
|
|
45
|
+
nil
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def local_defined?(name) = locals.reverse_each.any? { |s| s.key?(name) }
|
|
49
|
+
|
|
50
|
+
def locals_snapshot
|
|
51
|
+
locals.each_with_object({}) { |scope, merged| merged.merge!(scope) }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# `:=` always binds in the current lexical/function scope. Compound
|
|
55
|
+
# assignment walks outward to the nearest existing binding.
|
|
56
|
+
def local_define(name, value) = locals.last[name] = value
|
|
57
|
+
|
|
58
|
+
def local_set(name, value)
|
|
59
|
+
stack = locals
|
|
60
|
+
if worker_thread?
|
|
61
|
+
# Never let an ordinary compound assignment in a worker mutate the
|
|
62
|
+
# shared root REPL/script scope. If the binding only exists globally,
|
|
63
|
+
# shadow it in the task's current scope instead.
|
|
64
|
+
scope = stack[1..].to_a.reverse.find { |s| s.key?(name) } || stack.last
|
|
65
|
+
else
|
|
66
|
+
scope = stack.reverse.find { |s| s.key?(name) } || stack.last
|
|
67
|
+
end
|
|
68
|
+
scope[name] = value
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def last_status
|
|
72
|
+
return @last_status if Thread.current == @owner_thread
|
|
73
|
+
Thread.current[@status_key].nil? ? @last_status : Thread.current[@status_key]
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def last_status=(value)
|
|
77
|
+
if Thread.current == @owner_thread
|
|
78
|
+
@last_status = value
|
|
79
|
+
else
|
|
80
|
+
Thread.current[@status_key] = value
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def last_bg_pid
|
|
85
|
+
return @last_bg_pid if Thread.current == @owner_thread
|
|
86
|
+
Thread.current[@bg_key].nil? ? @last_bg_pid : Thread.current[@bg_key]
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def last_bg_pid=(value)
|
|
90
|
+
if Thread.current == @owner_thread
|
|
91
|
+
@last_bg_pid = value
|
|
92
|
+
else
|
|
93
|
+
Thread.current[@bg_key] = value
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def worker_thread? = Thread.current != @owner_thread
|
|
98
|
+
|
|
99
|
+
def add_job(job)
|
|
100
|
+
@jobs_lock.synchronize do
|
|
101
|
+
job.id = @next_job_id
|
|
102
|
+
@next_job_id += 1
|
|
103
|
+
@jobs << job
|
|
104
|
+
end
|
|
105
|
+
job
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def jobs_snapshot = @jobs_lock.synchronize { @jobs.dup }
|
|
109
|
+
|
|
110
|
+
def prune_jobs!
|
|
111
|
+
snapshot = jobs_snapshot
|
|
112
|
+
snapshot.each do |job|
|
|
113
|
+
next if job.done?
|
|
114
|
+
begin
|
|
115
|
+
job.refresh!
|
|
116
|
+
rescue StandardError => e
|
|
117
|
+
# Job bookkeeping is housekeeping. A platform-specific wait quirk must
|
|
118
|
+
# never tear down the user's interactive shell.
|
|
119
|
+
warn "srsh: job refresh failed for [#{job.id || '?'}]: #{e.class}: #{e.message}"
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
@jobs_lock.synchronize { @jobs.reject! { |job| job.done? && job.notified } }
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def hook(type, &block) = @hooks[type.to_sym] << block
|
|
126
|
+
def clear_hooks! = @hooks.clear
|
|
127
|
+
|
|
128
|
+
def run_hooks(type, *args)
|
|
129
|
+
@hooks[type.to_sym].each do |block|
|
|
130
|
+
block.call(*args)
|
|
131
|
+
rescue StandardError => e
|
|
132
|
+
warn "srsh hook #{type}: #{e.class}: #{e.message}"
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
data/lib/srsh/theme.rb
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
require_relative 'security'
|
|
3
|
+
|
|
4
|
+
module Srsh
|
|
5
|
+
class Theme
|
|
6
|
+
MAX_THEME_BYTES = 256 * 1024
|
|
7
|
+
ANSI_RE = /\A(?:\d{1,3})(?:;\d{1,3}){0,6}\z/
|
|
8
|
+
|
|
9
|
+
DEFAULTS = {
|
|
10
|
+
'classic' => {
|
|
11
|
+
border: '1;35', title: '1;33', key: '1;36', value: '0;37',
|
|
12
|
+
ok: '32', warn: '33', error: '31', dim: '90', path: '33', host: '36', mark: '35'
|
|
13
|
+
},
|
|
14
|
+
'mono' => {
|
|
15
|
+
border: '1;37', title: '1;37', key: '0;37', value: '0;37',
|
|
16
|
+
ok: '0;37', warn: '0;37', error: '0;37', dim: '90', path: '0;37', host: '0;37', mark: '0;37'
|
|
17
|
+
},
|
|
18
|
+
'neon' => {
|
|
19
|
+
border: '1;35', title: '1;92', key: '1;95', value: '0;37',
|
|
20
|
+
ok: '1;92', warn: '1;93', error: '1;91', dim: '90', path: '1;93', host: '1;96', mark: '1;95'
|
|
21
|
+
},
|
|
22
|
+
'ocean' => {
|
|
23
|
+
border: '1;34', title: '1;96', key: '36', value: '0;37',
|
|
24
|
+
ok: '1;92', warn: '1;93', error: '1;91', dim: '90', path: '34', host: '96', mark: '36'
|
|
25
|
+
}
|
|
26
|
+
}.freeze
|
|
27
|
+
|
|
28
|
+
attr_reader :name
|
|
29
|
+
|
|
30
|
+
def initialize(paths, state)
|
|
31
|
+
@paths = paths
|
|
32
|
+
@state = state
|
|
33
|
+
@themes = DEFAULTS.transform_values(&:dup)
|
|
34
|
+
load_user_themes
|
|
35
|
+
@base_themes = @themes.transform_values(&:dup).freeze
|
|
36
|
+
wanted = ENV['SRSH_THEME'].to_s.strip
|
|
37
|
+
wanted = File.read(paths.theme_state).strip if wanted.empty? && File.file?(paths.theme_state)
|
|
38
|
+
use(wanted.empty? ? 'classic' : wanted)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def names = @themes.keys.sort
|
|
42
|
+
|
|
43
|
+
def reset_dynamic!
|
|
44
|
+
current = @name
|
|
45
|
+
@themes = @base_themes.transform_values(&:dup)
|
|
46
|
+
use(@themes.key?(current) ? current : 'classic')
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def register(name, values)
|
|
50
|
+
clean = {}
|
|
51
|
+
values.each do |key, value|
|
|
52
|
+
value = value.to_s
|
|
53
|
+
clean[key.to_sym] = value if value.match?(ANSI_RE)
|
|
54
|
+
end
|
|
55
|
+
@themes[name.to_s] = @themes['classic'].merge(clean) unless clean.empty?
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def use(name)
|
|
59
|
+
return false unless @themes.key?(name)
|
|
60
|
+
@name = name
|
|
61
|
+
@state.theme_name = name
|
|
62
|
+
Security.atomic_write(@paths.theme_state, "#{name}\n") rescue nil
|
|
63
|
+
true
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def code(key) = @themes.fetch(@name, @themes['classic'])[key.to_sym]
|
|
67
|
+
|
|
68
|
+
def paint(text, key, io: STDOUT)
|
|
69
|
+
code = code(key)
|
|
70
|
+
color_ok = io.respond_to?(:tty?) && io.tty?
|
|
71
|
+
return text.to_s if !color_ok || code.nil?
|
|
72
|
+
"\e[#{code}m#{text}\e[0m"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def load_user_themes
|
|
78
|
+
Dir.glob(File.join(@paths.themes, '*.{theme,json}')).sort.each do |path|
|
|
79
|
+
next unless Security.private_regular_file?(path)
|
|
80
|
+
next if File.size(path) > MAX_THEME_BYTES
|
|
81
|
+
raw = File.extname(path) == '.json' ? JSON.parse(File.binread(path, MAX_THEME_BYTES + 1)) : parse_kv(path)
|
|
82
|
+
next unless raw.is_a?(Hash)
|
|
83
|
+
clean = {}
|
|
84
|
+
raw.each do |key, value|
|
|
85
|
+
value = value.to_s
|
|
86
|
+
clean[key.to_sym] = value if value.match?(ANSI_RE)
|
|
87
|
+
end
|
|
88
|
+
next if clean.empty?
|
|
89
|
+
@themes[File.basename(path, '.*')] = @themes['classic'].merge(clean)
|
|
90
|
+
rescue JSON::ParserError, SystemCallError
|
|
91
|
+
next
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def parse_kv(path)
|
|
96
|
+
data = File.binread(path, MAX_THEME_BYTES + 1)
|
|
97
|
+
return {} if data.bytesize > MAX_THEME_BYTES
|
|
98
|
+
data.force_encoding(Encoding::UTF_8)
|
|
99
|
+
return {} unless data.valid_encoding?
|
|
100
|
+
data.each_line.filter_map do |line|
|
|
101
|
+
line = line.strip
|
|
102
|
+
next if line.empty? || line.start_with?('#')
|
|
103
|
+
key, value = line.split('=', 2)
|
|
104
|
+
[key&.strip, value&.strip] if key && value
|
|
105
|
+
end.to_h
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
data/lib/srsh/version.rb
ADDED
data/lib/srsh.rb
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
begin
|
|
4
|
+
require 'srsh_native'
|
|
5
|
+
rescue LoadError
|
|
6
|
+
begin
|
|
7
|
+
require_relative '../ext/srsh_native/srsh_native'
|
|
8
|
+
rescue LoadError
|
|
9
|
+
# Optional: the complete shell remains Ruby-only when this is absent.
|
|
8
10
|
end
|
|
9
11
|
end
|
|
12
|
+
|
|
13
|
+
require_relative 'srsh/app'
|
|
14
|
+
|
|
15
|
+
require_relative 'srsh/process_identity'
|
metadata
CHANGED
|
@@ -1,32 +1,78 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: srsh
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 1.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- RobertFlexx
|
|
8
|
-
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
9
10
|
cert_chain: []
|
|
10
|
-
date:
|
|
11
|
+
date: 2026-09-02 00:00:00.000000000 Z
|
|
11
12
|
dependencies: []
|
|
12
|
-
description:
|
|
13
|
-
|
|
13
|
+
description: A Unix shell written in Ruby with a structured scripting language for
|
|
14
|
+
shell programs.
|
|
14
15
|
email:
|
|
15
|
-
- ''
|
|
16
16
|
executables:
|
|
17
17
|
- srsh
|
|
18
|
-
extensions:
|
|
18
|
+
extensions:
|
|
19
|
+
- ext/srsh_native/extconf.rb
|
|
19
20
|
extra_rdoc_files: []
|
|
20
21
|
files:
|
|
21
22
|
- LICENSE
|
|
22
23
|
- README.md
|
|
23
|
-
-
|
|
24
|
+
- bin/srsh
|
|
25
|
+
- docs/assets/slut.txt
|
|
26
|
+
- docs/assets/srsh-mark.svg
|
|
27
|
+
- docs/css/style.css
|
|
28
|
+
- docs/index.html
|
|
29
|
+
- docs/js/app.js
|
|
30
|
+
- examples/bridge.rsh
|
|
31
|
+
- examples/calculator.rsh
|
|
32
|
+
- examples/defer.rsh
|
|
33
|
+
- examples/hot.rsh
|
|
34
|
+
- examples/meta.rsh
|
|
35
|
+
- examples/modules.rsh
|
|
36
|
+
- examples/modules/text.rsh
|
|
37
|
+
- examples/paste.rsh
|
|
38
|
+
- examples/plugin.rb
|
|
39
|
+
- examples/power.rsh
|
|
40
|
+
- examples/tour.rsh
|
|
41
|
+
- ext/srsh_native/extconf.rb
|
|
42
|
+
- ext/srsh_native/srsh_native.c
|
|
43
|
+
- language-docs/LANGUAGE.md
|
|
44
|
+
- language-docs/MIGRATION.md
|
|
45
|
+
- language-docs/SECURITY.md
|
|
24
46
|
- lib/srsh.rb
|
|
25
|
-
- lib/srsh/
|
|
26
|
-
|
|
47
|
+
- lib/srsh/app.rb
|
|
48
|
+
- lib/srsh/builtins.rb
|
|
49
|
+
- lib/srsh/editor.rb
|
|
50
|
+
- lib/srsh/errors.rb
|
|
51
|
+
- lib/srsh/history.rb
|
|
52
|
+
- lib/srsh/language/evaluator.rb
|
|
53
|
+
- lib/srsh/language/lexer.rb
|
|
54
|
+
- lib/srsh/language/parser.rb
|
|
55
|
+
- lib/srsh/language/token.rb
|
|
56
|
+
- lib/srsh/language/values.rb
|
|
57
|
+
- lib/srsh/paths.rb
|
|
58
|
+
- lib/srsh/plugins.rb
|
|
59
|
+
- lib/srsh/process_identity.rb
|
|
60
|
+
- lib/srsh/security.rb
|
|
61
|
+
- lib/srsh/shell/executor.rb
|
|
62
|
+
- lib/srsh/shell/job.rb
|
|
63
|
+
- lib/srsh/shell/lexer.rb
|
|
64
|
+
- lib/srsh/shell/terminal.rb
|
|
65
|
+
- lib/srsh/state.rb
|
|
66
|
+
- lib/srsh/theme.rb
|
|
67
|
+
- lib/srsh/version.rb
|
|
68
|
+
homepage: https://github.com/RobertFlexx/RSH
|
|
27
69
|
licenses:
|
|
28
70
|
- MIT
|
|
29
|
-
metadata:
|
|
71
|
+
metadata:
|
|
72
|
+
source_code_uri: https://github.com/RobertFlexx/RSH
|
|
73
|
+
bug_tracker_uri: https://github.com/RobertFlexx/RSH/issues
|
|
74
|
+
rubygems_mfa_required: 'true'
|
|
75
|
+
post_install_message:
|
|
30
76
|
rdoc_options: []
|
|
31
77
|
require_paths:
|
|
32
78
|
- lib
|
|
@@ -34,14 +80,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
34
80
|
requirements:
|
|
35
81
|
- - ">="
|
|
36
82
|
- !ruby/object:Gem::Version
|
|
37
|
-
version: 3.
|
|
83
|
+
version: '3.2'
|
|
38
84
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
39
85
|
requirements:
|
|
40
86
|
- - ">="
|
|
41
87
|
- !ruby/object:Gem::Version
|
|
42
88
|
version: '0'
|
|
43
89
|
requirements: []
|
|
44
|
-
rubygems_version: 3.
|
|
90
|
+
rubygems_version: 3.4.19
|
|
91
|
+
signing_key:
|
|
45
92
|
specification_version: 4
|
|
46
|
-
summary:
|
|
93
|
+
summary: Simple Ruby Shell with the RSH scripting language
|
|
47
94
|
test_files: []
|