rubish-gem 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.dockerignore +23 -0
- data/Dockerfile +54 -0
- data/LICENSE.txt +21 -0
- data/README.md +39 -0
- data/Rakefile +12 -0
- data/lib/rubish/arithmetic.rb +140 -0
- data/lib/rubish/ast.rb +168 -0
- data/lib/rubish/builtins/arithmetic.rb +129 -0
- data/lib/rubish/builtins/bind_readline.rb +834 -0
- data/lib/rubish/builtins/directory_stack.rb +182 -0
- data/lib/rubish/builtins/echo_printf.rb +510 -0
- data/lib/rubish/builtins/hash_directories.rb +260 -0
- data/lib/rubish/builtins/read.rb +299 -0
- data/lib/rubish/builtins/trap.rb +324 -0
- data/lib/rubish/codegen.rb +1273 -0
- data/lib/rubish/completion.rb +840 -0
- data/lib/rubish/completions/bash_helpers.rb +530 -0
- data/lib/rubish/completions/git.rb +431 -0
- data/lib/rubish/completions/help_parser.rb +453 -0
- data/lib/rubish/completions/ssh.rb +114 -0
- data/lib/rubish/config.rb +267 -0
- data/lib/rubish/data/builtin_help.rb +716 -0
- data/lib/rubish/data/completion_data.rb +53 -0
- data/lib/rubish/data/readline_config.rb +47 -0
- data/lib/rubish/data/shell_options.rb +251 -0
- data/lib/rubish/data_define.rb +65 -0
- data/lib/rubish/execution_context.rb +1124 -0
- data/lib/rubish/expansion.rb +988 -0
- data/lib/rubish/history.rb +663 -0
- data/lib/rubish/lazy_loader.rb +127 -0
- data/lib/rubish/lexer.rb +1194 -0
- data/lib/rubish/parser.rb +1167 -0
- data/lib/rubish/prompt.rb +766 -0
- data/lib/rubish/repl.rb +2267 -0
- data/lib/rubish/runtime/builtins.rb +7222 -0
- data/lib/rubish/runtime/command.rb +1153 -0
- data/lib/rubish/runtime/job.rb +153 -0
- data/lib/rubish/runtime.rb +1169 -0
- data/lib/rubish/shell_state.rb +241 -0
- data/lib/rubish/startup_profiler.rb +67 -0
- data/lib/rubish/version.rb +5 -0
- data/lib/rubish.rb +60 -0
- data/sig/rubish.rbs +4 -0
- metadata +85 -0
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rubish
|
|
4
|
+
module Builtins
|
|
5
|
+
# Signal name mapping
|
|
6
|
+
# Map signal names/numbers to canonical signal names
|
|
7
|
+
# Includes both short names (HUP) and long names (SIGHUP)
|
|
8
|
+
SIGNALS = {
|
|
9
|
+
# Pseudo-signals (shell-specific, not OS signals)
|
|
10
|
+
'EXIT' => 0,
|
|
11
|
+
'ERR' => 'ERR', # Triggered on command failure
|
|
12
|
+
'DEBUG' => 'DEBUG', # Triggered before each command
|
|
13
|
+
'RETURN' => 'RETURN', # Triggered when function/sourced script returns
|
|
14
|
+
|
|
15
|
+
# Standard signals with numeric mappings
|
|
16
|
+
'0' => 0, # EXIT
|
|
17
|
+
'1' => 'HUP', 'HUP' => 'HUP', 'SIGHUP' => 'HUP',
|
|
18
|
+
'2' => 'INT', 'INT' => 'INT', 'SIGINT' => 'INT',
|
|
19
|
+
'3' => 'QUIT', 'QUIT' => 'QUIT', 'SIGQUIT' => 'QUIT',
|
|
20
|
+
'4' => 'ILL', 'ILL' => 'ILL', 'SIGILL' => 'ILL',
|
|
21
|
+
'5' => 'TRAP', 'TRAP' => 'TRAP', 'SIGTRAP' => 'TRAP',
|
|
22
|
+
'6' => 'ABRT', 'ABRT' => 'ABRT', 'SIGABRT' => 'ABRT', 'IOT' => 'ABRT', 'SIGIOT' => 'ABRT',
|
|
23
|
+
'7' => 'EMT', 'EMT' => 'EMT', 'SIGEMT' => 'EMT',
|
|
24
|
+
'8' => 'FPE', 'FPE' => 'FPE', 'SIGFPE' => 'FPE',
|
|
25
|
+
'9' => 'KILL', 'KILL' => 'KILL', 'SIGKILL' => 'KILL',
|
|
26
|
+
'10' => 'BUS', 'BUS' => 'BUS', 'SIGBUS' => 'BUS',
|
|
27
|
+
'11' => 'SEGV', 'SEGV' => 'SEGV', 'SIGSEGV' => 'SEGV',
|
|
28
|
+
'12' => 'SYS', 'SYS' => 'SYS', 'SIGSYS' => 'SYS',
|
|
29
|
+
'13' => 'PIPE', 'PIPE' => 'PIPE', 'SIGPIPE' => 'PIPE',
|
|
30
|
+
'14' => 'ALRM', 'ALRM' => 'ALRM', 'SIGALRM' => 'ALRM',
|
|
31
|
+
'15' => 'TERM', 'TERM' => 'TERM', 'SIGTERM' => 'TERM',
|
|
32
|
+
'16' => 'URG', 'URG' => 'URG', 'SIGURG' => 'URG',
|
|
33
|
+
'17' => 'STOP', 'STOP' => 'STOP', 'SIGSTOP' => 'STOP',
|
|
34
|
+
'18' => 'TSTP', 'TSTP' => 'TSTP', 'SIGTSTP' => 'TSTP',
|
|
35
|
+
'19' => 'CONT', 'CONT' => 'CONT', 'SIGCONT' => 'CONT',
|
|
36
|
+
'20' => 'CHLD', 'CHLD' => 'CHLD', 'SIGCHLD' => 'CHLD', 'CLD' => 'CHLD', 'SIGCLD' => 'CHLD',
|
|
37
|
+
'21' => 'TTIN', 'TTIN' => 'TTIN', 'SIGTTIN' => 'TTIN',
|
|
38
|
+
'22' => 'TTOU', 'TTOU' => 'TTOU', 'SIGTTOU' => 'TTOU',
|
|
39
|
+
'23' => 'IO', 'IO' => 'IO', 'SIGIO' => 'IO', 'POLL' => 'IO', 'SIGPOLL' => 'IO',
|
|
40
|
+
'24' => 'XCPU', 'XCPU' => 'XCPU', 'SIGXCPU' => 'XCPU',
|
|
41
|
+
'25' => 'XFSZ', 'XFSZ' => 'XFSZ', 'SIGXFSZ' => 'XFSZ',
|
|
42
|
+
'26' => 'VTALRM', 'VTALRM' => 'VTALRM', 'SIGVTALRM' => 'VTALRM',
|
|
43
|
+
'27' => 'PROF', 'PROF' => 'PROF', 'SIGPROF' => 'PROF',
|
|
44
|
+
'28' => 'WINCH', 'WINCH' => 'WINCH', 'SIGWINCH' => 'WINCH',
|
|
45
|
+
'29' => 'INFO', 'INFO' => 'INFO', 'SIGINFO' => 'INFO',
|
|
46
|
+
'30' => 'USR1', 'USR1' => 'USR1', 'SIGUSR1' => 'USR1',
|
|
47
|
+
'31' => 'USR2', 'USR2' => 'USR2', 'SIGUSR2' => 'USR2'
|
|
48
|
+
}.freeze
|
|
49
|
+
|
|
50
|
+
# Signals that cannot be trapped (for error messages)
|
|
51
|
+
UNTRAPABLE_SIGNALS = %w[KILL STOP].freeze
|
|
52
|
+
|
|
53
|
+
def trap(args)
|
|
54
|
+
if args.empty?
|
|
55
|
+
# List all traps
|
|
56
|
+
@state.traps.each do |sig, cmd|
|
|
57
|
+
sig_name = signal_display_name(sig)
|
|
58
|
+
puts "trap -- #{cmd.inspect} #{sig_name}"
|
|
59
|
+
end
|
|
60
|
+
return true
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# trap -l: list signal names (bash-style format)
|
|
64
|
+
if args.first == '-l'
|
|
65
|
+
# Get unique signals sorted by number, format like bash: " 1) HUP 2) INT ..."
|
|
66
|
+
signals = Signal.list.reject { |k, _| k == 'EXIT' } # EXIT is 0, handled specially
|
|
67
|
+
by_num = signals.group_by { |_, v| v }.transform_values { |pairs| pairs.map(&:first).min }
|
|
68
|
+
sorted = by_num.sort_by { |num, _| num }
|
|
69
|
+
|
|
70
|
+
# Print in columns like bash
|
|
71
|
+
col = 0
|
|
72
|
+
sorted.each do |num, name|
|
|
73
|
+
print format('%2d) %-8s', num, name)
|
|
74
|
+
col += 1
|
|
75
|
+
if col >= 5
|
|
76
|
+
puts
|
|
77
|
+
col = 0
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
puts if col > 0
|
|
81
|
+
return true
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# trap -p [signal...]: print trap commands
|
|
85
|
+
if args.first == '-p'
|
|
86
|
+
signals = args[1..] || []
|
|
87
|
+
if signals.empty?
|
|
88
|
+
@state.traps.each do |sig, cmd|
|
|
89
|
+
sig_name = signal_display_name(sig)
|
|
90
|
+
puts "trap -- #{cmd.inspect} #{sig_name}"
|
|
91
|
+
end
|
|
92
|
+
else
|
|
93
|
+
signals.each do |sig_arg|
|
|
94
|
+
sig = normalize_signal(sig_arg)
|
|
95
|
+
next unless sig
|
|
96
|
+
|
|
97
|
+
if @state.traps.key?(sig)
|
|
98
|
+
sig_name = signal_display_name(sig)
|
|
99
|
+
puts "trap -- #{@state.traps[sig].inspect} #{sig_name}"
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
return true
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# trap command signal [signal...]
|
|
107
|
+
# trap '' signal - ignore signal
|
|
108
|
+
# trap - signal - reset to default
|
|
109
|
+
# trap -- command signal - use -- to separate options from command
|
|
110
|
+
# Skip -- if present (end of options marker)
|
|
111
|
+
if args.first == '--'
|
|
112
|
+
args = args[1..]
|
|
113
|
+
end
|
|
114
|
+
command = args.first
|
|
115
|
+
signals = args[1..]
|
|
116
|
+
|
|
117
|
+
if signals.nil? || signals.empty?
|
|
118
|
+
puts 'trap: usage: trap [-lp] [[command] signal_spec ...]'
|
|
119
|
+
return false
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
signals.each do |sig_arg|
|
|
123
|
+
sig = normalize_signal(sig_arg)
|
|
124
|
+
unless sig
|
|
125
|
+
puts "trap: #{sig_arg}: invalid signal specification"
|
|
126
|
+
next
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
if command == '-'
|
|
130
|
+
# Reset to default
|
|
131
|
+
reset_trap(sig)
|
|
132
|
+
elsif command.empty?
|
|
133
|
+
# Ignore signal
|
|
134
|
+
set_trap(sig, '')
|
|
135
|
+
else
|
|
136
|
+
# Set trap
|
|
137
|
+
set_trap(sig, command)
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
true
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Pseudo-signals that are not real OS signals
|
|
145
|
+
PSEUDO_SIGNALS = [0, 'ERR', 'DEBUG', 'RETURN'].freeze
|
|
146
|
+
|
|
147
|
+
def set_trap(sig, command)
|
|
148
|
+
# KILL and STOP cannot be trapped or ignored
|
|
149
|
+
sig_name = sig.is_a?(Integer) ? nil : sig.to_s.upcase
|
|
150
|
+
if UNTRAPABLE_SIGNALS.include?(sig_name)
|
|
151
|
+
puts "trap: #{sig_name}: cannot be trapped"
|
|
152
|
+
return false
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Store the trap command
|
|
156
|
+
@state.traps[sig] = command
|
|
157
|
+
|
|
158
|
+
# Pseudo-signals are handled by the shell, not the OS
|
|
159
|
+
return true if PSEUDO_SIGNALS.include?(sig)
|
|
160
|
+
|
|
161
|
+
# Save original handler if not already saved
|
|
162
|
+
@state.original_traps[sig] ||= Signal.trap(sig, 'DEFAULT') rescue nil
|
|
163
|
+
|
|
164
|
+
if command.empty?
|
|
165
|
+
# Ignore the signal
|
|
166
|
+
Signal.trap(sig, 'IGNORE')
|
|
167
|
+
else
|
|
168
|
+
# Set up the handler
|
|
169
|
+
# Capture signal name for RUBISH_TRAPSIG/BASH_TRAPSIG
|
|
170
|
+
sig_name = sig.is_a?(Integer) ? Signal.signame(sig) : sig.to_s.sub(/^SIG/, '')
|
|
171
|
+
Signal.trap(sig) do
|
|
172
|
+
@state.current_trapsig = sig_name
|
|
173
|
+
begin
|
|
174
|
+
@state.executor&.call(command) if @state.executor
|
|
175
|
+
ensure
|
|
176
|
+
@state.current_trapsig = ''
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
true
|
|
181
|
+
rescue ArgumentError => e
|
|
182
|
+
puts "trap: #{e.message}"
|
|
183
|
+
false
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def reset_trap(sig)
|
|
187
|
+
@state.traps.delete(sig)
|
|
188
|
+
|
|
189
|
+
# Pseudo-signals have no OS signal to reset
|
|
190
|
+
return if PSEUDO_SIGNALS.include?(sig)
|
|
191
|
+
|
|
192
|
+
# Restore original handler
|
|
193
|
+
if @state.original_traps.key?(sig)
|
|
194
|
+
Signal.trap(sig, @state.original_traps.delete(sig) || 'DEFAULT')
|
|
195
|
+
else
|
|
196
|
+
Signal.trap(sig, 'DEFAULT')
|
|
197
|
+
end
|
|
198
|
+
rescue ArgumentError => e
|
|
199
|
+
puts "trap: #{e.message}"
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def exit_traps
|
|
203
|
+
return unless @state.traps.key?(0)
|
|
204
|
+
|
|
205
|
+
@state.current_trapsig = 'EXIT'
|
|
206
|
+
begin
|
|
207
|
+
@state.executor&.call(@state.traps[0]) if @state.executor
|
|
208
|
+
ensure
|
|
209
|
+
@state.current_trapsig = ''
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def err_trap
|
|
214
|
+
return unless @state.traps.key?('ERR')
|
|
215
|
+
return if @in_err_trap # Prevent recursion
|
|
216
|
+
|
|
217
|
+
@in_err_trap = true
|
|
218
|
+
@state.current_trapsig = 'ERR'
|
|
219
|
+
begin
|
|
220
|
+
@state.executor&.call(@state.traps['ERR']) if @state.executor
|
|
221
|
+
ensure
|
|
222
|
+
@in_err_trap = false
|
|
223
|
+
@state.current_trapsig = ''
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def err_trap_set?
|
|
228
|
+
@state.traps.key?('ERR') && !@state.traps['ERR'].empty?
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def save_and_clear_err_trap
|
|
232
|
+
# Save ERR trap and clear it (for functions/subshells when errtrace is off)
|
|
233
|
+
saved = @state.traps.delete('ERR')
|
|
234
|
+
saved
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def restore_err_trap(saved)
|
|
238
|
+
# Restore a previously saved ERR trap
|
|
239
|
+
if saved
|
|
240
|
+
@state.traps['ERR'] = saved
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def debug_trap
|
|
245
|
+
return unless @state.traps.key?('DEBUG')
|
|
246
|
+
return if @in_debug_trap # Prevent recursion
|
|
247
|
+
|
|
248
|
+
@in_debug_trap = true
|
|
249
|
+
@state.current_trapsig = 'DEBUG'
|
|
250
|
+
begin
|
|
251
|
+
@state.executor&.call(@state.traps['DEBUG']) if @state.executor
|
|
252
|
+
ensure
|
|
253
|
+
@in_debug_trap = false
|
|
254
|
+
@state.current_trapsig = ''
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def debug_trap_set?
|
|
259
|
+
@state.traps.key?('DEBUG') && !@state.traps['DEBUG'].empty?
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def return_trap
|
|
263
|
+
return unless @state.traps.key?('RETURN')
|
|
264
|
+
return if @in_return_trap # Prevent recursion
|
|
265
|
+
|
|
266
|
+
@in_return_trap = true
|
|
267
|
+
@state.current_trapsig = 'RETURN'
|
|
268
|
+
begin
|
|
269
|
+
@state.executor&.call(@state.traps['RETURN']) if @state.executor
|
|
270
|
+
ensure
|
|
271
|
+
@in_return_trap = false
|
|
272
|
+
@state.current_trapsig = ''
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def return_trap_set?
|
|
277
|
+
@state.traps.key?('RETURN') && !@state.traps['RETURN'].empty?
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def save_and_clear_functrace_traps
|
|
281
|
+
# Save DEBUG and RETURN traps and clear them (for functions when functrace is off)
|
|
282
|
+
saved = {}
|
|
283
|
+
saved['DEBUG'] = @state.traps.delete('DEBUG') if @state.traps.key?('DEBUG')
|
|
284
|
+
saved['RETURN'] = @state.traps.delete('RETURN') if @state.traps.key?('RETURN')
|
|
285
|
+
saved.empty? ? nil : saved
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def restore_functrace_traps(saved)
|
|
289
|
+
# Restore previously saved DEBUG and RETURN traps
|
|
290
|
+
return unless saved
|
|
291
|
+
|
|
292
|
+
@state.traps['DEBUG'] = saved['DEBUG'] if saved['DEBUG']
|
|
293
|
+
@state.traps['RETURN'] = saved['RETURN'] if saved['RETURN']
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def clear_traps
|
|
297
|
+
@state.traps.each_key do |sig|
|
|
298
|
+
reset_trap(sig) unless PSEUDO_SIGNALS.include?(sig)
|
|
299
|
+
end
|
|
300
|
+
@state.traps.clear
|
|
301
|
+
@state.original_traps.clear
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
def normalize_signal(sig_arg)
|
|
305
|
+
sig_str = sig_arg.to_s
|
|
306
|
+
|
|
307
|
+
# Look up in SIGNALS hash (handles both numeric and named signals)
|
|
308
|
+
sig_upper = sig_str.upcase
|
|
309
|
+
result = SIGNALS[sig_upper]
|
|
310
|
+
return result if result
|
|
311
|
+
|
|
312
|
+
# For pure numeric input not in SIGNALS, return as integer
|
|
313
|
+
return sig_str.to_i if sig_str =~ /\A\d+\z/
|
|
314
|
+
|
|
315
|
+
nil
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
private
|
|
319
|
+
|
|
320
|
+
def signal_display_name(sig)
|
|
321
|
+
sig == 0 ? 'EXIT' : sig
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
end
|