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,112 @@
1
+ # typed: false
2
+ # frozen_string_literal: true
3
+
4
+ module Rush
5
+ # The body of the racc-generated Rush::Parser (its `---- inner` block only
6
+ # `include`s this), kept in a normal linted/covered file. Rule actions call
7
+ # these factories; none contain node logic.
8
+ module ParserSupport
9
+ def initialize(lexer)
10
+ @lexer = lexer
11
+ end
12
+
13
+ def parse
14
+ do_parse
15
+ end
16
+
17
+ def next_token
18
+ @lexer.next_token
19
+ end
20
+
21
+ def on_error(token_id, value, _stack)
22
+ raise IncompleteInput, 'unexpected end of input' if value == false
23
+
24
+ near = value.is_a?(AST::Word) ? value.literal_text : value
25
+ raise ParseError, "syntax error at #{@lexer.location}: unexpected #{token_to_str(token_id)} `#{near}`"
26
+ end
27
+
28
+ private
29
+
30
+ def make_list(entries)
31
+ AST::List.new(entries.map { |and_or, sep| AST::ListEntry.new(and_or: and_or, async: sep == '&') })
32
+ end
33
+
34
+ def pending_entry(and_or)
35
+ [and_or, ';']
36
+ end
37
+
38
+ def terminate_list(entries, sep)
39
+ entries.last[1] = sep
40
+ entries
41
+ end
42
+
43
+ def append_and_or(entries, sep, and_or)
44
+ entries.last[1] = sep
45
+ entries << [and_or, ';']
46
+ end
47
+
48
+ def make_and_or(left, op, right)
49
+ AST::AndOr.new(left, op, right)
50
+ end
51
+
52
+ def make_pipeline(commands, negate)
53
+ AST::Pipeline.new(commands, negate)
54
+ end
55
+
56
+ def make_if(condition, consequent, alternative)
57
+ AST::If.new(condition, consequent, alternative)
58
+ end
59
+
60
+ def make_brace_group(body)
61
+ AST::BraceGroup.new(body)
62
+ end
63
+
64
+ def make_subshell(body)
65
+ AST::Subshell.new(body)
66
+ end
67
+
68
+ def make_redirected(command, redirects)
69
+ AST::Redirected.new(command, redirects)
70
+ end
71
+
72
+ def make_while(condition, body)
73
+ AST::While.new(condition, body)
74
+ end
75
+
76
+ def make_until(condition, body)
77
+ AST::Until.new(condition, body)
78
+ end
79
+
80
+ def make_for(name, words, body)
81
+ AST::For.new(name, words, body)
82
+ end
83
+
84
+ def make_case(word, items)
85
+ AST::Case.new(word, items)
86
+ end
87
+
88
+ def make_case_item(patterns, body)
89
+ AST::CaseItem.new(patterns: patterns, body: body)
90
+ end
91
+
92
+ def make_function(word, body)
93
+ AST::FunctionDef.new(word.literal_text, body)
94
+ end
95
+
96
+ def make_simple_command(prefix, word, suffix)
97
+ AST::SimpleCommand.new(prefix + [word].compact + suffix)
98
+ end
99
+
100
+ def make_redirect(kind, target)
101
+ AST::Redirect.new(kind: kind, target: target, io_number: nil)
102
+ end
103
+
104
+ def make_heredoc(holder)
105
+ AST::Redirect.new(kind: :heredoc, target: holder, io_number: nil)
106
+ end
107
+
108
+ def with_io_number(redirect, number)
109
+ redirect.with(io_number: number)
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,35 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ require 'strscan'
5
+
6
+ module Rush
7
+ # Shared left-to-right shell-pattern scanner. Concrete compilers implement
8
+ # the four append_* handlers; this class owns token dispatch and cursor
9
+ # progress so the Ruby fallback and native ERE translator cannot drift.
10
+ class PatternScanner
11
+ extend T::Sig
12
+
13
+ HANDLERS = { '\\' => :append_escape, '*' => :append_wildcard,
14
+ '?' => :append_wildcard, '[' => :append_bracket }.freeze
15
+
16
+ sig { params(pattern: String).void }
17
+ def initialize(pattern)
18
+ @scanner = StringScanner.new(pattern)
19
+ scan
20
+ end
21
+
22
+ private
23
+
24
+ sig { void }
25
+ def scan
26
+ step until @scanner.eos?
27
+ end
28
+
29
+ sig { void }
30
+ def step
31
+ handler = HANDLERS.fetch(@scanner.peek(1), :append_literal)
32
+ send(handler)
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,57 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Rush
5
+ # Coalesces caught signals until the evaluator reaches a safe command
6
+ # boundary. POSIX does not require multiple instances of one signal to queue;
7
+ # dash drains distinct pending signals in signal-number order.
8
+ class PendingSignals
9
+ extend T::Sig
10
+
11
+ sig { void }
12
+ def initialize
13
+ @names = T.let(empty, T::Hash[String, T::Boolean])
14
+ end
15
+
16
+ # Called from Ruby's Signal.trap block: deliberately only one Hash write,
17
+ # with no parser, evaluator, mutex or user shell code in handler context.
18
+ sig { params(name: String).void }
19
+ def record(name)
20
+ @names[name] = true
21
+ end
22
+
23
+ sig { params(blk: T.proc.params(name: String).void).void }
24
+ def drain(&blk)
25
+ take.each(&blk) until @names.empty?
26
+ end
27
+
28
+ sig { returns(T.nilable(String)) }
29
+ def first
30
+ @names.keys.min_by { |name| Signals.number(name) }
31
+ end
32
+
33
+ sig { returns(T::Boolean) }
34
+ def any?
35
+ @names.any?
36
+ end
37
+
38
+ sig { void }
39
+ def clear
40
+ @names.clear
41
+ end
42
+
43
+ private
44
+
45
+ sig { returns(T::Array[String]) }
46
+ def take
47
+ names = @names
48
+ @names = empty
49
+ names.keys.sort_by { |name| Signals.number(name) }
50
+ end
51
+
52
+ sig { returns(T::Hash[String, T::Boolean]) }
53
+ def empty
54
+ {} #: Hash[String, bool]
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,173 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Rush
5
+ # Runs a multi-stage pipeline: a pipe between each pair of stages, every stage
6
+ # forked (so they run concurrently and never deadlock on a full pipe buffer),
7
+ # the parent's pipe ends closed, then waitpid for all. The pipeline's status is
8
+ # normally the last stage's, or the rightmost non-zero stage's under pipefail.
9
+ # A stage is an arbitrary command — a simple command, but
10
+ # also a group/subshell/if/while/for/case or a function call — so it is run via
11
+ # the executor with the stage's pipe ends bound as the base IoTable. Each stage
12
+ # is a subshell environment (POSIX 2.12): exit/return end only the stage, loop
13
+ # control is inert, traps reset, and a fatal error aborts just that stage —
14
+ # SubshellRunner#run_body is exactly that resolution, so stages run through it.
15
+ # `start_stage` is the one irreducible fork/exit wrapper; the child-side
16
+ # `run_stage` (and its fd setup) is tested directly.
17
+ class PipelineRunner
18
+ extend T::Sig
19
+
20
+ # One stage of the pipeline: its position plus the shared pipe array and the
21
+ # stage count. The fd topology — which pipe end feeds this stage (input) and
22
+ # which it feeds (output), and so which ends to keep open (ends) — derives
23
+ # from all three, so the runner threads a single Stage rather than the
24
+ # (index, pipes) pair through every per-stage step.
25
+ class Stage
26
+ extend T::Sig
27
+
28
+ sig { returns(Integer) }
29
+ attr_reader :index, :count
30
+
31
+ sig { returns(T::Array[[IO, IO]]) }
32
+ attr_reader :pipes
33
+
34
+ sig { params(index: Integer, pipes: T::Array[[IO, IO]], count: Integer).void }
35
+ def initialize(index, pipes, count)
36
+ @index = index
37
+ @pipes = pipes
38
+ @count = count
39
+ end
40
+
41
+ sig { returns(T::Boolean) }
42
+ def last?
43
+ index == count - 1
44
+ end
45
+
46
+ sig { returns(T.nilable(IO)) }
47
+ def input
48
+ index.positive? ? pipes.fetch(index - 1).first : nil
49
+ end
50
+
51
+ sig { returns(T.nilable(IO)) }
52
+ def output
53
+ last? ? nil : pipes.fetch(index).last
54
+ end
55
+
56
+ sig { returns(T::Array[IO]) }
57
+ def ends
58
+ [input, output].compact
59
+ end
60
+
61
+ # Layer this stage's pipe ends over the base IoTable: stdin from the
62
+ # previous pipe (unless first), stdout to the next pipe (unless last).
63
+ sig { params(base: IoTable).returns(IoTable) }
64
+ def io(base)
65
+ base = base.with(0, input) if input
66
+ output ? base.with(1, output) : base
67
+ end
68
+
69
+ sig { void }
70
+ def close_unused
71
+ keep = ends
72
+ pipes.each { |pipe| close_unkept(pipe, keep) }
73
+ end
74
+
75
+ private
76
+
77
+ sig { params(pipe: [IO, IO], keep: T::Array[IO]).void }
78
+ def close_unkept(pipe, keep)
79
+ pipe.each { |io| io.close unless keep.include?(io) }
80
+ end
81
+ end
82
+
83
+ sig { params(executor: Executor, commands: T::Array[AST::Node]).void }
84
+ def initialize(executor, commands)
85
+ @executor = executor
86
+ @commands = commands
87
+ end
88
+
89
+ # The whole pipeline is one job: under job control its group (led by the
90
+ # first stage) owns the terminal until every stage has settled, so the
91
+ # reclaim wraps the full wait, not each stage's — and a ^Z parks all of
92
+ # it as one Stopped entry.
93
+ sig { returns(Status) }
94
+ def call
95
+ pipes = build_pipes
96
+ pids = start_stages(pipes)
97
+ close_all(pipes)
98
+ @executor.job_control.foreground(pids, text: text) { wait(pids) }
99
+ end
100
+
101
+ private
102
+
103
+ # The adopted-job text of a ^Z-parked pipeline (nil without job control).
104
+ sig { returns(T.nilable(String)) }
105
+ def text
106
+ CommandText.stages(@commands) if @executor.job_control.monitored?
107
+ end
108
+
109
+ sig { returns(T::Array[[IO, IO]]) }
110
+ def build_pipes
111
+ Array.new(@commands.size - 1) { @executor.system.pipe }
112
+ end
113
+
114
+ # Stages fork sequentially, each joining the first stage's process group
115
+ # under job control (dash: one group per pipeline, first process is the
116
+ # leader). Because stage 0's group is settled — parent-side setpgid —
117
+ # before stage 1 is even forked, later members can never race the group
118
+ # into existence.
119
+ sig { params(pipes: T::Array[[IO, IO]]).returns(T::Array[Integer]) }
120
+ def start_stages(pipes)
121
+ @commands.each_index.with_object([]) do |index, pids|
122
+ pids << start_stage(Stage.new(index, pipes, @commands.size), pids.first)
123
+ end
124
+ end
125
+
126
+ sig { params(stage: Stage, leader: T.nilable(Integer)).returns(Integer) }
127
+ def start_stage(stage, leader)
128
+ @executor.job_control.launch(leader: leader) { @executor.system.exit!(run_stage(stage).exitstatus) }
129
+ end
130
+
131
+ # Child-side. The relay is armed before the body (and its subshell
132
+ # reset) runs: a stage of a monitor-mode job propagates a member's
133
+ # stop upward instead of blocking on it (rush-l4o).
134
+ sig { params(stage: Stage).returns(Status) }
135
+ def run_stage(stage)
136
+ close_unused(stage)
137
+ @executor.jobs.control.arm_stage_relay
138
+ body = @commands.fetch(stage.index)
139
+ @executor.with_io(stage.io(@executor.io)) { SubshellRunner.new(@executor, body).run_body }
140
+ end
141
+
142
+ sig { params(stage: Stage).void }
143
+ def close_unused(stage)
144
+ stage.close_unused
145
+ end
146
+
147
+ sig { params(pipes: T::Array[[IO, IO]]).void }
148
+ def close_all(pipes)
149
+ pipes.each { |pipe| pipe.each(&:close) }
150
+ end
151
+
152
+ sig { params(pids: T::Array[Integer]).returns(Status) }
153
+ def wait(pids)
154
+ statuses = PipelineStatuses.new(pids.map { |pid| wait_status(pid) })
155
+ pipefail? ? statuses.pipefail_verdict : statuses.verdict
156
+ end
157
+
158
+ # A directly-killed stage reports on the shell's stderr, like dash's
159
+ # per-stage printsignal (probed: two killed stages print two lines, in
160
+ # stage order, unmoved by the stages' own redirects). The common case —
161
+ # an external dying inside the stage — reports from the stage
162
+ # supervisor's own External instead.
163
+ sig { params(pid: Integer).returns(Status) }
164
+ def wait_status(pid)
165
+ SignalReport.report(@executor.jobs.await(pid), @executor.io.get(2))
166
+ end
167
+
168
+ sig { returns(T::Boolean) }
169
+ def pipefail?
170
+ @executor.state.options.on?(:pipefail)
171
+ end
172
+ end
173
+ end
@@ -0,0 +1,45 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Rush
5
+ # Chooses the observable status for a settled multi-stage pipeline: the
6
+ # last stage's (or, under pipefail, the rightmost failure), with any
7
+ # stage's stop signal riding along — a ^Z parks the whole job even when
8
+ # the final stage had already exited (Status#with_stop, dash-probed).
9
+ class PipelineStatuses
10
+ extend T::Sig
11
+
12
+ sig { params(entries: T::Array[Status]).void }
13
+ def initialize(entries)
14
+ @entries = entries
15
+ end
16
+
17
+ sig { returns(Status) }
18
+ def verdict
19
+ last_stage.with_stop(job_stopsig)
20
+ end
21
+
22
+ sig { returns(Status) }
23
+ def pipefail_verdict
24
+ pipefail.with_stop(job_stopsig)
25
+ end
26
+
27
+ sig { returns(Status) }
28
+ def last_stage
29
+ @entries.fetch(-1)
30
+ end
31
+
32
+ sig { returns(Status) }
33
+ def pipefail
34
+ @entries.reverse_each { |status| return status unless status.success? }
35
+ Status.success
36
+ end
37
+
38
+ private
39
+
40
+ sig { returns(T.nilable(Integer)) }
41
+ def job_stopsig
42
+ @entries.filter_map(&:stopsig).first
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,45 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ require 'forwardable'
5
+
6
+ module Rush
7
+ # The positional parameters $1..$n: a list of strings that `set` replaces,
8
+ # `shift` drops the front of, and a function call brackets with #with so the
9
+ # caller's are restored on return. Reads ($1, $#, $@, the for-loop word list)
10
+ # delegate to the underlying array.
11
+ class Positional
12
+ extend T::Sig
13
+ extend Forwardable
14
+
15
+ def_delegators :@values, :==, :[], :size, :empty?, :join, :map, :each, :to_a
16
+
17
+ sig { params(values: T::Array[String]).void }
18
+ def initialize(values = [])
19
+ @values = values
20
+ end
21
+
22
+ sig { params(values: T::Array[String]).void }
23
+ def replace(values)
24
+ @values = values
25
+ end
26
+
27
+ sig { params(count: Integer).void }
28
+ def shift(count)
29
+ @values = @values.drop(count)
30
+ end
31
+
32
+ sig do
33
+ type_parameters(:U)
34
+ .params(values: T::Array[String], blk: T.proc.returns(T.type_parameter(:U)))
35
+ .returns(T.type_parameter(:U))
36
+ end
37
+ def with(values, &blk)
38
+ saved = T.let(@values, T::Array[String])
39
+ @values = values
40
+ yield
41
+ ensure
42
+ @values = T.must(saved)
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,78 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Rush
5
+ # Translates one shell pattern into a POSIX extended regular expression. The
6
+ # libc regex engine then supplies LC_COLLATE equivalence classes, collating
7
+ # symbols and locale ranges that Ruby's regexp/fnmatch APIs do not expose.
8
+ # :reek:InstanceVariableAssumption -- @scanner is initialized by PatternScanner#initialize
9
+ class PosixPattern < PatternScanner
10
+ extend T::Sig
11
+
12
+ ERE_SPECIAL = /[.\^$+(){}|\[\]*?\\]/
13
+ RUBY_GLOB_SPECIAL = %w[{ }].freeze
14
+
15
+ sig { returns(String) }
16
+ attr_reader :source, :glob_source
17
+
18
+ sig { params(pattern: String).void }
19
+ def initialize(pattern)
20
+ @source = +'^'
21
+ @glob_source = +''
22
+ super
23
+ @source << '$'
24
+ end
25
+
26
+ private
27
+
28
+ sig { void }
29
+ def append_escape
30
+ @scanner.getch
31
+ escaped = @scanner.getch || '\\'
32
+ @glob_source << '\\' << escaped
33
+ append_ere_literal(escaped)
34
+ end
35
+
36
+ sig { void }
37
+ def append_wildcard
38
+ char = @scanner.getch.to_s
39
+ append_discovery(char)
40
+ @source << (char == '*' ? '.*' : '.')
41
+ end
42
+
43
+ sig { void }
44
+ def append_bracket
45
+ bracket = BracketExpression.parse(@scanner.string, @scanner.pos)
46
+ return append_literal unless bracket
47
+
48
+ @source << bracket_source(bracket)
49
+ append_discovery('*')
50
+ @scanner.pos = bracket.finish
51
+ end
52
+
53
+ sig { params(bracket: BracketExpression).returns(String) }
54
+ def bracket_source(bracket)
55
+ source = bracket.source.sub(/\A\[!/, '[^')
56
+ source.include?('\\') ? EscapedBracket.new(source).source : source
57
+ end
58
+
59
+ sig { void }
60
+ def append_literal
61
+ char = @scanner.getch.to_s
62
+ append_discovery(char)
63
+ append_ere_literal(char)
64
+ end
65
+
66
+ sig { params(char: String).void }
67
+ def append_discovery(char)
68
+ return if char == '*' && @glob_source.end_with?('*')
69
+
70
+ @glob_source << (RUBY_GLOB_SPECIAL.include?(char) ? "\\#{char}" : char)
71
+ end
72
+
73
+ sig { params(char: String).void }
74
+ def append_ere_literal(char)
75
+ @source << (char.match?(ERE_SPECIAL) ? "\\#{char}" : char)
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,89 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Rush
5
+ # The program text and shell parameters implied by
6
+ # `-c command_string [command_name [argument...]]`.
7
+ class CommandInput
8
+ extend T::Sig
9
+
10
+ sig { params(operands: T::Array[String]).void }
11
+ def initialize(operands)
12
+ @operands = operands
13
+ end
14
+
15
+ sig { returns(String) }
16
+ def name
17
+ @operands.fetch(1, 'rush')
18
+ end
19
+
20
+ sig { returns(T::Array[String]) }
21
+ def positionals
22
+ @operands.drop(2)
23
+ end
24
+
25
+ sig { returns(String) }
26
+ def source
27
+ @operands.fetch(0) { raise InvocationError, '-c requires an argument' }
28
+ end
29
+ end
30
+
31
+ # The program text and shell parameters implied by a script-file operand:
32
+ # the path becomes $0, later operands the positional parameters, and the
33
+ # file is read lazily — unreadable files are an invocation error (status 2,
34
+ # like dash).
35
+ class ScriptInput
36
+ extend T::Sig
37
+
38
+ sig { params(operands: T::Array[String], system: SystemCalls).void }
39
+ def initialize(operands, system)
40
+ @operands = operands
41
+ @system = system
42
+ end
43
+
44
+ sig { returns(String) }
45
+ def name
46
+ @operands.fetch(0)
47
+ end
48
+
49
+ sig { returns(T::Array[String]) }
50
+ def positionals
51
+ @operands.drop(1)
52
+ end
53
+
54
+ sig { returns(String) }
55
+ def source
56
+ @system.read_file(name)
57
+ rescue SystemCallError => e
58
+ raise InvocationError, "cannot open #{name}: #{e.message}"
59
+ end
60
+ end
61
+
62
+ # The program text and shell parameters implied by reading stdin (implicitly
63
+ # or via -s): the operands become the positional parameters and $0 stays the
64
+ # shell name.
65
+ class StdinInput
66
+ extend T::Sig
67
+
68
+ sig { params(operands: T::Array[String], system: SystemCalls).void }
69
+ def initialize(operands, system)
70
+ @operands = operands
71
+ @system = system
72
+ end
73
+
74
+ sig { returns(String) }
75
+ def name
76
+ 'rush'
77
+ end
78
+
79
+ sig { returns(T::Array[String]) }
80
+ def positionals
81
+ @operands
82
+ end
83
+
84
+ sig { returns(String) }
85
+ def source
86
+ @system.stdin.read
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,69 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Rush
5
+ # Reads a shell source one line at a time, accumulating until the buffer parses
6
+ # to a complete program. POSIX reads and executes command by command, so an
7
+ # earlier command (a function definition, a future `alias`) can shape later
8
+ # input and a syntax error only stops the commands that follow it — the ones
9
+ # before it have already run. IncompleteInput means "the construct is
10
+ # unfinished": pull another line (multi-line here-documents included). At end of
11
+ # input a pending buffer gets one final, non-interactive parse so an
12
+ # unterminated here-document still runs with the body so far, exactly like dash.
13
+ class ProgramReader
14
+ extend T::Sig
15
+
16
+ sig do
17
+ params(aliases: AliasTable,
18
+ next_line: T.proc.params(buffered: T::Boolean).returns(T.nilable(String))).void
19
+ end
20
+ def initialize(aliases: AliasTable.new, &next_line)
21
+ @next_line = next_line
22
+ @aliases = aliases
23
+ @buffer = +''
24
+ @lines = SourceLineCounter.new
25
+ end
26
+
27
+ # The next complete program, or :eof. Raises ParseError on a real syntax
28
+ # error, so the caller decides policy: batch aborts, the REPL reports and
29
+ # resumes.
30
+ sig { returns(T.any(AST::List, Symbol)) }
31
+ def next_program
32
+ @buffer = +''
33
+ loop do
34
+ outcome = read_more
35
+ return outcome unless outcome == :more
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ sig { returns(T.any(AST::List, Symbol)) }
42
+ def read_more
43
+ line = @next_line.call(!@buffer.empty?)
44
+ return finish unless line
45
+
46
+ record_line(line)
47
+ attempt(@buffer << line)
48
+ end
49
+
50
+ sig { params(line: String).void }
51
+ def record_line(line)
52
+ @buffer.empty? ? @lines.start(line) : @lines.continue(line)
53
+ end
54
+
55
+ sig { params(source: String).returns(T.any(AST::List, Symbol)) }
56
+ def attempt(source)
57
+ Parser.new(Lexer.new(source, interactive: true, aliases: @aliases, line_offset: @lines.offset)).parse
58
+ rescue IncompleteInput
59
+ :more
60
+ end
61
+
62
+ sig { returns(T.any(AST::List, Symbol)) }
63
+ def finish
64
+ return :eof if @buffer.empty?
65
+
66
+ Parser.new(Lexer.new(@buffer, interactive: false, aliases: @aliases, line_offset: @lines.offset)).parse
67
+ end
68
+ end
69
+ end