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,134 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ require 'strscan'
5
+
6
+ module Rush
7
+ class Lexer
8
+ # Scans one word from the shared StringScanner into an AST::Word of typed
9
+ # segments: literal runs (quote-removed, with a `quoted` flag) and :param
10
+ # segments for $name / ${...}. Handles the three quoting forms; command
11
+ # substitution and arithmetic arrive in later slices.
12
+ class WordScanner
13
+ extend T::Sig
14
+ include ScannerPredicates
15
+
16
+ TERMINATOR = /[ \t\n;&|<>()]/
17
+ DISPATCH = {
18
+ "'" => :single_quote, '"' => :double_quote, '\\' => :escape,
19
+ '$' => :dollar, '`' => :backtick
20
+ }.freeze
21
+
22
+ # Scan the next shell word from a live lexer scanner, stopping at the first
23
+ # unquoted terminator (blank / operator). `interactive` mirrors the lexer:
24
+ # an accumulating parse may ask for more input, the final one may not.
25
+ sig { params(scanner: StringScanner, interactive: T::Boolean).returns(AST::Word) }
26
+ def self.next_word(scanner, interactive:)
27
+ new(scanner, terminator: TERMINATOR, interactive: interactive).scan
28
+ end
29
+
30
+ # Scan a complete, already-delimited string (a ${...} operator word or
31
+ # arithmetic source) as one word: no terminators apply (blanks/operators
32
+ # are literal), only quote / $ / ` stay special.
33
+ sig { params(text: String).returns(AST::Word) }
34
+ def self.entire(text)
35
+ new(StringScanner.new(text), terminator: nil, interactive: false).scan
36
+ end
37
+
38
+ # terminator: the character class that ends a word, or nil in whole mode.
39
+ sig { params(scanner: StringScanner, terminator: T.nilable(Regexp), interactive: T::Boolean).void }
40
+ def initialize(scanner, terminator:, interactive:)
41
+ @scanner = scanner
42
+ @terminator = terminator
43
+ @interactive = interactive
44
+ @buffer = SegmentBuffer.new
45
+ end
46
+
47
+ sig { returns(AST::Word) }
48
+ def scan
49
+ loop do
50
+ char = @scanner.getch or return @buffer.word
51
+ return rewind_then_build if terminates?(char)
52
+
53
+ step(char)
54
+ end
55
+ end
56
+
57
+ private
58
+
59
+ sig { params(char: String).returns(T::Boolean) }
60
+ def terminates?(char)
61
+ terminator = @terminator
62
+ terminator ? char.match?(terminator) : false
63
+ end
64
+
65
+ sig { returns(AST::Word) }
66
+ def rewind_then_build
67
+ @scanner.unscan
68
+ @buffer.word
69
+ end
70
+
71
+ sig { params(char: String).void }
72
+ def step(char)
73
+ handler = DISPATCH.fetch(char, nil)
74
+ handler ? send(handler) : (@buffer << char)
75
+ end
76
+
77
+ sig { void }
78
+ def single_quote
79
+ content = @scanner.scan(/[^']*/)
80
+ raise IncompleteInput, 'unterminated single quote' unless @scanner.scan("'")
81
+
82
+ push(content, quoted: true)
83
+ end
84
+
85
+ sig { void }
86
+ def double_quote
87
+ DoubleQuoteScanner.new(@scanner, @buffer, -> { continuation }).scan
88
+ end
89
+
90
+ # Line continuation (POSIX 2.2.1): the backslash-newline pair vanishes.
91
+ # When the newline ends an accumulating (interactive) buffer, the logical
92
+ # line continues on input the lexer does not have yet, so ask the reader
93
+ # for the next line. The final parse at end of input — and `entire` mode,
94
+ # whose text is already complete — just drops the pair, like dash.
95
+ sig { void }
96
+ def continuation
97
+ @scanner.skip(/\n/)
98
+ raise IncompleteInput, 'line continuation' if @interactive && @terminator && @scanner.eos?
99
+ end
100
+
101
+ # A lone `$` that begins no valid reference stays a literal dollar: merged
102
+ # into the current literal run when bare, a quoted literal segment in "...".
103
+ sig { void }
104
+ def dollar
105
+ @scanner.unscan
106
+ segment = DollarScanner.new(@scanner).read(quoted: false)
107
+ segment ? @buffer.push(segment) : (@buffer << '$')
108
+ end
109
+
110
+ sig { void }
111
+ def backtick
112
+ @scanner.unscan
113
+ @buffer.push(DollarScanner.new(@scanner).read_backtick(quoted: false))
114
+ end
115
+
116
+ sig { void }
117
+ def escape
118
+ char = @scanner.getch or return trailing_backslash
119
+ char == "\n" ? continuation : push(char, quoted: true)
120
+ end
121
+
122
+ # A backslash that ends the input stays literal, like dash.
123
+ sig { void }
124
+ def trailing_backslash
125
+ @buffer << '\\'
126
+ end
127
+
128
+ sig { params(value: T.untyped, quoted: T::Boolean).void }
129
+ def push(value, quoted:)
130
+ @buffer.push(AST::LiteralSegment.new(value, quoted))
131
+ end
132
+ end
133
+ end
134
+ end
data/lib/rush/lexer.rb ADDED
@@ -0,0 +1,174 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ require 'strscan'
5
+ require_relative 'lexer/operator_table'
6
+ require_relative 'lexer/lex_state'
7
+ require_relative 'lexer/source_lines'
8
+ require_relative 'lexer/scanner_predicates'
9
+ require_relative 'lexer/token_predicates'
10
+ require_relative 'lexer/quote_skips'
11
+ require_relative 'lexer/case_frame'
12
+ require_relative 'lexer/case_tracker'
13
+ require_relative 'lexer/paren_regions'
14
+ require_relative 'lexer/paren_reader'
15
+ require_relative 'lexer/substitution_reader'
16
+ require_relative 'lexer/braced_reader'
17
+ require_relative 'lexer/param_scanner'
18
+ require_relative 'lexer/quoted_word'
19
+ require_relative 'lexer/dollar_scanner'
20
+ require_relative 'lexer/double_quote_scanner'
21
+ require_relative 'lexer/word_scanner'
22
+ require_relative 'lexer/heredoc_body'
23
+ require_relative 'lexer/heredoc_reader'
24
+ require_relative 'lexer/token_classifier'
25
+ require_relative 'lexer/alias_expander'
26
+
27
+ module Rush
28
+ # StringScanner pump that yields [symbol, value] pairs for racc. It skips
29
+ # blanks and comments, then emits NEWLINE, an IO_NUMBER, an operator, or a
30
+ # WORD/ASSIGNMENT_WORD (classified against LexState, which advances after each
31
+ # token to track command position — the seed of POSIX Grammar Rules 1-9).
32
+ class Lexer
33
+ extend T::Sig
34
+ include TokenPredicates
35
+
36
+ # Between tokens, blanks, comments and backslash-newline continuations
37
+ # (POSIX 2.2.1) are all insignificant. A continuation at the buffer's very
38
+ # end is left for the word scanner, which asks for the next line instead
39
+ # of ending the token stream.
40
+ INSIGNIFICANT = /(?:[ \t]+|#[^\n]*|\\\n(?=.))+/m
41
+ # "Immediately before < or >" sees through spliced continuations, like the
42
+ # operator matcher: dash-verified, `2\<newline>>f` redirects fd 2.
43
+ IO_NUMBER = /\d+(?=(?:\\\n)*[<>])/
44
+ HEREDOC_OPS = { DLESS: :plain, DLESSDASH: :strip }.freeze
45
+
46
+ sig { params(source: String, interactive: T::Boolean, aliases: T.nilable(AliasTable), line_offset: Integer).void }
47
+ def initialize(source, interactive: false, aliases: nil, line_offset: 0)
48
+ @scanner = StringScanner.new(source)
49
+ @aliases = AliasExpander.new(aliases)
50
+ @interactive = interactive
51
+ @state = LexState.new
52
+ @lines = SourceLines.new(@scanner, line_offset)
53
+ @awaiting = nil
54
+ @heredocs = []
55
+ end
56
+
57
+ sig { returns(Integer) }
58
+ def location
59
+ @scanner.charpos
60
+ end
61
+
62
+ sig { returns([T.untyped, T.untyped]) }
63
+ def next_token
64
+ drain
65
+ return [false, false] if @scanner.eos?
66
+
67
+ token = scan_token
68
+ token ? emit(token) : next_token
69
+ end
70
+
71
+ private
72
+
73
+ sig { params(token: [T.untyped, T.untyped]).returns([T.untyped, T.untyped]) }
74
+ def emit(token)
75
+ @state.advance(token.first)
76
+ @aliases.spend
77
+ token
78
+ end
79
+
80
+ # Skip blanks and comments; when the current frame is an exhausted alias
81
+ # replacement, restore the input beneath it and keep skipping.
82
+ sig { void }
83
+ def drain
84
+ skip_insignificant
85
+ return unless @scanner.eos? && @aliases.nested?
86
+
87
+ @scanner = @aliases.pop
88
+ drain
89
+ end
90
+
91
+ sig { void }
92
+ def skip_insignificant
93
+ @scanner.skip(INSIGNIFICANT)
94
+ end
95
+
96
+ sig { returns(T.nilable([T.untyped, T.untyped])) }
97
+ def scan_token
98
+ return heredoc_newline if @scanner.scan("\n")
99
+
100
+ io_number || operator || word
101
+ end
102
+
103
+ sig { returns(T.nilable([T.untyped, T.untyped])) }
104
+ def io_number
105
+ digits = @scanner.scan(IO_NUMBER)
106
+ digits ? [:IO_NUMBER, Integer(digits, 10)] : nil
107
+ end
108
+
109
+ sig { returns(T.nilable([T.untyped, T.untyped])) }
110
+ def operator
111
+ matched = @scanner.scan(OperatorTable::PATTERN)
112
+ matched ? operator_token(matched) : nil
113
+ end
114
+
115
+ # The match may carry line continuations between its characters (see
116
+ # OperatorTable::PATTERN); splicing them out recovers the operator.
117
+ sig { params(matched: String).returns([T.untyped, T.untyped]) }
118
+ def operator_token(matched)
119
+ operator = matched.gsub(OperatorTable::CONTINUATION, '')
120
+ symbol = OperatorTable::OPERATORS.fetch(operator)
121
+ @awaiting = HEREDOC_OPS.fetch(symbol, nil)
122
+ [symbol, operator]
123
+ end
124
+
125
+ # A word awaited as a here-document delimiter is taken raw — never
126
+ # classified, aliased or reserved. Otherwise classify it, then (only a
127
+ # plain WORD) splice its alias replacement in its place; a splice returns
128
+ # nil so next_token re-reads from the new frame.
129
+ sig { returns(T.nilable([T.untyped, T.untyped])) }
130
+ def word
131
+ scanned = @lines.word { WordScanner.next_word(@scanner, interactive: @interactive) }
132
+ return delimiter(scanned) if @awaiting
133
+
134
+ token = TokenClassifier.new(scanned, @state).call
135
+ replacement = alias_for(token, scanned)
136
+ replacement ? splice(replacement) : token
137
+ end
138
+
139
+ sig { params(token: [T.untyped, T.untyped], word: AST::Word).returns(T.nilable(AliasExpander::Replacement)) }
140
+ def alias_for(token, word)
141
+ return if !word_token?(token) || !@state.command_mode?
142
+
143
+ @aliases.expand(word, @state.expects_command?)
144
+ end
145
+
146
+ sig { params(replacement: AliasExpander::Replacement).returns(NilClass) }
147
+ def splice(replacement)
148
+ @aliases.push(replacement, @scanner)
149
+ @scanner = StringScanner.new(replacement.value)
150
+ nil
151
+ end
152
+
153
+ sig { params(word: AST::Word).returns([T.untyped, T.untyped]) }
154
+ def delimiter(word)
155
+ holder = HereDoc.new(delimiter: word.segments.map(&:value).join,
156
+ quoted: word.segments.any?(&:quoted), strip: @awaiting == :strip,
157
+ source_line: word.source_line)
158
+ @awaiting = nil
159
+ @heredocs << holder
160
+ [:WORD, holder]
161
+ end
162
+
163
+ # On the newline that ends the command line, drain the pending here-docs:
164
+ # read each body from the lines that follow, in the order the `<<`s appeared.
165
+ sig { returns([T.untyped, T.untyped]) }
166
+ def heredoc_newline
167
+ start = @scanner.pos
168
+ HeredocReader.new(@scanner, interactive: @interactive).fill(@heredocs)
169
+ @heredocs = []
170
+ @lines.heredoc_newline(start)
171
+ [:NEWLINE, "\n"]
172
+ end
173
+ end
174
+ end
@@ -0,0 +1,29 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Rush
5
+ # Shared break/continue handling for the loop runners. A multi-level signal is
6
+ # re-raised with one fewer level so the next enclosing loop handles it; a
7
+ # single-level signal stops here and yields the last command status. Hosts must
8
+ # expose @executor.
9
+ module LoopControlHandling
10
+ extend T::Sig
11
+
12
+ private
13
+
14
+ sig { params(signal: LoopControl).returns(Status) }
15
+ def unwind(signal)
16
+ relayed = relay(signal)
17
+ # Kernel.raise (not bare raise): in this mixin Sorbet can't resolve a bare
18
+ # raise on the module's self.
19
+ Kernel.raise relayed if relayed
20
+
21
+ @executor.state.last_status
22
+ end
23
+
24
+ sig { params(signal: LoopControl).returns(T.nilable(LoopControl)) }
25
+ def relay(signal)
26
+ signal.class.new(signal.count - 1) if signal.count > 1
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,50 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Rush
5
+ # Lexical loop nesting for break/continue: the count of for/while/until loops
6
+ # enclosing the current command in the same execution environment. A function
7
+ # call or subshell starts fresh (#without), so a break inside a function cannot
8
+ # reach the caller's loop, while dot/eval/group bodies run inline and keep the
9
+ # count. break/continue read #depth to clamp their level and #any? to no-op
10
+ # when there is no enclosing loop.
11
+ class LoopNesting
12
+ extend T::Sig
13
+
14
+ sig { returns(Integer) }
15
+ attr_reader :depth
16
+
17
+ sig { void }
18
+ def initialize
19
+ @depth = 0
20
+ end
21
+
22
+ sig { void }
23
+ def enter
24
+ @depth += 1
25
+ end
26
+
27
+ sig { void }
28
+ def leave
29
+ @depth -= 1
30
+ end
31
+
32
+ sig { returns(T::Boolean) }
33
+ def any?
34
+ @depth.positive?
35
+ end
36
+
37
+ sig do
38
+ type_parameters(:U)
39
+ .params(blk: T.proc.returns(T.type_parameter(:U)))
40
+ .returns(T.type_parameter(:U))
41
+ end
42
+ def without(&blk)
43
+ saved = T.let(@depth, Integer)
44
+ @depth = 0
45
+ yield
46
+ ensure
47
+ @depth = T.must(saved)
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,55 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Rush
5
+ # Runs a while/until loop, catching break/continue at the loop boundary (see
6
+ # LoopControlHandling). The loop's status is the last body status (0 if the
7
+ # body never runs).
8
+ class LoopRunner
9
+ extend T::Sig
10
+
11
+ include LoopControlHandling
12
+
13
+ sig { params(executor: Executor, condition: AST::Node, body: AST::Node, sense: Symbol).void }
14
+ def initialize(executor, condition, body, sense)
15
+ @executor = executor
16
+ @condition = condition
17
+ @body = body
18
+ @sense = sense
19
+ end
20
+
21
+ # Bracket the loop so break/continue see the right nesting depth; leave runs
22
+ # even when break unwinds.
23
+ sig { returns(Status) }
24
+ def call
25
+ @executor.state.with_loop { run_loop }
26
+ end
27
+
28
+ private
29
+
30
+ sig { returns(Status) }
31
+ def run_loop
32
+ # T.let pins the loop variable's type: Status.success is now sig'd (Status),
33
+ # but #iterate is unsig'd (untyped), and Sorbet forbids a variable changing
34
+ # type across a loop (srb.help/7001). Steep ignores T.let (see sorbet_dsl.rbs).
35
+ status = T.let(Status.success, Status)
36
+ status = iterate while proceed?
37
+ status
38
+ rescue BreakSignal => e
39
+ unwind(e)
40
+ end
41
+
42
+ sig { returns(T::Boolean) }
43
+ def proceed?
44
+ met = @executor.succeeds?(@condition)
45
+ @sense == :while ? met : !met
46
+ end
47
+
48
+ sig { returns(Status) }
49
+ def iterate
50
+ @executor.run(@body)
51
+ rescue ContinueSignal => e
52
+ unwind(e)
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,39 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Rush
5
+ # A `-abc` / `+abc` cluster of single-letter options, the shape POSIX shares
6
+ # between the set builtin and the sh invocation line (XCU set / sh): the
7
+ # leading sign and the letters after it. parse answers nil for anything
8
+ # else — `--`, a lone dash or plus, or a plain operand.
9
+ class OptionCluster
10
+ extend T::Sig
11
+
12
+ sig { returns(String) }
13
+ attr_reader :sign
14
+
15
+ sig { returns(T::Array[String]) }
16
+ attr_reader :letters
17
+
18
+ sig { params(arg: T.nilable(String)).returns(T.nilable(OptionCluster)) }
19
+ def self.parse(arg)
20
+ return unless arg.is_a?(String) && arg.length > 1 && arg != '--' && arg.start_with?('-', '+')
21
+
22
+ new(arg)
23
+ end
24
+
25
+ sig { params(flag: String).void }
26
+ def initialize(flag)
27
+ sign, *letters = flag.chars
28
+ @sign = T.must(sign)
29
+ @letters = T.let(letters, T::Array[String])
30
+ end
31
+
32
+ # Yield each option letter with the cluster's sign, the shape both
33
+ # consumers toggle by.
34
+ sig { params(blk: T.proc.params(letter: String, sign: String).void).void }
35
+ def each_letter(&blk)
36
+ letters.each { |letter| yield(letter, sign) }
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,76 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Rush
5
+ # The shell options toggled by `set -[+]o name` / `set -[+]x` and friends
6
+ # (:errexit, :nounset, :noexec, :xtrace, :noglob, :verbose, :pipefail), plus the
7
+ # invocation-only :interactive (-i) and :stdin (-s) flags: the set of those
8
+ # currently on, with #set to flip one, #on? to query it, #letters to
9
+ # render $-, and the bare `set -o`/`set +o` listings. The letter/name
10
+ # tables live here so the set builtin and Invocation share one vocabulary.
11
+ class Options
12
+ extend T::Sig
13
+
14
+ LETTERS = { 'a' => :allexport, 'C' => :noclobber, 'e' => :errexit, 'u' => :nounset,
15
+ 'x' => :xtrace, 'f' => :noglob, 'v' => :verbose, 'm' => :monitor,
16
+ 'n' => :noexec }.freeze
17
+ # Every long name in dash's listing order — the bare `set -o`/`set +o`
18
+ # output shows the invocation-only flags too, like dash does.
19
+ NAMES = { 'errexit' => :errexit, 'noglob' => :noglob, 'interactive' => :interactive,
20
+ 'monitor' => :monitor, 'noexec' => :noexec, 'stdin' => :stdin, 'xtrace' => :xtrace,
21
+ 'verbose' => :verbose, 'noclobber' => :noclobber, 'allexport' => :allexport,
22
+ 'nounset' => :nounset, 'pipefail' => :pipefail }.freeze
23
+ # The names `set -o name` accepts: everything listable minus the flags
24
+ # only invocation can set.
25
+ LONG = NAMES.except('interactive', 'stdin').freeze
26
+ # $- renders enabled flags in dash's order (its option list, reversed), so
27
+ # differential corpus lines printing $- compare equal against the oracle.
28
+ DASH_ORDER = T.let([[:nounset, 'u'], [:allexport, 'a'], [:noclobber, 'C'], [:verbose, 'v'],
29
+ [:xtrace, 'x'], [:stdin, 's'], [:noexec, 'n'], [:monitor, 'm'], [:interactive, 'i'],
30
+ [:noglob, 'f'], [:errexit, 'e']].freeze, T::Array[[Symbol, String]])
31
+
32
+ sig { void }
33
+ def initialize
34
+ @enabled = Set.new
35
+ end
36
+
37
+ sig { params(name: Symbol, enabled: T::Boolean).void }
38
+ def set(name, enabled)
39
+ enabled ? @enabled.add(name) : @enabled.delete(name)
40
+ end
41
+
42
+ sig { params(name: Symbol).returns(T::Boolean) }
43
+ def on?(name)
44
+ @enabled.include?(name)
45
+ end
46
+
47
+ # The current option flags as the $- special parameter renders them.
48
+ sig { returns(String) }
49
+ def letters
50
+ DASH_ORDER.filter_map { |option, letter| letter if on?(option) }.join
51
+ end
52
+
53
+ # The bare `set -o` table rows in dash's format: the name padded to 16
54
+ # columns, then on/off.
55
+ sig { returns(T::Array[String]) }
56
+ def settings_rows
57
+ render_settings('%<name>-16son', '%<name>-16soff')
58
+ end
59
+
60
+ # The bare `set +o` output: `set -o name` / `set +o name` lines that
61
+ # reproduce the current settings when re-input (POSIX XCU set).
62
+ sig { returns(T::Array[String]) }
63
+ def reinput_lines
64
+ render_settings('set -o %<name>s', 'set +o %<name>s')
65
+ end
66
+
67
+ private
68
+
69
+ # One rendered line per long option name: the on form when the option is
70
+ # enabled, the off form otherwise.
71
+ sig { params(on_form: String, off_form: String).returns(T::Array[String]) }
72
+ def render_settings(on_form, off_form)
73
+ NAMES.map { |name, option| format(on?(option) ? on_form : off_form, name: name) }
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,39 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Rush
5
+ # A runtime string subjected to parameter expansion ONLY — the treatment
6
+ # POSIX gives PS1/PS2 prompts and the ENV pathname: $name and ${...} become
7
+ # ParamSegments via the shared ParamScanner; any other character — including
8
+ # command substitution, backticks and backslashes — is literal text. #word
9
+ # yields the segments for Expansion::Pipeline#expand_value.
10
+ class ParamText
11
+ extend T::Sig
12
+
13
+ sig { params(text: String).void }
14
+ def initialize(text)
15
+ @scanner = StringScanner.new(text)
16
+ @buffer = SegmentBuffer.new
17
+ end
18
+
19
+ sig { returns(AST::Word) }
20
+ def word
21
+ step until @scanner.eos?
22
+ @buffer.word
23
+ end
24
+
25
+ private
26
+
27
+ sig { void }
28
+ def step
29
+ char = T.must(@scanner.getch)
30
+ char == '$' ? dollar : @buffer << char
31
+ end
32
+
33
+ sig { void }
34
+ def dollar
35
+ ref = Lexer::ParamScanner.new(@scanner, quoted: false).read
36
+ ref ? @buffer.push(AST::ParamSegment.new(ref, false)) : @buffer << '$'
37
+ end
38
+ end
39
+ end