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.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/.dockerignore +23 -0
  3. data/Dockerfile +54 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +39 -0
  6. data/Rakefile +12 -0
  7. data/lib/rubish/arithmetic.rb +140 -0
  8. data/lib/rubish/ast.rb +168 -0
  9. data/lib/rubish/builtins/arithmetic.rb +129 -0
  10. data/lib/rubish/builtins/bind_readline.rb +834 -0
  11. data/lib/rubish/builtins/directory_stack.rb +182 -0
  12. data/lib/rubish/builtins/echo_printf.rb +510 -0
  13. data/lib/rubish/builtins/hash_directories.rb +260 -0
  14. data/lib/rubish/builtins/read.rb +299 -0
  15. data/lib/rubish/builtins/trap.rb +324 -0
  16. data/lib/rubish/codegen.rb +1273 -0
  17. data/lib/rubish/completion.rb +840 -0
  18. data/lib/rubish/completions/bash_helpers.rb +530 -0
  19. data/lib/rubish/completions/git.rb +431 -0
  20. data/lib/rubish/completions/help_parser.rb +453 -0
  21. data/lib/rubish/completions/ssh.rb +114 -0
  22. data/lib/rubish/config.rb +267 -0
  23. data/lib/rubish/data/builtin_help.rb +716 -0
  24. data/lib/rubish/data/completion_data.rb +53 -0
  25. data/lib/rubish/data/readline_config.rb +47 -0
  26. data/lib/rubish/data/shell_options.rb +251 -0
  27. data/lib/rubish/data_define.rb +65 -0
  28. data/lib/rubish/execution_context.rb +1124 -0
  29. data/lib/rubish/expansion.rb +988 -0
  30. data/lib/rubish/history.rb +663 -0
  31. data/lib/rubish/lazy_loader.rb +127 -0
  32. data/lib/rubish/lexer.rb +1194 -0
  33. data/lib/rubish/parser.rb +1167 -0
  34. data/lib/rubish/prompt.rb +766 -0
  35. data/lib/rubish/repl.rb +2267 -0
  36. data/lib/rubish/runtime/builtins.rb +7222 -0
  37. data/lib/rubish/runtime/command.rb +1153 -0
  38. data/lib/rubish/runtime/job.rb +153 -0
  39. data/lib/rubish/runtime.rb +1169 -0
  40. data/lib/rubish/shell_state.rb +241 -0
  41. data/lib/rubish/startup_profiler.rb +67 -0
  42. data/lib/rubish/version.rb +5 -0
  43. data/lib/rubish.rb +60 -0
  44. data/sig/rubish.rbs +4 -0
  45. 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