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,18 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Rush
5
+ module AST
6
+ # `while cond; do body; done` — runs the body while the condition succeeds.
7
+ class While < ConditionLoop
8
+ extend T::Sig
9
+
10
+ private
11
+
12
+ sig { returns(Symbol) }
13
+ def kind
14
+ :while
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,44 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Rush
5
+ module AST
6
+ # An unexpanded word: an ordered list of typed segments. The expander turns
7
+ # the segments into fields; `literal_text` is the simple concatenation used
8
+ # for error messages and (unquoted) assignment-name detection.
9
+ class Word < Node
10
+ extend T::Sig
11
+
12
+ attr_reader :segments, :source_line
13
+
14
+ sig { params(segments: T::Array[WordSegment[T.untyped]], source_line: Integer).void }
15
+ def initialize(segments, source_line: 1)
16
+ @segments = segments
17
+ @source_line = source_line
18
+ end
19
+
20
+ sig { params(text: String, source_line: Integer).returns(Word) }
21
+ def self.literal(text, source_line: 1)
22
+ new([LiteralSegment.new(text, false)], source_line: source_line)
23
+ end
24
+
25
+ sig { returns(String) }
26
+ def literal_text
27
+ segments.map(&:value).join
28
+ end
29
+
30
+ # The text when this word is a bare name — one unquoted literal segment, no
31
+ # quoting or substitution — else nil. Used for reserved words, NAME=, and
32
+ # alias substitution, all of which only apply to a plain literal word.
33
+ sig { returns(T.nilable(String)) }
34
+ def literal_name
35
+ (segments.first.literal_value if segments.one?)
36
+ end
37
+
38
+ sig { returns(T.nilable(String)) }
39
+ def first_literal_value
40
+ segments.fetch(0).literal_value
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,203 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Rush
5
+ module AST
6
+ # A typed piece of a word. Subclasses know how to expand themselves (the
7
+ # segment-level counterpart of a command node's #execute), so there is no
8
+ # `kind` to dispatch on: `quoted` records whether it came from a quoted
9
+ # context (which governs field splitting and pathname expansion), `value` is
10
+ # the kind-specific payload (literal text, a ParamRef, or substitution
11
+ # source). literal_value is the text when the segment can stand in as a bare
12
+ # name; splittable?/splat? are its field-splitting roles.
13
+ class WordSegment
14
+ extend T::Sig
15
+ extend T::Generic
16
+
17
+ Value = type_member
18
+
19
+ sig { returns(Value) }
20
+ attr_reader :value
21
+
22
+ sig { returns(T::Boolean) }
23
+ attr_reader :quoted
24
+
25
+ sig { params(value: Value, quoted: T::Boolean).void }
26
+ def initialize(value, quoted)
27
+ @value = value
28
+ @quoted = quoted
29
+ end
30
+
31
+ # Each concrete segment expands itself to a string (the segment-level
32
+ # counterpart of a command node's #execute); the base is abstract.
33
+ sig { params(_executor: Executor).returns(String) }
34
+ def expand(_executor)
35
+ raise NotImplementedError
36
+ end
37
+
38
+ sig { returns(T.nilable(String)) }
39
+ # mutant:disable -- `nil` is the default "not a literal" value; an empty
40
+ # method body has the same public semantics for this abstract fallback.
41
+ def literal_value
42
+ nil
43
+ end
44
+
45
+ # This segment as dash's canonical job text prints it (CommandText):
46
+ # each kind re-spells itself; the base is abstract.
47
+ sig { returns(String) }
48
+ def canon
49
+ raise NotImplementedError
50
+ end
51
+
52
+ sig { returns(T::Boolean) }
53
+ def splittable?
54
+ false
55
+ end
56
+
57
+ sig { returns(T::Boolean) }
58
+ def splat?
59
+ false
60
+ end
61
+
62
+ sig { params(executor: Executor, parts: T::Array[Expansion::FieldPart]).void }
63
+ def append_field_parts(executor, parts)
64
+ parts << [expand(executor), splittable?, false, quoted]
65
+ end
66
+
67
+ # A copy with a rewritten value (e.g. after tilde expansion).
68
+ sig { params(new_value: Value).returns(WordSegment[Value]) }
69
+ def with_value(new_value)
70
+ self.class.new(new_value, quoted)
71
+ end
72
+
73
+ sig { params(other: T.untyped).returns(T::Boolean) }
74
+ def ==(other)
75
+ return false unless other.instance_of?(self.class)
76
+ return false unless equality_value.eql?(other.equality_value)
77
+ return false unless quoted.eql?(other.quoted)
78
+
79
+ true
80
+ end
81
+
82
+ alias eql? ==
83
+
84
+ sig { returns(T.untyped) }
85
+ def equality_value
86
+ value
87
+ end
88
+
89
+ sig { returns(Integer) }
90
+ def hash
91
+ [self.class, value, quoted].hash
92
+ end
93
+ end
94
+
95
+ # Literal text. It expands to itself, can act as a bare name when unquoted,
96
+ # and is never field-split.
97
+ class LiteralSegment < WordSegment
98
+ extend T::Sig
99
+ extend T::Generic
100
+
101
+ Value = type_member { { fixed: String } }
102
+
103
+ # dash escapes \ " $ ` inside the double quotes a quoted run gets.
104
+ CANON_ESCAPES = T.let(/[\\"$`]/, Regexp)
105
+
106
+ sig { params(_executor: Executor).returns(String) }
107
+ def expand(_executor)
108
+ value
109
+ end
110
+
111
+ sig { returns(T.nilable(String)) }
112
+ def literal_value
113
+ (value unless quoted)
114
+ end
115
+
116
+ sig { returns(String) }
117
+ def canon
118
+ quoted ? value.gsub(CANON_ESCAPES) { |char| "\\#{char}" } : value
119
+ end
120
+ end
121
+
122
+ # A segment substituted at expansion time (param / command / arithmetic): its
123
+ # unquoted result undergoes field splitting.
124
+ class DynamicSegment < WordSegment
125
+ extend T::Sig
126
+ extend T::Generic
127
+
128
+ Value = type_member
129
+
130
+ sig { returns(T::Boolean) }
131
+ def splittable?
132
+ !quoted
133
+ end
134
+ end
135
+
136
+ # $name / ${...}: value is the ParamRef.
137
+ class ParamSegment < DynamicSegment
138
+ extend T::Sig
139
+ extend T::Generic
140
+
141
+ Value = type_member { { fixed: ParamRef } }
142
+
143
+ sig { params(executor: Executor).returns(String) }
144
+ def expand(executor)
145
+ Expansion::ParameterExpander.new(executor, value, quoted: quoted).expand
146
+ end
147
+
148
+ # $@ (always) and unquoted $* expand to one field per positional parameter.
149
+ sig { returns(T::Boolean) }
150
+ def splat?
151
+ !value.op && (value.name == '@' || (value.name == '*' && !quoted))
152
+ end
153
+
154
+ sig { params(executor: Executor, parts: T::Array[Expansion::FieldPart]).void }
155
+ def append_field_parts(executor, parts)
156
+ parts.concat(Expansion::ParameterExpander.new(executor, value, quoted: quoted).expand_parts)
157
+ end
158
+
159
+ sig { returns(String) }
160
+ def canon
161
+ value.canon
162
+ end
163
+ end
164
+
165
+ # $(...) / `...`: value is the command-substitution source.
166
+ class CommandSegment < DynamicSegment
167
+ extend T::Sig
168
+ extend T::Generic
169
+
170
+ Value = type_member { { fixed: String } }
171
+
172
+ sig { params(executor: Executor).returns(String) }
173
+ def expand(executor)
174
+ Expansion::CommandSubstitution.new(executor, value).expand
175
+ end
176
+
177
+ # dash elides a command substitution's body from the job text.
178
+ sig { returns(String) }
179
+ def canon
180
+ '$(...)'
181
+ end
182
+ end
183
+
184
+ # $((...)): value is the arithmetic source.
185
+ class ArithSegment < DynamicSegment
186
+ extend T::Sig
187
+ extend T::Generic
188
+
189
+ Value = type_member { { fixed: String } }
190
+
191
+ sig { params(executor: Executor).returns(String) }
192
+ def expand(executor)
193
+ Expansion::ArithmeticExpander.new(executor, value).expand
194
+ end
195
+
196
+ # An arithmetic expansion keeps its raw source in the job text.
197
+ sig { returns(String) }
198
+ def canon
199
+ "$((#{value}))"
200
+ end
201
+ end
202
+ end
203
+ end
@@ -0,0 +1,68 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Rush
5
+ # Runs an asynchronous list entry in a forked subshell. The parent records
6
+ # the child pid for $! and in the job table (where the wait builtin finds
7
+ # it) and immediately returns success (launch semantics); the child
8
+ # isolates itself per POSIX (stdin from /dev/null, SIGINT/SIGQUIT ignored)
9
+ # and resolves shell-control exceptions to an exit status like a subshell.
10
+ class BackgroundRunner
11
+ extend T::Sig
12
+
13
+ sig { params(executor: Executor, body: AST::Node).void }
14
+ def initialize(executor, body)
15
+ @executor = executor
16
+ @body = body
17
+ end
18
+
19
+ sig { returns(Status) }
20
+ def call
21
+ pid = @executor.job_control.launch_background { run_child }
22
+ @executor.state.record_background_pid(pid)
23
+ @executor.jobs.record(pid, text: @executor.job_control.job_text(@body))
24
+ Status.success
25
+ end
26
+
27
+ sig { returns(Status) }
28
+ def run_body
29
+ isolate
30
+ SubshellRunner.new(@executor, @body).run_body
31
+ end
32
+
33
+ private
34
+
35
+ # POSIX 2.9.3.1 / 2.11 with job control disabled: an async child reads
36
+ # stdin from /dev/null (its own redirections may rebind it) and starts
37
+ # with SIGINT/SIGQUIT ignored. Both apply ONLY without job control — under
38
+ # set -m the job sits in its own process group, so POSIX (and dash,
39
+ # probed) leave stdin and SIGINT alone; monitored? is read before
40
+ # enter_subshell switches the machinery off for this child. The subshell
41
+ # entry (trap reset + job-table clear) must run first — an interactive
42
+ # session's base handlers reinstall OS defaults as they drop, which would
43
+ # undo the ignores. The immediate repeat inside SubshellRunner#run_body is
44
+ # harmless because isolation creates no reset-sensitive shell state between
45
+ # entries: no body, child job, pending signal, caught trap or EXIT action.
46
+ # Raw dispositions and stdin are deliberately installed there, and the
47
+ # second entry does not reset them. This is not general idempotence. The
48
+ # ignores are a real SIG_IGN, kept off the trap table: they
49
+ # survive exec and nested subshells, while `trap` overrides them and
50
+ # `trap - INT` restores the OS default (dash-verified).
51
+ sig { void }
52
+ def isolate
53
+ monitored = @executor.job_control.monitored?
54
+ @executor.enter_subshell
55
+ return if monitored
56
+
57
+ %w[INT QUIT].each { |name| @executor.system.trap_signal(name, 'IGNORE') { nil } }
58
+ @executor.replace_io(@executor.io.with(0, @executor.system.open_file(File::NULL, 'r')))
59
+ end
60
+
61
+ sig { returns(T.noreturn) }
62
+ def run_child
63
+ # :nocov:
64
+ @executor.system.exit!(run_body.exitstatus)
65
+ # :nocov:
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,107 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ require 'strscan'
5
+
6
+ module Rush
7
+ # One shell-pattern bracket expression. It recognizes the nested POSIX
8
+ # character-class, equivalence-class and collating-symbol delimiters so their
9
+ # inner `]` does not terminate the outer expression.
10
+ class BracketExpression
11
+ extend T::Sig
12
+
13
+ CLASS_NAMES = %w[alnum alpha blank cntrl digit graph lower print punct space upper xdigit].freeze
14
+
15
+ sig { returns(String) }
16
+ attr_reader :source
17
+
18
+ sig { returns(Integer) }
19
+ attr_reader :finish
20
+
21
+ sig { params(pattern: String, start: Integer).returns(T.nilable(BracketExpression)) }
22
+ def self.parse(pattern, start)
23
+ finish, special = BracketScanner.new(pattern, start).call
24
+ return new(T.must(pattern[start...finish]), finish, special, true) if finish
25
+
26
+ new(T.must(pattern[start..]), pattern.length, true, false) if special
27
+ end
28
+
29
+ sig { params(source: String, finish: Integer, special: T::Boolean, closed: T::Boolean).void }
30
+ def initialize(source, finish, special, closed)
31
+ @source = source
32
+ @finish = finish
33
+ @special = special
34
+ @closed = closed
35
+ end
36
+
37
+ sig { returns(T::Boolean) }
38
+ def special?
39
+ @special
40
+ end
41
+
42
+ sig { returns(String) }
43
+ def glob_source
44
+ special? ? '?' : source
45
+ end
46
+
47
+ sig { params(regexp: String, glob: String, scanner: StringScanner).void }
48
+ def append_to(regexp, glob, scanner)
49
+ regexp << regex
50
+ glob << glob_source
51
+ scanner.pos = finish
52
+ end
53
+
54
+ # Ruby regexp supports the twelve required POSIX named classes. The two
55
+ # collation forms have a portable one-character meaning in the POSIX locale;
56
+ # multi-character locale collation is deliberately left invalid here.
57
+ sig { returns(String) }
58
+ def regex
59
+ raise RegexpError unless @closed
60
+
61
+ "[#{regexp_body}]"
62
+ rescue RegexpError
63
+ '(?!)'
64
+ end
65
+
66
+ private
67
+
68
+ sig { returns(String) }
69
+ def regexp_body
70
+ body = collating(classes(negation(source.delete_prefix('[').delete_suffix(']'))))
71
+ escape_brackets(body)
72
+ end
73
+
74
+ sig { params(body: String).returns(String) }
75
+ def escape_brackets(body)
76
+ body = body.sub(/\A(\^?)\]/) { "#{Regexp.last_match(1)}\\]" }
77
+ body.gsub(/(?<!\\)\[(?!:)/, '\\\\[')
78
+ end
79
+
80
+ sig { params(body: String).returns(String) }
81
+ def negation(body)
82
+ body.sub(/\A[!^]/, '^')
83
+ end
84
+
85
+ sig { params(body: String).returns(String) }
86
+ def classes(body)
87
+ body.gsub(/\[:([a-z]+):\]/) do
88
+ name = Regexp.last_match(1)
89
+ raise RegexpError unless CLASS_NAMES.include?(name)
90
+
91
+ "[:#{name}:]"
92
+ end
93
+ end
94
+
95
+ sig { params(body: String).returns(String) }
96
+ def collating(body)
97
+ body.gsub(/\[([=.])(.*?)\1\]/) { collating_element(T.must(Regexp.last_match(2))) }
98
+ end
99
+
100
+ sig { params(element: String).returns(String) }
101
+ def collating_element(element)
102
+ raise RegexpError unless element.each_char.one?
103
+
104
+ Regexp.escape(element)
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,64 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Rush
5
+ # Finds the outer close of a shell bracket expression without mistaking the
6
+ # `]` inside POSIX `[:class:]`, `[=equiv=]` or `[.collating.]` for it.
7
+ class BracketScanner
8
+ extend T::Sig
9
+
10
+ TOKEN_ENDS = { ':' => ':]', '=' => '=]', '.' => '.]' }.freeze
11
+
12
+ sig { params(pattern: String, start: Integer).void }
13
+ def initialize(pattern, start)
14
+ @pattern = pattern
15
+ @index = first_index(start)
16
+ @special = false
17
+ end
18
+
19
+ sig { returns([T.nilable(Integer), T::Boolean]) }
20
+ def call
21
+ advance until complete?
22
+ [finish, @special]
23
+ end
24
+
25
+ private
26
+
27
+ sig { params(start: Integer).returns(Integer) }
28
+ def first_index(start)
29
+ index = start + 1
30
+ index += 1 if %w[! ^].include?(@pattern[index])
31
+ @pattern[index] == ']' ? index + 1 : index
32
+ end
33
+
34
+ sig { returns(T::Boolean) }
35
+ def complete?
36
+ current = @pattern[@index]
37
+ !current || current == ']'
38
+ end
39
+
40
+ sig { void }
41
+ def advance
42
+ token_finish = nested_finish
43
+ @special = true if token_finish
44
+ @index = token_finish || escaped_finish || (@index + 1)
45
+ end
46
+
47
+ sig { returns(T.nilable(Integer)) }
48
+ def escaped_finish
49
+ @index + 2 if @pattern[@index] == '\\' && @pattern[@index + 1]
50
+ end
51
+
52
+ sig { returns(T.nilable(Integer)) }
53
+ def nested_finish
54
+ ending = TOKEN_ENDS[@pattern[@index + 1].to_s] if @pattern[@index] == '['
55
+ found = @pattern.index(ending, @index + 2) if ending
56
+ found + 2 if found
57
+ end
58
+
59
+ sig { returns(T.nilable(Integer)) }
60
+ def finish
61
+ @index + 1 if @index < @pattern.length
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,74 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Rush
5
+ module Builtins
6
+ # `alias [name[=value]...]` defines or prints aliases. With no operands it
7
+ # lists every alias as a single-quoted `name=value` on stdout. An operand
8
+ # containing `=` past its first character defines (the name is everything
9
+ # before that `=`); a bare name prints that alias, or reports `alias: NAME not
10
+ # found` on stderr and yields status 1 while still processing the rest.
11
+ class Alias < Base
12
+ extend T::Sig
13
+
14
+ sig { returns(Status) }
15
+ def call
16
+ if operands.empty?
17
+ list
18
+ else
19
+ operands.reduce(success) { |status, operand| keep(status, handle(operand)) }
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ sig { params(operand: String).returns(Status) }
26
+ def handle(operand)
27
+ name, sep, value = operand.partition('=')
28
+ sep.empty? || name.empty? ? query(operand) : define(name, value)
29
+ end
30
+
31
+ sig { params(name: String, value: String).returns(Status) }
32
+ def define(name, value)
33
+ aliases.define(name, value)
34
+ success
35
+ end
36
+
37
+ sig { params(name: String).returns(Status) }
38
+ def query(name)
39
+ value = aliases.value(name)
40
+ return show(name, value) if value
41
+
42
+ stderr.puts("alias: #{name} not found")
43
+ failure
44
+ end
45
+
46
+ sig { returns(Status) }
47
+ def list
48
+ aliases.listing.each { |name, value| show(name, value) }
49
+ success
50
+ end
51
+
52
+ sig { params(name: String, value: String).returns(Status) }
53
+ def show(name, value)
54
+ stdout.puts(single_quote("#{name}=#{value}"))
55
+ success
56
+ end
57
+
58
+ sig { params(text: String).returns(String) }
59
+ def single_quote(text)
60
+ "'#{text.gsub("'", %q('"'"'))}'"
61
+ end
62
+
63
+ sig { params(status: Status, result: Status).returns(Status) }
64
+ def keep(status, result)
65
+ status.success? ? result : status
66
+ end
67
+
68
+ sig { returns(AliasTable) }
69
+ def aliases
70
+ executor.state.aliases
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,96 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Rush
5
+ module Builtins
6
+ # Base for builtins. Subclasses implement #call returning a Status. Streams
7
+ # come from the per-command IoTable so redirections apply to builtins too.
8
+ class Base
9
+ extend T::Sig
10
+
11
+ # A signed decimal integer (surrounding blanks allowed); the caller's min
12
+ # enforces whether negative or zero values are valid for this operand. dash
13
+ # parses it into a C int, so a value past INT_MAX overflows and is rejected
14
+ # like a non-numeric one.
15
+ NUMERIC_OPERAND = /\A\s*[+-]?\d+\s*\z/
16
+ INT_MAX = 2_147_483_647
17
+
18
+ sig do
19
+ params(executor: Executor, argv: T::Array[String], io: IoTable,
20
+ environment: T.nilable(T::Hash[String, String])).void
21
+ end
22
+ def initialize(executor, argv, io, environment = nil)
23
+ @executor = executor
24
+ @argv = argv
25
+ @io = io
26
+ @environment = environment
27
+ end
28
+
29
+ sig { returns(Status) }
30
+ def call
31
+ raise NotImplementedError
32
+ end
33
+
34
+ private
35
+
36
+ sig { returns(Executor) }
37
+ attr_reader :executor
38
+
39
+ sig { returns(T::Array[String]) }
40
+ attr_reader :argv
41
+
42
+ sig { returns(IoTable) }
43
+ attr_reader :io
44
+
45
+ # The exported environment assembled for this simple-command invocation,
46
+ # including temporary prefix assignments. Most builtins do not consume it;
47
+ # `command` forwards it when its nested target is external.
48
+ sig { returns(T::Hash[String, String]) }
49
+ def command_environment
50
+ @environment || executor.state.variables.exported
51
+ end
52
+
53
+ sig { returns(T::Array[String]) }
54
+ def operands
55
+ argv.drop(1)
56
+ end
57
+
58
+ sig { returns(T.untyped) }
59
+ def stdout
60
+ io.get(1)
61
+ end
62
+
63
+ sig { returns(T.untyped) }
64
+ def stderr
65
+ io.get(2)
66
+ end
67
+
68
+ sig { returns(Status) }
69
+ def success
70
+ Status.success
71
+ end
72
+
73
+ sig { params(code: Integer).returns(Status) }
74
+ def failure(code = 1)
75
+ Status.failure(code)
76
+ end
77
+
78
+ # Parse a numeric operand for a special builtin: an exit code for
79
+ # exit/return (min 0), or a loop level for break/continue (min 1). dash
80
+ # rejects a non-numeric, too-small or out-of-range value with a
81
+ # special-builtin error (which aborts a non-interactive shell).
82
+ sig { params(text: String, min: Integer).returns(Integer) }
83
+ def numeric_operand(text, min: 0)
84
+ value = Integer(text, 10) if text.match?(NUMERIC_OPERAND)
85
+ return T.must(value) if legal_operand?(value, min)
86
+
87
+ raise BuiltinError, "#{argv.first}: Illegal number: #{text}"
88
+ end
89
+
90
+ sig { params(value: T.nilable(Integer), min: Integer).returns(T::Boolean) }
91
+ def legal_operand?(value, min)
92
+ !!value&.between?(min, INT_MAX)
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,19 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Rush
5
+ module Builtins
6
+ # `break [n]` — exit the n-th enclosing loop (default 1); the shared
7
+ # POSIX 2.9.5 semantics live in LoopJump.
8
+ class Break < LoopJump
9
+ extend T::Sig
10
+
11
+ private
12
+
13
+ sig { returns(T.class_of(LoopControl)) }
14
+ def signal
15
+ BreakSignal
16
+ end
17
+ end
18
+ end
19
+ end