rush-shell 0.0.2

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 (210) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +94 -0
  4. data/exe/rush +10 -0
  5. data/grammar/shell.y +272 -0
  6. data/grammar/shell.y.output +2354 -0
  7. data/lib/rush/alias_table.rb +48 -0
  8. data/lib/rush/assignments.rb +48 -0
  9. data/lib/rush/ast/and_or.rb +37 -0
  10. data/lib/rush/ast/assignment.rb +20 -0
  11. data/lib/rush/ast/brace_group.rb +25 -0
  12. data/lib/rush/ast/case_node.rb +41 -0
  13. data/lib/rush/ast/condition_loop.rb +34 -0
  14. data/lib/rush/ast/for_node.rb +34 -0
  15. data/lib/rush/ast/function_def.rb +26 -0
  16. data/lib/rush/ast/if_node.rb +29 -0
  17. data/lib/rush/ast/list.rb +47 -0
  18. data/lib/rush/ast/node.rb +17 -0
  19. data/lib/rush/ast/param_ref.rb +59 -0
  20. data/lib/rush/ast/pipeline.rb +45 -0
  21. data/lib/rush/ast/redirect.rb +21 -0
  22. data/lib/rush/ast/redirected.rb +26 -0
  23. data/lib/rush/ast/simple_command.rb +62 -0
  24. data/lib/rush/ast/subshell.rb +24 -0
  25. data/lib/rush/ast/until_node.rb +18 -0
  26. data/lib/rush/ast/while_node.rb +18 -0
  27. data/lib/rush/ast/word.rb +44 -0
  28. data/lib/rush/ast/word_segment.rb +203 -0
  29. data/lib/rush/background_runner.rb +68 -0
  30. data/lib/rush/bracket_expression.rb +107 -0
  31. data/lib/rush/bracket_scanner.rb +64 -0
  32. data/lib/rush/builtins/alias_.rb +74 -0
  33. data/lib/rush/builtins/base.rb +96 -0
  34. data/lib/rush/builtins/break_.rb +19 -0
  35. data/lib/rush/builtins/cd.rb +127 -0
  36. data/lib/rush/builtins/colon.rb +16 -0
  37. data/lib/rush/builtins/command.rb +137 -0
  38. data/lib/rush/builtins/command_options.rb +103 -0
  39. data/lib/rush/builtins/continue_.rb +19 -0
  40. data/lib/rush/builtins/declare.rb +27 -0
  41. data/lib/rush/builtins/defaults.rb +28 -0
  42. data/lib/rush/builtins/dot.rb +52 -0
  43. data/lib/rush/builtins/echo.rb +37 -0
  44. data/lib/rush/builtins/eval.rb +25 -0
  45. data/lib/rush/builtins/exec.rb +54 -0
  46. data/lib/rush/builtins/exit.rb +45 -0
  47. data/lib/rush/builtins/export.rb +20 -0
  48. data/lib/rush/builtins/false_.rb +16 -0
  49. data/lib/rush/builtins/fd_operand.rb +87 -0
  50. data/lib/rush/builtins/getopts.rb +96 -0
  51. data/lib/rush/builtins/hash.rb +64 -0
  52. data/lib/rush/builtins/job_resume.rb +107 -0
  53. data/lib/rush/builtins/jobs.rb +86 -0
  54. data/lib/rush/builtins/kill.rb +152 -0
  55. data/lib/rush/builtins/local.rb +40 -0
  56. data/lib/rush/builtins/loop_jump.rb +45 -0
  57. data/lib/rush/builtins/printf.rb +39 -0
  58. data/lib/rush/builtins/printf_formatter.rb +111 -0
  59. data/lib/rush/builtins/pwd.rb +17 -0
  60. data/lib/rush/builtins/read.rb +93 -0
  61. data/lib/rush/builtins/read_input.rb +146 -0
  62. data/lib/rush/builtins/readonly.rb +20 -0
  63. data/lib/rush/builtins/registry.rb +36 -0
  64. data/lib/rush/builtins/return_.rb +24 -0
  65. data/lib/rush/builtins/set.rb +101 -0
  66. data/lib/rush/builtins/shift.rb +35 -0
  67. data/lib/rush/builtins/test_.rb +48 -0
  68. data/lib/rush/builtins/test_context.rb +24 -0
  69. data/lib/rush/builtins/test_expr.rb +74 -0
  70. data/lib/rush/builtins/test_grammar.rb +117 -0
  71. data/lib/rush/builtins/test_operators.rb +94 -0
  72. data/lib/rush/builtins/test_tokens.rb +69 -0
  73. data/lib/rush/builtins/times.rb +35 -0
  74. data/lib/rush/builtins/trap.rb +85 -0
  75. data/lib/rush/builtins/true_.rb +16 -0
  76. data/lib/rush/builtins/type_.rb +28 -0
  77. data/lib/rush/builtins/ulimit.rb +258 -0
  78. data/lib/rush/builtins/umask.rb +81 -0
  79. data/lib/rush/builtins/unalias.rb +46 -0
  80. data/lib/rush/builtins/unset.rb +40 -0
  81. data/lib/rush/builtins/wait.rb +127 -0
  82. data/lib/rush/cli.rb +31 -0
  83. data/lib/rush/command_assignments.rb +56 -0
  84. data/lib/rush/command_lookup.rb +228 -0
  85. data/lib/rush/command_resolution.rb +115 -0
  86. data/lib/rush/command_runner.rb +152 -0
  87. data/lib/rush/command_text.rb +140 -0
  88. data/lib/rush/environment.rb +134 -0
  89. data/lib/rush/errexit_context.rb +68 -0
  90. data/lib/rush/error_policy.rb +64 -0
  91. data/lib/rush/errors.rb +100 -0
  92. data/lib/rush/escape_table.rb +22 -0
  93. data/lib/rush/escaped_bracket.rb +49 -0
  94. data/lib/rush/executor.rb +164 -0
  95. data/lib/rush/exit_trap.rb +109 -0
  96. data/lib/rush/expansion/arithmetic/evaluator.rb +43 -0
  97. data/lib/rush/expansion/arithmetic/nodes.rb +180 -0
  98. data/lib/rush/expansion/arithmetic/number.rb +104 -0
  99. data/lib/rush/expansion/arithmetic/parser.rb +155 -0
  100. data/lib/rush/expansion/arithmetic/tokenizer.rb +56 -0
  101. data/lib/rush/expansion/arithmetic_expander.rb +38 -0
  102. data/lib/rush/expansion/command_substitution.rb +87 -0
  103. data/lib/rush/expansion/field_part.rb +12 -0
  104. data/lib/rush/expansion/field_splitter.rb +23 -0
  105. data/lib/rush/expansion/glob_expander.rb +48 -0
  106. data/lib/rush/expansion/ifs.rb +62 -0
  107. data/lib/rush/expansion/ifs_scanner.rb +168 -0
  108. data/lib/rush/expansion/parameter_expander.rb +204 -0
  109. data/lib/rush/expansion/parameter_forms.rb +22 -0
  110. data/lib/rush/expansion/pattern_removal.rb +63 -0
  111. data/lib/rush/expansion/pipeline.rb +124 -0
  112. data/lib/rush/expansion/read_char.rb +13 -0
  113. data/lib/rush/expansion/read_field_scanner.rb +159 -0
  114. data/lib/rush/expansion/read_splitter.rb +40 -0
  115. data/lib/rush/expansion/resolver.rb +23 -0
  116. data/lib/rush/expansion/tilde_expander.rb +102 -0
  117. data/lib/rush/external.rb +75 -0
  118. data/lib/rush/fd_entry.rb +60 -0
  119. data/lib/rush/for_runner.rb +50 -0
  120. data/lib/rush/function_frame.rb +30 -0
  121. data/lib/rush/function_runner.rb +37 -0
  122. data/lib/rush/function_table.rb +35 -0
  123. data/lib/rush/getopts_parser.rb +195 -0
  124. data/lib/rush/getopts_state.rb +104 -0
  125. data/lib/rush/here_doc.rb +57 -0
  126. data/lib/rush/interactive_signals.rb +26 -0
  127. data/lib/rush/invocation.rb +190 -0
  128. data/lib/rush/io_table.rb +94 -0
  129. data/lib/rush/job_control.rb +197 -0
  130. data/lib/rush/job_report.rb +45 -0
  131. data/lib/rush/job_spec.rb +51 -0
  132. data/lib/rush/job_table/control.rb +103 -0
  133. data/lib/rush/job_table/interruptible_wait.rb +70 -0
  134. data/lib/rush/job_table/job.rb +181 -0
  135. data/lib/rush/job_table.rb +194 -0
  136. data/lib/rush/lexer/alias_expander.rb +129 -0
  137. data/lib/rush/lexer/braced_reader.rb +86 -0
  138. data/lib/rush/lexer/case_frame.rb +114 -0
  139. data/lib/rush/lexer/case_tracker.rb +146 -0
  140. data/lib/rush/lexer/dollar_scanner.rb +53 -0
  141. data/lib/rush/lexer/double_quote_scanner.rb +71 -0
  142. data/lib/rush/lexer/heredoc_body.rb +91 -0
  143. data/lib/rush/lexer/heredoc_reader.rb +93 -0
  144. data/lib/rush/lexer/lex_state.rb +128 -0
  145. data/lib/rush/lexer/operator_table.rb +32 -0
  146. data/lib/rush/lexer/param_scanner.rb +43 -0
  147. data/lib/rush/lexer/paren_reader.rb +113 -0
  148. data/lib/rush/lexer/paren_regions.rb +74 -0
  149. data/lib/rush/lexer/quote_skips.rb +77 -0
  150. data/lib/rush/lexer/quoted_word.rb +67 -0
  151. data/lib/rush/lexer/scanner_predicates.rb +20 -0
  152. data/lib/rush/lexer/source_lines.rb +43 -0
  153. data/lib/rush/lexer/substitution_reader.rb +77 -0
  154. data/lib/rush/lexer/token_classifier.rb +144 -0
  155. data/lib/rush/lexer/token_predicates.rb +23 -0
  156. data/lib/rush/lexer/word_scanner.rb +134 -0
  157. data/lib/rush/lexer.rb +174 -0
  158. data/lib/rush/loop_control_handling.rb +29 -0
  159. data/lib/rush/loop_nesting.rb +50 -0
  160. data/lib/rush/loop_runner.rb +55 -0
  161. data/lib/rush/option_cluster.rb +39 -0
  162. data/lib/rush/options.rb +76 -0
  163. data/lib/rush/param_text.rb +39 -0
  164. data/lib/rush/parser.rb +1213 -0
  165. data/lib/rush/parser_support.rb +112 -0
  166. data/lib/rush/pattern_scanner.rb +35 -0
  167. data/lib/rush/pending_signals.rb +57 -0
  168. data/lib/rush/pipeline_runner.rb +173 -0
  169. data/lib/rush/pipeline_statuses.rb +45 -0
  170. data/lib/rush/positional.rb +45 -0
  171. data/lib/rush/posix_pattern.rb +78 -0
  172. data/lib/rush/program_input.rb +89 -0
  173. data/lib/rush/program_reader.rb +69 -0
  174. data/lib/rush/program_session.rb +83 -0
  175. data/lib/rush/prompt.rb +52 -0
  176. data/lib/rush/redirect_scope.rb +64 -0
  177. data/lib/rush/redirection/dup_redirect.rb +58 -0
  178. data/lib/rush/redirection/file_redirect.rb +50 -0
  179. data/lib/rush/redirection/here_doc_redirect.rb +17 -0
  180. data/lib/rush/redirection/registry.rb +53 -0
  181. data/lib/rush/repl.rb +133 -0
  182. data/lib/rush/runtime_type_checks.rb +16 -0
  183. data/lib/rush/scope.rb +79 -0
  184. data/lib/rush/segment_buffer.rb +47 -0
  185. data/lib/rush/shell_parameters.rb +77 -0
  186. data/lib/rush/shell_pattern.rb +80 -0
  187. data/lib/rush/shell_state.rb +163 -0
  188. data/lib/rush/shell_variables.rb +67 -0
  189. data/lib/rush/signal_report.rb +41 -0
  190. data/lib/rush/signals.rb +68 -0
  191. data/lib/rush/source.rb +82 -0
  192. data/lib/rush/source_line_counter.rb +41 -0
  193. data/lib/rush/source_runner.rb +47 -0
  194. data/lib/rush/startup.rb +75 -0
  195. data/lib/rush/status.rb +93 -0
  196. data/lib/rush/stop_relay.rb +32 -0
  197. data/lib/rush/subshell_runner.rb +70 -0
  198. data/lib/rush/system_calls/collation.rb +155 -0
  199. data/lib/rush/system_calls/file_tests.rb +95 -0
  200. data/lib/rush/system_calls/process_control.rb +165 -0
  201. data/lib/rush/system_calls/process_identity.rb +43 -0
  202. data/lib/rush/system_calls/resource_limits.rb +51 -0
  203. data/lib/rush/system_calls.rb +206 -0
  204. data/lib/rush/terminal.rb +126 -0
  205. data/lib/rush/trap_runner.rb +169 -0
  206. data/lib/rush/trap_table.rb +49 -0
  207. data/lib/rush/umask_mode.rb +133 -0
  208. data/lib/rush/version.rb +6 -0
  209. data/lib/rush.rb +190 -0
  210. metadata +527 -0
