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,48 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Rush
5
+ # The shell's alias definitions: a name maps to the replacement text the lexer
6
+ # splices in when the name appears in command position (POSIX 2.3.1). The
7
+ # `alias` builtin validates names; the table just stores what it is given.
8
+ class AliasTable
9
+ extend T::Sig
10
+
11
+ sig { void }
12
+ def initialize
13
+ @aliases = {}
14
+ end
15
+
16
+ sig { params(name: String, value: String).returns(String) }
17
+ def define(name, value)
18
+ @aliases[name] = value
19
+ end
20
+
21
+ sig { params(name: String).returns(T.nilable(String)) }
22
+ def remove(name)
23
+ @aliases.delete(name)
24
+ end
25
+
26
+ sig { params(name: String).returns(T.nilable(String)) }
27
+ def value(name)
28
+ @aliases.fetch(name, nil)
29
+ end
30
+
31
+ sig { params(name: String).returns(T::Boolean) }
32
+ def key?(name)
33
+ @aliases.key?(name)
34
+ end
35
+
36
+ sig { void }
37
+ def clear
38
+ @aliases.clear
39
+ end
40
+
41
+ # name => value pairs sorted by name, for `alias` with no operands. dash lists
42
+ # in hash order; rush sorts for a deterministic, testable listing.
43
+ sig { returns(T::Array[[String, String]]) }
44
+ def listing
45
+ @aliases.sort
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,48 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Rush
5
+ # Applies `name[=value]` declaration operands to shell variables. A missing `=`
6
+ # leaves the variable unchanged; `name=` assigns the empty string. The local
7
+ # path snapshots the parsed name before assignment.
8
+ class Assignments
9
+ extend T::Sig
10
+
11
+ sig { params(variables: ShellVariables).void }
12
+ def initialize(variables)
13
+ @variables = variables
14
+ end
15
+
16
+ sig { params(text: String).returns(String) }
17
+ def apply(text)
18
+ name, value = parse(text)
19
+ assign(name, value)
20
+ name
21
+ end
22
+
23
+ sig { params(text: String).returns(String) }
24
+ def apply_local(text)
25
+ name, value = parse(text)
26
+ @variables.declare_local(name)
27
+ assign(name, value)
28
+ name
29
+ end
30
+
31
+ private
32
+
33
+ sig { params(text: String).returns([String, T.nilable(String)]) }
34
+ def parse(text)
35
+ return [text, nil] unless text.include?('=')
36
+
37
+ parts = text.split('=', 2)
38
+ name = parts.fetch(0)
39
+ value = parts.fetch(1)
40
+ [name, value]
41
+ end
42
+
43
+ sig { params(name: String, value: T.nilable(String)).void }
44
+ def assign(name, value)
45
+ @variables.assign(name, value) if value
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,37 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Rush
5
+ module AST
6
+ # `pipeline && pipeline` / `pipeline || pipeline`, left-associative. Runs the
7
+ # left side, then the right side only when the operator's short-circuit allows.
8
+ class AndOr < Node
9
+ extend T::Sig
10
+
11
+ attr_reader :left, :op, :right
12
+
13
+ sig { params(left: Node, op: Symbol, right: Node).void }
14
+ def initialize(left, op, right)
15
+ @left = left
16
+ @op = op
17
+ @right = right
18
+ end
19
+
20
+ # The left side is non-final, so it runs in a tested context (errexit
21
+ # suppressed); only the final command's status reaches the errexit check.
22
+ sig { params(executor: Executor).returns(Status) }
23
+ def execute(executor)
24
+ status = executor.errexit.tested { executor.run(left) }
25
+ run_right?(status) ? executor.run(right) : status
26
+ end
27
+
28
+ private
29
+
30
+ sig { params(status: Status).returns(T::Boolean) }
31
+ def run_right?(status)
32
+ succeeded = status.success?
33
+ op == :and ? succeeded : !succeeded
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,20 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Rush
5
+ module AST
6
+ # `name=value` — value is an unexpanded Word (it receives tilde and the
7
+ # step-1 expansions, but no field splitting or pathname expansion).
8
+ Assignment = Data.define(:name, :value)
9
+
10
+ # Behaviour for the Data-defined assignment payload.
11
+ class Assignment
12
+ extend T::Sig
13
+
14
+ sig { returns(Integer) }
15
+ def source_line
16
+ value.source_line
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,25 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Rush
5
+ module AST
6
+ # `{ list; }` — groups commands without a subshell, so they run in the
7
+ # current shell environment. (Redirections on the group arrive with the
8
+ # fd-management slice.)
9
+ class BraceGroup < Node
10
+ extend T::Sig
11
+
12
+ attr_reader :body
13
+
14
+ sig { params(body: Node).void }
15
+ def initialize(body)
16
+ @body = body
17
+ end
18
+
19
+ sig { params(executor: Executor).returns(Status) }
20
+ def execute(executor)
21
+ executor.run(body)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,41 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Rush
5
+ module AST
6
+ # One `pattern|pattern) body ;;` arm of a case statement.
7
+ CaseItem = Data.define(:patterns, :body)
8
+
9
+ # `case word in pattern) body ;; ... esac`. Expands the subject, then runs the
10
+ # body of the first arm with a pattern (fnmatch glob) that matches it; no
11
+ # match yields status 0. There is no fall-through.
12
+ class Case < Node
13
+ extend T::Sig
14
+
15
+ attr_reader :word, :items
16
+
17
+ sig { params(word: Word, items: T::Array[CaseItem]).void }
18
+ def initialize(word, items)
19
+ @word = word
20
+ @items = items
21
+ end
22
+
23
+ sig { params(executor: Executor).returns(Status) }
24
+ def execute(executor)
25
+ subject = executor.expander.expand_value(word)
26
+ item = items.find { |candidate| matches?(executor, candidate, subject) }
27
+ item ? executor.run(item.body) : Status.success
28
+ end
29
+
30
+ private
31
+
32
+ sig { params(executor: Executor, item: CaseItem, subject: String).returns(T::Boolean) }
33
+ def matches?(executor, item, subject)
34
+ item.patterns.any? do |pattern|
35
+ expanded = executor.expander.expand_pattern(pattern)
36
+ executor.system.fnmatch?(expanded, subject, locale: executor.state.variables.locale_settings)
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,34 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Rush
5
+ module AST
6
+ # Shared shape of the two condition-driven loops: hold the condition and
7
+ # body, delegate to LoopRunner with the subclass's kind — :while runs the
8
+ # body while the condition succeeds, :until until it does.
9
+ class ConditionLoop < Node
10
+ extend T::Sig
11
+
12
+ attr_reader :condition, :body
13
+
14
+ sig { params(condition: Node, body: Node).void }
15
+ def initialize(condition, body)
16
+ @condition = condition
17
+ @body = body
18
+ end
19
+
20
+ sig { params(executor: Executor).returns(Status) }
21
+ def execute(executor)
22
+ LoopRunner.new(executor, condition, body, kind).call
23
+ end
24
+
25
+ private
26
+
27
+ # The LoopRunner mode this node runs in.
28
+ sig { returns(Symbol) }
29
+ def kind
30
+ raise NotImplementedError
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,34 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Rush
5
+ module AST
6
+ # `for name [in words]; do body; done`. With an explicit word list the words
7
+ # are expanded (and field-split); with no `in` clause the loop iterates over
8
+ # the positional parameters ($@).
9
+ class For < Node
10
+ extend T::Sig
11
+
12
+ attr_reader :name, :words, :body
13
+
14
+ sig { params(name: String, words: T.nilable(T::Array[Word]), body: Node).void }
15
+ def initialize(name, words, body)
16
+ @name = name
17
+ @words = words
18
+ @body = body
19
+ end
20
+
21
+ sig { params(executor: Executor).returns(Status) }
22
+ def execute(executor)
23
+ ForRunner.new(executor, name, values(executor), body).call
24
+ end
25
+
26
+ private
27
+
28
+ sig { params(executor: Executor).returns(T::Array[String]) }
29
+ def values(executor)
30
+ words ? executor.expander.expand(words) : executor.state.positional.to_a
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,26 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Rush
5
+ module AST
6
+ # `name() compound-command` — registers the body in the function table and
7
+ # succeeds. The body runs (in the current shell) when the name is invoked.
8
+ class FunctionDef < Node
9
+ extend T::Sig
10
+
11
+ attr_reader :name, :body
12
+
13
+ sig { params(name: String, body: Node).void }
14
+ def initialize(name, body)
15
+ @name = name
16
+ @body = body
17
+ end
18
+
19
+ sig { params(executor: Executor).returns(Status) }
20
+ def execute(executor)
21
+ executor.state.functions.define(name, body)
22
+ Status.success
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,29 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Rush
5
+ module AST
6
+ # `if cond; then body; [elif/else ...] fi`. Runs the condition list; on
7
+ # success runs the consequent, otherwise the alternative (an else List, a
8
+ # nested If for elif, or nil). With no matching branch the status is 0.
9
+ class If < Node
10
+ extend T::Sig
11
+
12
+ attr_reader :condition, :consequent, :alternative
13
+
14
+ sig { params(condition: Node, consequent: Node, alternative: T.nilable(Node)).void }
15
+ def initialize(condition, consequent, alternative)
16
+ @condition = condition
17
+ @consequent = consequent
18
+ @alternative = alternative
19
+ end
20
+
21
+ sig { params(executor: Executor).returns(Status) }
22
+ def execute(executor)
23
+ return executor.run(consequent) if executor.succeeds?(condition)
24
+
25
+ alternative ? executor.run(alternative) : Status.success
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,47 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Rush
5
+ module AST
6
+ # One element of a list: an and-or list and whether it was terminated by `&`.
7
+ ListEntry = Data.define(:and_or, :async)
8
+
9
+ # A `;` / `&` / newline-separated list of and-or lists and the program root.
10
+ # Runs each entry in order; the list's status is the last entry's. An empty
11
+ # list (a blank or comment-only line, which is its own program under
12
+ # command-by-command reading) preserves $?, like dash. Async (`&`) entries
13
+ # fork a background subshell and return launch status in the parent.
14
+ class List < Node
15
+ extend T::Sig
16
+
17
+ attr_reader :entries
18
+
19
+ sig { params(entries: T::Array[ListEntry]).void }
20
+ def initialize(entries)
21
+ @entries = entries
22
+ end
23
+
24
+ sig { params(executor: Executor).returns(Status) }
25
+ def execute(executor)
26
+ entries.reduce(executor.state.last_status) { |_status, entry| run_entry(executor, entry) }
27
+ end
28
+
29
+ # A blank or comment-only program runs no command; SourceRunner skips it
30
+ # when tracking eval/dot's result status.
31
+ sig { returns(T::Boolean) }
32
+ def empty?
33
+ entries.empty?
34
+ end
35
+
36
+ private
37
+
38
+ # An async (&) command is exempt from errexit, so it runs in a tested context.
39
+ sig { params(executor: Executor, entry: ListEntry).returns(Status) }
40
+ def run_entry(executor, entry)
41
+ return executor.errexit.tested { executor.run_async(entry.and_or) } if entry.async
42
+
43
+ executor.run(entry.and_or)
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,17 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Rush
5
+ module AST
6
+ # Base for every AST node. Subclasses implement #execute(executor) so the
7
+ # executor dispatches polymorphically and never switches on node type.
8
+ class Node
9
+ extend T::Sig
10
+
11
+ sig { params(_executor: Executor).returns(Status) }
12
+ def execute(_executor)
13
+ raise NotImplementedError
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,59 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Rush
5
+ module AST
6
+ # Parses the body of ${...}: a parameter name plus an optional operator — the
7
+ # default/assign/error/alternative forms (- = ? +, optionally ':'-prefixed),
8
+ # the prefix/suffix removal forms (# ## % %%), or the ${#name} length form
9
+ # (recognised before the others, since a leading # also names $#).
10
+ PARAM_BRACED = /\A(\w+|[@*#?$!0-])(:?[-=?+]|\#{1,2}|%{1,2})?(.*)\z/m
11
+
12
+ ParamRef = Data.define(:name, :op, :arg)
13
+
14
+ # A parameter reference parsed from $name or ${...}. The operator word is
15
+ # expanded lazily at expansion time.
16
+ class ParamRef
17
+ extend T::Sig
18
+
19
+ sig { params(name: String).returns(ParamRef) }
20
+ def self.simple(name)
21
+ new(name: name, op: nil, arg: nil)
22
+ end
23
+
24
+ sig { params(body: String).returns(ParamRef) }
25
+ def self.parse(body)
26
+ return new(name: body.delete_prefix('#'), op: '#len', arg: nil) if length?(body)
27
+
28
+ match = braced_match(body)
29
+ new(name: braced_name(match), op: match[2], arg: match[3])
30
+ end
31
+
32
+ sig { params(body: String).returns(T::Boolean) }
33
+ def self.length?(body)
34
+ body.start_with?('#') && body.length > 1
35
+ end
36
+
37
+ # dash's job text always braces a parameter — ${T}, "${@}", ${T:-9}
38
+ # with the op's raw source argument (CommandText).
39
+ sig { returns(String) }
40
+ def canon
41
+ return "${##{name}}" if op == '#len'
42
+
43
+ "${#{name}#{op}#{arg}}"
44
+ end
45
+
46
+ sig { params(body: String).returns(MatchData) }
47
+ def self.braced_match(body)
48
+ PARAM_BRACED.match(body) || raise(ExpansionError, 'bad substitution')
49
+ end
50
+ private_class_method :braced_match
51
+
52
+ sig { params(match: MatchData).returns(String) }
53
+ def self.braced_name(match)
54
+ match[1] || raise(ExpansionError, 'bad substitution')
55
+ end
56
+ private_class_method :braced_name
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,45 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Rush
5
+ module AST
6
+ # A pipeline of one or more commands, optionally negated with `!`. A single
7
+ # command runs in-process (so builtins affect the shell); a multi-stage
8
+ # pipeline forks each stage via PipelineRunner. Negation inverts the status.
9
+ class Pipeline < Node
10
+ extend T::Sig
11
+
12
+ attr_reader :commands, :negate
13
+
14
+ sig { params(commands: T::Array[Node], negate: T::Boolean).void }
15
+ def initialize(commands, negate)
16
+ @commands = commands
17
+ @negate = negate
18
+ end
19
+
20
+ # A negated pipeline (`! cmd`) is exempt from errexit and runs its stages in
21
+ # a tested context; otherwise the leaf status is the errexit check point.
22
+ sig { params(executor: Executor).returns(Status) }
23
+ def execute(executor)
24
+ return invert(executor.errexit.tested { run_stages(executor) }) if negate
25
+
26
+ executor.errexit.exit_on_error(run_stages(executor))
27
+ end
28
+
29
+ private
30
+
31
+ sig { params(executor: Executor).returns(Status) }
32
+ def run_stages(executor)
33
+ case commands
34
+ in [command] then executor.run(command)
35
+ else PipelineRunner.new(executor, commands).call
36
+ end
37
+ end
38
+
39
+ sig { params(status: Status).returns(Status) }
40
+ def invert(status)
41
+ status.success? ? Status.failure : Status.success
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,21 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Rush
5
+ module AST
6
+ # A redirection: a kind (:in/:out/:append/:dup_in/:dup_out/:readwrite/
7
+ # :clobber), an unexpanded target Word, and an optional explicit fd parsed
8
+ # from a leading IO_NUMBER (nil means the kind's default fd).
9
+ Redirect = Data.define(:kind, :target, :io_number)
10
+
11
+ # Behaviour for the Data-defined redirection payload.
12
+ class Redirect
13
+ extend T::Sig
14
+
15
+ sig { returns(Integer) }
16
+ def source_line
17
+ target.source_line
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,26 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Rush
5
+ module AST
6
+ # A compound command (group, subshell, loop, if, case) carrying redirections
7
+ # on the whole command: they build a base IoTable that every command in the
8
+ # body inherits, restored once the command finishes. Its status is the body's.
9
+ class Redirected < Node
10
+ extend T::Sig
11
+
12
+ attr_reader :command, :redirects
13
+
14
+ sig { params(command: Node, redirects: T::Array[Redirect]).void }
15
+ def initialize(command, redirects)
16
+ @command = command
17
+ @redirects = redirects
18
+ end
19
+
20
+ sig { params(executor: Executor).returns(Status) }
21
+ def execute(executor)
22
+ executor.run_redirected(command, redirects)
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,62 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Rush
5
+ module AST
6
+ # `name=val ... cmd arg ... >file` — variable assignments, argv words and
7
+ # redirections, preserving both grouped accessors and original source parts.
8
+ class SimpleCommand < Node
9
+ extend T::Sig
10
+
11
+ Part = T.type_alias { T.any(Assignment, Word, Redirect) }
12
+
13
+ attr_reader :parts, :source_line
14
+
15
+ sig do
16
+ params(assignments: T::Array[Assignment], words: T::Array[Word], redirects: T::Array[Redirect],
17
+ source_line: Integer).returns(SimpleCommand)
18
+ end
19
+ def self.from_groups(assignments, words, redirects, source_line: 1)
20
+ new(assignments + words + redirects, source_line: source_line)
21
+ end
22
+
23
+ sig { params(parts: T::Array[Part]).returns(Integer) }
24
+ def self.source_line(parts)
25
+ parts.map(&:source_line).min || 1
26
+ end
27
+
28
+ sig { params(parts: T::Array[Part], source_line: Integer).void }
29
+ def initialize(parts, source_line: self.class.source_line(parts))
30
+ @parts = parts
31
+ @source_line = source_line
32
+ end
33
+
34
+ sig { returns(T::Array[Assignment]) }
35
+ def assignments
36
+ result = T.let([], T::Array[Assignment])
37
+ parts.each { |part| result << part if part.is_a?(Assignment) }
38
+ result
39
+ end
40
+
41
+ sig { returns(T::Array[Word]) }
42
+ def words
43
+ result = T.let([], T::Array[Word])
44
+ parts.each { |part| result << part if part.is_a?(Word) }
45
+ result
46
+ end
47
+
48
+ sig { returns(T::Array[Redirect]) }
49
+ def redirects
50
+ result = T.let([], T::Array[Redirect])
51
+ parts.each { |part| result << part if part.is_a?(Redirect) }
52
+ result
53
+ end
54
+
55
+ sig { params(executor: Executor).returns(Status) }
56
+ def execute(executor)
57
+ executor.state.record_lineno(source_line)
58
+ executor.run_simple(self)
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,24 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Rush
5
+ module AST
6
+ # `( list )` — runs the list in a subshell (a forked child) so cd and
7
+ # variable changes do not affect the current shell. Its status is the list's.
8
+ class Subshell < Node
9
+ extend T::Sig
10
+
11
+ attr_reader :body
12
+
13
+ sig { params(body: Node).void }
14
+ def initialize(body)
15
+ @body = body
16
+ end
17
+
18
+ sig { params(executor: Executor).returns(Status) }
19
+ def execute(executor)
20
+ SubshellRunner.new(executor, body).call
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,18 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Rush
5
+ module AST
6
+ # `until cond; do body; done` — runs the body until the condition succeeds.
7
+ class Until < ConditionLoop
8
+ extend T::Sig
9
+
10
+ private
11
+
12
+ sig { returns(Symbol) }
13
+ def kind
14
+ :until
15
+ end
16
+ end
17
+ end
18
+ end