@@ -0,0 +1,258 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Rush
5
+ module Builtins
6
+ # One ulimit resource: option flag, dash list label, SystemCalls key, and display/set scale.
7
+ UlimitResource = Data.define(:flag, :label, :resource, :scale)
8
+ # Parsed ulimit invocation: list-all flag, selected resource/limit side, and optional value.
9
+ UlimitRequest = Data.define(:all, :resource, :target, :explicit_target, :value)
10
+ # Parser error code plus optional option text for diagnostics.
11
+ UlimitParseError = Data.define(:kind, :detail)
12
+ ULIMIT_RESOURCE_LIST = [
13
+ UlimitResource.new('t', 'time(seconds)', :cpu, 1),
14
+ UlimitResource.new('f', 'file(blocks)', :fsize, 512),
15
+ UlimitResource.new('d', 'data(kbytes)', :data, 1024),
16
+ UlimitResource.new('s', 'stack(kbytes)', :stack, 1024),
17
+ UlimitResource.new('c', 'coredump(blocks)', :core, 512),
18
+ UlimitResource.new('m', 'memory(kbytes)', :rss, 1024),
19
+ UlimitResource.new('l', 'locked memory(kbytes)', :memlock, 1024),
20
+ UlimitResource.new('p', 'process', :nproc, 1),
21
+ UlimitResource.new('n', 'nofiles', :nofile, 1),
22
+ UlimitResource.new('v', 'vmemory(kbytes)', :as, 1024),
23
+ UlimitResource.new('w', 'locks', :locks, 1),
24
+ UlimitResource.new('r', 'rtprio', :rtprio, 1)
25
+ ].freeze
26
+ ULIMIT_RESOURCES = ULIMIT_RESOURCE_LIST.to_h { |resource| [resource.flag, resource] }.freeze
27
+ ULIMIT_DEFAULT_RESOURCE = T.let(ULIMIT_RESOURCES.fetch('f'), UlimitResource)
28
+ ULIMIT_TARGETS = { 'H' => :hard, 'S' => :soft }.freeze
29
+
30
+ # Mutable parser for ulimit's compact option clusters (`-Hn`, `-Sn`, `-a`).
31
+ # It returns either a UlimitRequest or a small parse-error value that the
32
+ # builtin turns into the user-facing diagnostic.
33
+ class UlimitRequestBuilder
34
+ extend T::Sig
35
+
36
+ sig { params(resources: T::Hash[String, UlimitResource], default: UlimitResource).void }
37
+ def initialize(resources, default)
38
+ @resources = resources
39
+ @state = { all: false, resource: default, target: :soft, explicit_target: false }
40
+ end
41
+
42
+ sig { params(args: T::Array[String]).returns(T.any(UlimitRequest, UlimitParseError)) }
43
+ def call(args)
44
+ rest = args.dup
45
+ value = extract_value(rest)
46
+ return error(:too_many) if rest.any? { |arg| !arg.start_with?('-') }
47
+
48
+ parse_options(rest, value)
49
+ end
50
+
51
+ private
52
+
53
+ sig { params(args: T::Array[String]).returns(T.nilable(String)) }
54
+ def extract_value(args)
55
+ last = args.last
56
+ args.pop if last && !last.start_with?('-')
57
+ end
58
+
59
+ sig { params(args: T::Array[String], value: T.nilable(String)).returns(T.any(UlimitRequest, UlimitParseError)) }
60
+ def parse_options(args, value)
61
+ args.each { |arg| apply(arg).tap { |error| return error if error } }
62
+ return error(:too_many) if @state.fetch(:all) && value
63
+
64
+ request(value)
65
+ end
66
+
67
+ sig { params(arg: String).returns(T.nilable(UlimitParseError)) }
68
+ def apply(arg)
69
+ return error(:illegal_option, arg) if arg == '-'
70
+
71
+ apply_flags(arg.delete_prefix('-'))
72
+ end
73
+
74
+ sig { params(flags: String).returns(T.nilable(UlimitParseError)) }
75
+ def apply_flags(flags)
76
+ flags.each_char do |flag|
77
+ apply_or_error(flag).tap { |error| return error if error }
78
+ end
79
+ nil
80
+ end
81
+
82
+ sig { params(flag: String).returns(T::Boolean) }
83
+ def known_flag?(flag)
84
+ flag == 'a' || @resources.key?(flag) || ULIMIT_TARGETS.key?(flag)
85
+ end
86
+
87
+ sig { params(flag: String).returns(T.nilable(UlimitParseError)) }
88
+ def apply_or_error(flag)
89
+ return return_error(flag) unless known_flag?(flag)
90
+
91
+ apply_flag(flag)
92
+ nil
93
+ end
94
+
95
+ sig { params(flag: String).returns(UlimitParseError) }
96
+ def return_error(flag)
97
+ error(:illegal_option, "-#{flag}")
98
+ end
99
+
100
+ sig { params(flag: String).void }
101
+ def apply_flag(flag)
102
+ @state[:all] = true if flag == 'a'
103
+ @state[:resource] = @resources.fetch(flag) if @resources.key?(flag)
104
+ apply_target(flag) if ULIMIT_TARGETS.key?(flag)
105
+ end
106
+
107
+ sig { params(flag: String).void }
108
+ def apply_target(flag)
109
+ @state[:target] = ULIMIT_TARGETS.fetch(flag)
110
+ @state[:explicit_target] = true
111
+ end
112
+
113
+ sig { params(value: T.nilable(String)).returns(UlimitRequest) }
114
+ def request(value)
115
+ UlimitRequest.new(
116
+ @state.fetch(:all), @state.fetch(:resource), @state.fetch(:target), @state.fetch(:explicit_target), value
117
+ )
118
+ end
119
+
120
+ sig { params(kind: Symbol, detail: T.nilable(String)).returns(UlimitParseError) }
121
+ def error(kind, detail = nil)
122
+ UlimitParseError.new(kind, detail)
123
+ end
124
+ end
125
+
126
+ # `ulimit [-H|-S] [-a|-resource] [limit]` — query or set process resource
127
+ # limits. Values are printed in dash/POSIX units (blocks or kbytes for byte
128
+ # resources); `unlimited` maps to the platform's infinity value.
129
+ class Ulimit < Base
130
+ extend T::Sig
131
+
132
+ sig { returns(Status) }
133
+ def call
134
+ request = parse_request
135
+ return parse_error(request) if request.is_a?(UlimitParseError)
136
+
137
+ run_request(request)
138
+ end
139
+
140
+ private
141
+
142
+ sig { returns(T.any(UlimitRequest, UlimitParseError)) }
143
+ def parse_request
144
+ UlimitRequestBuilder.new(ULIMIT_RESOURCES, ULIMIT_DEFAULT_RESOURCE).call(operands)
145
+ end
146
+
147
+ sig { params(error: UlimitParseError).returns(Status) }
148
+ def parse_error(error)
149
+ return too_many if error.kind == :too_many
150
+
151
+ illegal_option(T.must(error.detail))
152
+ end
153
+
154
+ sig { params(request: UlimitRequest).returns(Status) }
155
+ def run_request(request)
156
+ return list_all(request) if request.all
157
+ return change_limit(request) if request.value
158
+
159
+ print_limit(request.resource, request.target)
160
+ end
161
+
162
+ sig { params(request: UlimitRequest).returns(Status) }
163
+ def list_all(request)
164
+ ULIMIT_RESOURCE_LIST.each { |resource| print_list_entry(resource, request.target) }
165
+ success
166
+ end
167
+
168
+ sig { params(resource: UlimitResource, target: Symbol).void }
169
+ def print_list_entry(resource, target)
170
+ stdout.printf("%<label>-20s %<value>s\n", label: resource.label, value: limit_text(resource, target))
171
+ end
172
+
173
+ sig { params(request: UlimitRequest).returns(Status) }
174
+ def change_limit(request)
175
+ value = parse_value(T.must(request.value), request.resource)
176
+ return value if value.is_a?(Status)
177
+
178
+ apply_limit(request, value)
179
+ end
180
+
181
+ sig { params(request: UlimitRequest, value: Integer).returns(Status) }
182
+ def apply_limit(request, value)
183
+ limits = limits_for(request, value)
184
+ executor.system.setrlimit(request.resource.resource, limits.fetch(0), limits.fetch(1))
185
+ success
186
+ rescue SystemCallError, ArgumentError
187
+ limit_error
188
+ end
189
+
190
+ sig { params(request: UlimitRequest, value: Integer).returns([Integer, Integer]) }
191
+ def limits_for(request, value)
192
+ soft, hard = executor.system.getrlimit(request.resource.resource)
193
+ limits_for_set(request, value, soft, hard)
194
+ end
195
+
196
+ sig do
197
+ params(request: UlimitRequest, value: Integer, soft: Integer, hard: Integer).returns([Integer, Integer])
198
+ end
199
+ def limits_for_set(request, value, soft, hard)
200
+ return [value, hard] if request.explicit_target && request.target == :soft
201
+ return [soft, value] if request.explicit_target && request.target == :hard
202
+
203
+ [value, value]
204
+ end
205
+
206
+ sig { params(resource: UlimitResource, target: Symbol).returns(Status) }
207
+ def print_limit(resource, target)
208
+ stdout.puts(limit_text(resource, target))
209
+ success
210
+ end
211
+
212
+ sig { params(resource: UlimitResource, target: Symbol).returns(String) }
213
+ def limit_text(resource, target)
214
+ soft, hard = executor.system.getrlimit(resource.resource)
215
+ format_limit(target == :hard ? hard : soft, resource)
216
+ end
217
+
218
+ sig { params(text: String, resource: UlimitResource).returns(T.any(Integer, Status)) }
219
+ def parse_value(text, resource)
220
+ return executor.system.infinity_limit if text == 'unlimited'
221
+ return Integer(text, 10) * resource.scale if text.match?(/\A\d+\z/)
222
+
223
+ illegal_number(text)
224
+ end
225
+
226
+ sig { params(value: Integer, resource: UlimitResource).returns(String) }
227
+ def format_limit(value, resource)
228
+ return 'unlimited' if value >= executor.system.infinity_limit
229
+
230
+ (value / resource.scale).to_s
231
+ end
232
+
233
+ sig { returns(Status) }
234
+ def limit_error
235
+ stderr.puts('rush: ulimit: error setting limit')
236
+ failure(2)
237
+ end
238
+
239
+ sig { returns(Status) }
240
+ def too_many
241
+ stderr.puts('rush: ulimit: too many arguments')
242
+ failure(2)
243
+ end
244
+
245
+ sig { params(option: String).returns(Status) }
246
+ def illegal_option(option)
247
+ stderr.puts("rush: ulimit: Illegal option #{option}")
248
+ failure(2)
249
+ end
250
+
251
+ sig { params(text: String).returns(Status) }
252
+ def illegal_number(text)
253
+ stderr.puts("rush: ulimit: Illegal number: #{text}")
254
+ failure(2)
255
+ end
256
+ end
257
+ end
258
+ end
@@ -0,0 +1,81 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Rush
5
+ module Builtins
6
+ # `umask [-S] [mode]` — print or set the file creation mask. Octal operands
7
+ # set the mask directly; symbolic operands edit the allowed permission bits
8
+ # (the complement of the mask), as shown by `umask -S`.
9
+ class Umask < Base
10
+ extend T::Sig
11
+
12
+ sig { returns(Status) }
13
+ def call
14
+ return illegal_option(operands.first) if invalid_option?
15
+
16
+ symbolic, mode = parse_operands
17
+ return change_mask(mode) if mode
18
+ return display_symbolic if symbolic
19
+
20
+ display_octal
21
+ end
22
+
23
+ private
24
+
25
+ sig { returns(Integer) }
26
+ def current_mask
27
+ executor.system.current_umask
28
+ end
29
+
30
+ sig { params(mode: String).returns(Status) }
31
+ def change_mask(mode)
32
+ parsed = UmaskMode.parse(mode, current_mask)
33
+ return illegal(mode) unless parsed
34
+
35
+ executor.system.change_umask(parsed)
36
+ success
37
+ end
38
+
39
+ sig { returns(T::Boolean) }
40
+ def invalid_option?
41
+ operands.any? { |arg| invalid_option_operand?(arg) }
42
+ end
43
+
44
+ sig { params(arg: String).returns(T::Boolean) }
45
+ def invalid_option_operand?(arg)
46
+ arg.start_with?('-') && arg != '-S'
47
+ end
48
+
49
+ sig { returns([T::Boolean, T.nilable(String)]) }
50
+ def parse_operands
51
+ args = operands.reject { |arg| arg == '-S' }
52
+ [operands.include?('-S'), args.first]
53
+ end
54
+
55
+ sig { returns(Status) }
56
+ def display_symbolic
57
+ stdout.puts(UmaskMode.format_symbolic(current_mask))
58
+ success
59
+ end
60
+
61
+ sig { returns(Status) }
62
+ def display_octal
63
+ stdout.puts(UmaskMode.format_octal(current_mask))
64
+ success
65
+ end
66
+
67
+ sig { params(mode: String).returns(Status) }
68
+ def illegal(mode)
69
+ label = mode.match?(/\A[0-9]+\z/) ? 'number' : 'mode'
70
+ stderr.puts("rush: umask: Illegal #{label}: #{mode}")
71
+ failure(2)
72
+ end
73
+
74
+ sig { params(option: T.nilable(String)).returns(Status) }
75
+ def illegal_option(option)
76
+ stderr.puts("rush: umask: Illegal option #{option}")
77
+ failure(2)
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,46 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Rush
5
+ module Builtins
6
+ # `unalias name...` removes each named alias; `unalias -a` removes them all.
7
+ # A name with no alias is reported as `unalias: NAME not found` on stderr with
8
+ # status 1, but the remaining names are still processed.
9
+ class Unalias < Base
10
+ extend T::Sig
11
+
12
+ sig { returns(Status) }
13
+ def call
14
+ return remove_all if operands.first == '-a'
15
+
16
+ operands.reduce(success) { |status, name| keep(status, remove(name)) }
17
+ end
18
+
19
+ private
20
+
21
+ sig { returns(Status) }
22
+ def remove_all
23
+ aliases.clear
24
+ success
25
+ end
26
+
27
+ sig { params(name: String).returns(Status) }
28
+ def remove(name)
29
+ return success if aliases.remove(name)
30
+
31
+ stderr.puts("unalias: #{name} not found")
32
+ failure(1)
33
+ end
34
+
35
+ sig { params(status: Status, result: Status).returns(Status) }
36
+ def keep(status, result)
37
+ status.success? ? result : status
38
+ end
39
+
40
+ sig { returns(AliasTable) }
41
+ def aliases
42
+ executor.state.aliases
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,40 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Rush
5
+ module Builtins
6
+ # `unset [-fv] name ...` — remove each name. The default (and -v) unsets a
7
+ # shell variable; -f unsets a function. Unsetting an absent name succeeds.
8
+ class Unset < Base
9
+ extend T::Sig
10
+
11
+ sig { returns(Status) }
12
+ def call
13
+ names.each { |name| remove(name) }
14
+ success
15
+ end
16
+
17
+ private
18
+
19
+ sig { returns(T::Array[String]) }
20
+ def names
21
+ operands.first&.start_with?('-') ? operands.drop(1) : operands
22
+ end
23
+
24
+ sig { returns(T::Boolean) }
25
+ def function?
26
+ operands.first == '-f'
27
+ end
28
+
29
+ sig { params(name: String).void }
30
+ def remove(name)
31
+ function? ? state.functions.undefine(name) : state.variables.unset(name)
32
+ end
33
+
34
+ sig { returns(ShellState) }
35
+ def state
36
+ executor.state
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,127 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Rush
5
+ module Builtins
6
+ # `wait [pid | %id ...]`: with no operands, wait for every known
7
+ # background job (status 0); with operands, wait for each and return the
8
+ # last one's status — 127 when a pid is not a known child, and a %id that
9
+ # resolves to no job is a status-2 error (No such job / No current job,
10
+ # like dash). POSIX gives an unknown last pid its 127 even after a known
11
+ # one; dash instead keeps the last known operand's status — the standard
12
+ # wins (journal). A malformed operand reports on stderr with status 2,
13
+ # and wait is a regular builtin, so the shell carries on (dash-verified).
14
+ class Wait < Base
15
+ extend T::Sig
16
+
17
+ # dash's operand shape: an explicit + sign is accepted; a value past
18
+ # INT_MAX overflows like a non-numeric one; a leading dash reads as an
19
+ # (unsupported) option, except the bare `-`, which is a bad number.
20
+ NUMERIC = /\A\+?\d+\z/
21
+
22
+ sig { returns(Status) }
23
+ def call
24
+ args = operands
25
+ args = args.drop(1) if args.first == '--'
26
+ return all if args.empty?
27
+
28
+ each_pid(args)
29
+ end
30
+
31
+ private
32
+
33
+ # No operands: collect every known job — a stopped one answers its
34
+ # 128+stopsig at once, so wait never blocks on it (dash-probed).
35
+ sig { returns(Status) }
36
+ def all
37
+ executor.jobs.ordered.each do |job|
38
+ status = interruptible_wait(job.pid)
39
+ return pending_status(T.must(status)) if executor.trap_runner.pending?
40
+ end
41
+ success
42
+ end
43
+
44
+ # Malformed pids are rejected up front (observationally equivalent to
45
+ # dash's lazy check: an already-waited job stays remembered, so a later
46
+ # wait answers the same); %ids validate through resolution itself.
47
+ sig { params(args: T::Array[String]).returns(Status) }
48
+ def each_pid(args)
49
+ malformed = args.find { |operand| !operand.start_with?('%') && !parse(operand) }
50
+ return bad(malformed) if malformed
51
+
52
+ statuses(args)
53
+ rescue JobError => e
54
+ no_job(e)
55
+ end
56
+
57
+ sig { params(args: T::Array[String]).returns(Status) }
58
+ def statuses(args)
59
+ status = T.let(success, Status)
60
+ status = status_of(T.must(args.shift)) until args.empty? || executor.trap_runner.pending?
61
+ pending_status(status)
62
+ end
63
+
64
+ sig { params(operand: String).returns(Status) }
65
+ def status_of(operand)
66
+ return awaited(job_pid(operand)) if operand.start_with?('%')
67
+
68
+ awaited(T.must(parse(operand)))
69
+ end
70
+
71
+ sig { params(operand: String).returns(Integer) }
72
+ def job_pid(operand)
73
+ JobSpec.resolve(executor.jobs, operand).pid
74
+ end
75
+
76
+ sig { params(error: JobError).returns(Status) }
77
+ def no_job(error)
78
+ stderr.puts("wait: #{error.message}")
79
+ failure(2)
80
+ end
81
+
82
+ sig { params(operand: String).returns(T.nilable(Integer)) }
83
+ def parse(operand)
84
+ return unless operand.match?(NUMERIC)
85
+
86
+ value = Integer(operand, 10)
87
+ value if value <= INT_MAX
88
+ end
89
+
90
+ # A pid/%id operand whose job died by a signal reports strsignal on
91
+ # stderr, like dash's wait-by-pid (probed; the bare `wait` in #all
92
+ # never reports, also probed).
93
+ sig { params(pid: Integer).returns(Status) }
94
+ def awaited(pid)
95
+ status = interruptible_wait(pid)
96
+ status ? SignalReport.report(status, stderr) : failure(127)
97
+ end
98
+
99
+ sig { params(pid: Integer).returns(T.nilable(Status)) }
100
+ def interruptible_wait(pid)
101
+ executor.jobs.wait_for_interruptibly(pid) { executor.trap_runner.pending_exitstatus }
102
+ end
103
+
104
+ sig { params(status: Status).returns(Status) }
105
+ def pending_status(status)
106
+ code = executor.trap_runner.pending_exitstatus
107
+ code ? Status.new(code) : status
108
+ end
109
+
110
+ sig { params(operand: String).returns(Status) }
111
+ def bad(operand)
112
+ stderr.puts("wait: #{diagnosis(operand)}")
113
+ failure(2)
114
+ end
115
+
116
+ sig { params(operand: String).returns(String) }
117
+ def diagnosis(operand)
118
+ option?(operand) ? "Illegal option #{operand}" : "Illegal number: #{operand}"
119
+ end
120
+
121
+ sig { params(operand: String).returns(T::Boolean) }
122
+ def option?(operand)
123
+ operand.start_with?('-') && operand != '-'
124
+ end
125
+ end
126
+ end
127
+ end
data/lib/rush/cli.rb ADDED
@@ -0,0 +1,31 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Rush
5
+ # Entry point: parse the invocation, run the session it implies (interactive
6
+ # REPL or non-interactive source), and return the process exit code. A
7
+ # malformed command line — unknown option, missing -c/-o argument, unreadable
8
+ # script file — reports on stderr and exits 2, like dash.
9
+ class CLI
10
+ extend T::Sig
11
+
12
+ sig { params(argv: T::Array[String], system: SystemCalls).returns(Integer) }
13
+ def self.run(argv, system: SystemCalls.new)
14
+ new(argv, system).run
15
+ end
16
+
17
+ sig { params(argv: T::Array[String], system: SystemCalls).void }
18
+ def initialize(argv, system)
19
+ @argv = argv
20
+ @system = system
21
+ end
22
+
23
+ sig { returns(Integer) }
24
+ def run
25
+ Invocation.new(@argv, @system).session.run
26
+ rescue InvocationError => e
27
+ @system.stderr.puts("rush: #{e.message}")
28
+ 2
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,56 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Rush
5
+ # Applies assignment words attached to a simple command. Bare assignments
6
+ # persist in shell variables; assignments before an external command overlay
7
+ # the exported environment without mutating the shell.
8
+ class CommandAssignments
9
+ extend T::Sig
10
+
11
+ sig { params(assignments: T::Array[AST::Assignment], expander: Expansion::Pipeline).void }
12
+ def initialize(assignments, expander)
13
+ @assignments = assignments
14
+ @expander = expander
15
+ end
16
+
17
+ sig { returns(T::Array[String]) }
18
+ def names
19
+ @assignments.map(&:name)
20
+ end
21
+
22
+ sig { params(variables: ShellVariables).void }
23
+ def persist_to(variables)
24
+ @assignments.each { |assignment| variables.assign(assignment.name, expand(assignment)) }
25
+ end
26
+
27
+ sig { params(variables: ShellVariables).returns(T::Hash[String, String]) }
28
+ def environment_for(variables)
29
+ @assignments.each_with_object(variables.exported) do |assignment, env|
30
+ env[assignment.name] = expand(assignment)
31
+ end
32
+ end
33
+
34
+ sig { params(environment: T::Hash[String, String]).returns(T::Hash[String, String]) }
35
+ def temporary_environment(environment)
36
+ names.to_h { |name| [name, environment.fetch(name)] }
37
+ end
38
+
39
+ sig { params(environment: T::Hash[String, String], variables: ShellVariables).void }
40
+ def persist_environment(environment, variables)
41
+ temporary_environment(environment).each { |name, value| variables.assign(name, value) }
42
+ end
43
+
44
+ sig { params(variables: ShellVariables).void }
45
+ def validate(variables)
46
+ @assignments.each { |assignment| variables.validate_assignment(assignment.name) }
47
+ end
48
+
49
+ private
50
+
51
+ sig { params(assignment: AST::Assignment).returns(String) }
52
+ def expand(assignment)
53
+ @expander.expand_value(assignment.value, tilde: :assignment)
54
+ end
55
+ end
56
+ end