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,37 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Rush
5
+ # Calls a shell function: bind the arguments as the positional parameters, run
6
+ # the body in the current shell, and restore the previous positionals. A scope
7
+ # is opened so `local` declarations are undone on return. `return` unwinds to
8
+ # here as a ReturnSignal.
9
+ class FunctionRunner
10
+ extend T::Sig
11
+
12
+ sig { params(executor: Executor, body: AST::Node, args: T::Array[String]).void }
13
+ def initialize(executor, body, args)
14
+ @executor = executor
15
+ @state = executor.state
16
+ @body = body
17
+ @args = args
18
+ end
19
+
20
+ # A function body runs inside a ShellState-owned function frame: locals are
21
+ # scoped, positionals are rebound, and loop depth is reset (POSIX 2.9.5).
22
+ sig { returns(Status) }
23
+ def call
24
+ @state.with_function_frame(@args) { invoke }
25
+ end
26
+
27
+ private
28
+
29
+ sig { returns(Status) }
30
+ def invoke
31
+ @executor.run(@body)
32
+ @state.last_status
33
+ rescue ReturnSignal => e
34
+ Status.new(e.code)
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,35 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Rush
5
+ # Name -> function body (an AST node), defined by function definitions and
6
+ # looked up during command dispatch.
7
+ class FunctionTable
8
+ extend T::Sig
9
+
10
+ sig { void }
11
+ def initialize
12
+ @functions = {}
13
+ end
14
+
15
+ sig { params(name: String, body: AST::Node).returns(AST::Node) }
16
+ def define(name, body)
17
+ @functions[name] = body
18
+ end
19
+
20
+ sig { params(name: String).returns(T.nilable(AST::Node)) }
21
+ def undefine(name)
22
+ @functions.delete(name)
23
+ end
24
+
25
+ sig { params(name: String).returns(AST::Node) }
26
+ def fetch(name)
27
+ @functions.fetch(name)
28
+ end
29
+
30
+ sig { params(name: String).returns(T::Boolean) }
31
+ def key?(name)
32
+ @functions.key?(name)
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,195 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Rush
5
+ # Normal vs silent getopts error-mode formatting for bad options and missing
6
+ # arguments.
7
+ class GetoptsErrorMode
8
+ extend T::Sig
9
+
10
+ sig { returns(String) }
11
+ attr_reader :missing_name
12
+
13
+ sig { params(mode: Symbol).void }
14
+ def initialize(mode)
15
+ @missing_name = mode == :silent ? ':' : '?'
16
+ @keep_optarg = mode == :silent
17
+ @diagnostic = mode == :normal
18
+ end
19
+
20
+ sig { params(value: String).returns(T.nilable(String)) }
21
+ def optarg(value)
22
+ value if @keep_optarg
23
+ end
24
+
25
+ sig { params(text: String).returns(T.nilable(String)) }
26
+ def message(text)
27
+ text if @diagnostic
28
+ end
29
+ end
30
+
31
+ # The getopts optstring operand (POSIX `getopts optstring name [arg...]`):
32
+ # a leading colon selects silent error reporting, then each option letter,
33
+ # `:`-suffixed when it requires an argument. Owns which letters are legal
34
+ # and which take arguments; the parser keeps the cursor choreography.
35
+ class Optstring
36
+ extend T::Sig
37
+
38
+ sig { returns(GetoptsErrorMode) }
39
+ attr_reader :error_mode
40
+
41
+ sig { params(text: String).void }
42
+ def initialize(text)
43
+ @error_mode = GetoptsErrorMode.new(text.start_with?(':') ? :silent : :normal)
44
+ @letters = text.delete_prefix(':')
45
+ end
46
+
47
+ sig { params(option: String).returns(T::Boolean) }
48
+ def valid?(option)
49
+ !!(option != ':' && index(option))
50
+ end
51
+
52
+ sig { params(option: String).returns(T::Boolean) }
53
+ def requires_argument?(option)
54
+ position = index(option)
55
+ !!(position && @letters[position + 1] == ':')
56
+ end
57
+
58
+ private
59
+
60
+ sig { params(option: String).returns(T.nilable(Integer)) }
61
+ def index(option)
62
+ @letters.index(option)
63
+ end
64
+ end
65
+
66
+ # Pure parser for one POSIX getopts step. It walks the argv words and the
67
+ # hidden cluster cursor against the Optstring's semantics; the builtin
68
+ # applies the returned variable updates.
69
+ class GetoptsParser
70
+ extend T::Sig
71
+
72
+ # Sentinel optarg: leave OPTARG untouched (a real optarg is always a
73
+ # String, so a Symbol can never collide with one).
74
+ KEEP = :keep
75
+ # The variable updates and status produced by one getopts step.
76
+ Result = Data.define(:name, :optarg, :optind, :message, :exitstatus)
77
+ # The option character plus the argv word it came from.
78
+ Option = Data.define(:argument, :name)
79
+
80
+ sig { params(state: GetoptsState, optstring: String, arguments: T::Array[String], optind: Integer).void }
81
+ def initialize(state:, optstring:, arguments:, optind:)
82
+ @state = state
83
+ @optstring = Optstring.new(optstring)
84
+ @error_mode = @optstring.error_mode
85
+ @arguments = arguments
86
+ @state.prepare(arguments, optind)
87
+ end
88
+
89
+ sig { returns(Result) }
90
+ def call
91
+ arg = @state.current(@arguments)
92
+ return finish if !arg || arg == '-' || !arg.start_with?('-')
93
+ return finish_double_dash if arg == '--'
94
+
95
+ dispatch(option(arg))
96
+ end
97
+
98
+ private
99
+
100
+ sig { params(option: Option).returns(Result) }
101
+ def dispatch(option)
102
+ name = option.name
103
+ return invalid(option) unless @optstring.valid?(name)
104
+ return argument_value(option) if @optstring.requires_argument?(name)
105
+
106
+ found(name, '')
107
+ end
108
+
109
+ sig { params(name: String, value: String).returns(Result) }
110
+ def found(name, value)
111
+ consume_found(value)
112
+ result(name, value, nil, 0)
113
+ end
114
+
115
+ sig { params(option: Option).returns(Result) }
116
+ def invalid(option)
117
+ name = option.name
118
+ @state.consume_option(option.argument)
119
+ error_result('?', name, "Illegal option -#{name}")
120
+ end
121
+
122
+ sig { params(option: Option).returns(Result) }
123
+ def missing(option)
124
+ consume_missing(option.argument)
125
+ error_result(@error_mode.missing_name, option.name, "No arg for -#{option.name} option")
126
+ end
127
+
128
+ sig { returns(Result) }
129
+ def finish
130
+ @state.finish
131
+ result('?', KEEP, nil, 1)
132
+ end
133
+
134
+ sig { returns(Result) }
135
+ def finish_double_dash
136
+ @state.finish_double_dash
137
+ result('?', KEEP, nil, 1)
138
+ end
139
+
140
+ sig { params(value: String).void }
141
+ def consume_found(value)
142
+ current = T.must(@state.current(@arguments))
143
+ return @state.consume_option(current) unless @optstring.requires_argument?(@state.option(current))
144
+
145
+ attached_value?(current, value) ? @state.consume_attached_argument : @state.consume_detached_argument
146
+ end
147
+
148
+ sig { params(_argument: String).void }
149
+ def consume_missing(_argument)
150
+ @state.consume_attached_argument
151
+ end
152
+
153
+ sig { params(option: Option).returns(Result) }
154
+ def argument_value(option)
155
+ return found(option.name, attached_argument(option.argument)) if attached?(option.argument)
156
+
157
+ next_argument ? found(option.name, T.must(next_argument)) : missing(option)
158
+ end
159
+
160
+ sig { params(name: String, silent_optarg: String, normal_message: String).returns(Result) }
161
+ def error_result(name, silent_optarg, normal_message)
162
+ result(name, @error_mode.optarg(silent_optarg), @error_mode.message(normal_message), 0)
163
+ end
164
+
165
+ sig { params(name: String, optarg: T.untyped, message: T.nilable(String), exitstatus: Integer).returns(Result) }
166
+ def result(name, optarg, message, exitstatus)
167
+ Result.new(name: name, optarg: optarg, optind: @state.optind, message: message, exitstatus: exitstatus)
168
+ end
169
+
170
+ sig { params(argument: String).returns(Option) }
171
+ def option(argument)
172
+ Option.new(argument: argument, name: @state.option(argument))
173
+ end
174
+
175
+ sig { params(argument: String).returns(String) }
176
+ def attached_argument(argument)
177
+ argument[(@state.cursor + 1)..].to_s
178
+ end
179
+
180
+ sig { params(argument: String, value: String).returns(T::Boolean) }
181
+ def attached_value?(argument, value)
182
+ !value.empty? && attached?(argument)
183
+ end
184
+
185
+ sig { params(argument: String).returns(T::Boolean) }
186
+ def attached?(argument)
187
+ @state.cursor < argument.length - 1
188
+ end
189
+
190
+ sig { returns(T.nilable(String)) }
191
+ def next_argument
192
+ @arguments[@state.current_index + 1]
193
+ end
194
+ end
195
+ end
@@ -0,0 +1,104 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Rush
5
+ # Cursor state for POSIX getopts. OPTIND is public and 1-based, but while
6
+ # parsing an option cluster such as -ab dash already publishes OPTIND=2 after
7
+ # returning -a; the private cursor still points at b in argv[0]. This object
8
+ # keeps that hidden intra-argument cursor and resets it when OPTIND or the
9
+ # argument vector changes.
10
+ class GetoptsState
11
+ extend T::Sig
12
+
13
+ sig { returns(Integer) }
14
+ attr_reader :current_index, :cursor, :optind
15
+
16
+ sig { void }
17
+ def initialize
18
+ @signature = nil
19
+ @current_index = 0
20
+ @cursor = 1
21
+ @optind = 1
22
+ end
23
+
24
+ sig { params(arguments: T::Array[String], optind: Integer).void }
25
+ def prepare(arguments, optind)
26
+ signature = arguments.join("\0")
27
+ reset(signature, optind) if signature != @signature || optind != @optind
28
+ end
29
+
30
+ sig { params(arguments: T::Array[String]).returns(T.nilable(String)) }
31
+ def current(arguments)
32
+ arguments[@current_index]
33
+ end
34
+
35
+ sig { params(argument: String).returns(String) }
36
+ def option(argument)
37
+ T.must(argument[@cursor])
38
+ end
39
+
40
+ sig { params(argument: String).void }
41
+ def consume_option(argument)
42
+ cluster_remains?(argument) ? consume_cluster : consume_final_option
43
+ end
44
+
45
+ sig { params(argument: String).returns(T::Boolean) }
46
+ def cluster_remains?(argument)
47
+ @cursor < argument.length - 1
48
+ end
49
+
50
+ sig { void }
51
+ def consume_cluster
52
+ @cursor += 1
53
+ @optind = @current_index + 2
54
+ end
55
+
56
+ sig { void }
57
+ def consume_final_option
58
+ @current_index += 1
59
+ @cursor = 1
60
+ publish
61
+ end
62
+
63
+ sig { void }
64
+ def consume_attached_argument
65
+ @current_index += 1
66
+ @cursor = 1
67
+ publish
68
+ end
69
+
70
+ sig { void }
71
+ def consume_detached_argument
72
+ @current_index += 2
73
+ @cursor = 1
74
+ publish
75
+ end
76
+
77
+ sig { void }
78
+ def finish
79
+ @cursor = 1
80
+ publish
81
+ end
82
+
83
+ sig { void }
84
+ def finish_double_dash
85
+ @current_index += 1
86
+ finish
87
+ end
88
+
89
+ private
90
+
91
+ sig { params(signature: String, optind: Integer).void }
92
+ def reset(signature, optind)
93
+ @signature = signature
94
+ @current_index = [optind - 1, 0].max
95
+ @cursor = 1
96
+ publish
97
+ end
98
+
99
+ sig { void }
100
+ def publish
101
+ @optind = @current_index + 1
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,57 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Rush
5
+ # A here-document redirect target. The lexer creates it when it sees `<<word`
6
+ # (recording the delimiter, whether it was quoted, and the `<<-` tab strip),
7
+ # then fills #body — an AST::Word — once it collects the body lines at the next
8
+ # newline. It quacks like a Word (responds to #segments) so the expansion
9
+ # pipeline expands the body as a single field at execution time.
10
+ class HereDoc
11
+ extend T::Sig
12
+
13
+ # Immutable metadata captured from the delimiter token.
14
+ Definition = Data.define(:delimiter, :quoted, :strip, :source_line)
15
+
16
+ sig { returns(AST::Word) }
17
+ attr_reader :body
18
+
19
+ sig { params(delimiter: String, quoted: T::Boolean, strip: T::Boolean, source_line: Integer).void }
20
+ def initialize(delimiter:, quoted:, strip:, source_line: 1)
21
+ @definition = Definition.new(delimiter, quoted, strip, source_line)
22
+ @body = AST::Word.new([], source_line: source_line)
23
+ end
24
+
25
+ sig { returns(String) }
26
+ def delimiter
27
+ @definition.delimiter
28
+ end
29
+
30
+ sig { returns(T::Boolean) }
31
+ def quoted
32
+ @definition.quoted
33
+ end
34
+
35
+ sig { returns(T::Boolean) }
36
+ def strip
37
+ @definition.strip
38
+ end
39
+
40
+ sig { returns(Integer) }
41
+ def source_line
42
+ @definition.source_line
43
+ end
44
+
45
+ # The lexer fills the body — an AST::Word — once it has gathered the body
46
+ # lines at the next newline; it is the only writer, so #body stays read-only.
47
+ sig { params(body: AST::Word).void }
48
+ def fill(body)
49
+ @body = body
50
+ end
51
+
52
+ sig { returns(T::Array[AST::WordSegment[T.untyped]]) }
53
+ def segments
54
+ @body.segments
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,26 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Rush
5
+ # The interactive shell's default signal dispositions (POSIX: "ignored in a
6
+ # manner that allows trap to work"; dash is the oracle): SIGINT interrupts
7
+ # the current line — its handler raises Interrupted, which the session turns
8
+ # into $?=130 and a fresh prompt — while SIGQUIT and SIGTERM are swallowed by
9
+ # no-op handlers. All three are handler blocks rather than SIG_IGN, so
10
+ # children spawned via exec revert to the OS defaults. `trap` overrides
11
+ # them; `trap -` and subshells restore through TrapRunner's base table.
12
+ class InteractiveSignals
13
+ extend T::Sig
14
+
15
+ # Non-lambda procs: Signal.trap calls handlers with the signal number, and
16
+ # a strict lambda would die on the extra argument.
17
+ sig { params(executor: Executor).void }
18
+ def self.install(executor)
19
+ executor.trap_runner.install_base(
20
+ 'INT' => proc { raise Interrupted, 'interrupted' },
21
+ 'QUIT' => proc {},
22
+ 'TERM' => proc {}
23
+ )
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,190 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Rush
5
+ # Consumes the leading -/+ option clusters of an `sh` argv — the set-builtin
6
+ # letters plus -c, -i, -s and `-o name`, ending at `--` or the first operand —
7
+ # leaving the operands in place. Exposes the explicit option settings and
8
+ # whether -c was given.
9
+ class InvocationFlags
10
+ extend T::Sig
11
+
12
+ FLAGS = Options::LETTERS.merge('i' => :interactive, 's' => :stdin, 'l' => :login).freeze
13
+
14
+ sig { returns(T::Hash[Symbol, T::Boolean]) }
15
+ attr_reader :settings
16
+
17
+ sig { returns(T::Boolean) }
18
+ attr_reader :command
19
+
20
+ sig { params(argv: T::Array[String]).void }
21
+ def initialize(argv)
22
+ @argv = argv
23
+ @settings = T.let(
24
+ {}, #: Hash[Symbol, bool]
25
+ T::Hash[Symbol, T::Boolean]
26
+ )
27
+ @command = T.let(false, T::Boolean)
28
+ consume_clusters
29
+ # `--` or the obsolescent lone `-` ends the options and is consumed
30
+ # (POSIX sh synopsis; dash-verified: `sh -` reads stdin, `sh - file`
31
+ # runs the file, while a `-` after the -c string stays $0).
32
+ @argv.shift if ['--', '-'].include?(@argv.first)
33
+ end
34
+
35
+ private
36
+
37
+ sig { void }
38
+ def consume_clusters
39
+ while (cluster = OptionCluster.parse(@argv.first))
40
+ @argv.shift
41
+ apply_cluster(cluster)
42
+ end
43
+ end
44
+
45
+ sig { params(cluster: OptionCluster).void }
46
+ def apply_cluster(cluster)
47
+ cluster.each_letter { |letter, sign| apply_letter(letter, sign) }
48
+ end
49
+
50
+ sig { params(letter: String, sign: String).void }
51
+ def apply_letter(letter, sign)
52
+ case letter
53
+ when 'c' then enable_command(sign)
54
+ when 'o' then apply_long(sign, @argv.shift)
55
+ else apply_flag(letter, sign)
56
+ end
57
+ end
58
+
59
+ sig { params(sign: String).void }
60
+ def enable_command(sign)
61
+ raise InvocationError, "Illegal option #{sign}c" unless sign == '-'
62
+
63
+ @command = true
64
+ end
65
+
66
+ sig { params(sign: String, option_name: T.nilable(String)).void }
67
+ def apply_long(sign, option_name)
68
+ raise InvocationError, "#{sign}o requires an argument" unless option_name
69
+
70
+ option = Options::LONG.fetch(option_name) do
71
+ raise InvocationError, "Illegal option #{sign}o #{option_name}"
72
+ end
73
+ @settings[option] = sign == '-'
74
+ end
75
+
76
+ sig { params(letter: String, sign: String).void }
77
+ def apply_flag(letter, sign)
78
+ option = FLAGS.fetch(letter) { raise InvocationError, "Illegal option #{sign}#{letter}" }
79
+ @settings[option] = sign == '-'
80
+ end
81
+ end
82
+
83
+ # Parsed `sh` invocation: option flags (via InvocationFlags), then operands.
84
+ # Picks the program input — -c command string, script file, or stdin — and
85
+ # decides POSIX interactivity (-i, or no operands, no -c, and stdin and
86
+ # stderr both attached to a terminal), building the session it all implies.
87
+ class Invocation
88
+ extend T::Sig
89
+
90
+ sig { params(argv: T::Array[String], system: SystemCalls).void }
91
+ def initialize(argv, system)
92
+ @system = system
93
+ rest = argv.dup
94
+ @flags = T.let(InvocationFlags.new(rest), InvocationFlags)
95
+ @operands = T.let(rest, T::Array[String])
96
+ @input = T.let(build_input, T.any(CommandInput, ScriptInput, StdinInput))
97
+ end
98
+
99
+ # The session this invocation implies, wired to its shell state and
100
+ # startup files (login profiles, interactive ENV).
101
+ sig { returns(ProgramSession) }
102
+ def session
103
+ state = shell_state
104
+ return Repl.new(@system, state: state, startup: startup) if repl?
105
+
106
+ Source.new(self, @system, state: state, startup: startup)
107
+ end
108
+
109
+ sig { returns(T::Boolean) }
110
+ def repl?
111
+ interactive? && stdin_source?
112
+ end
113
+
114
+ sig { returns(T::Boolean) }
115
+ def interactive?
116
+ @flags.settings.fetch(:interactive) { auto_interactive? }
117
+ end
118
+
119
+ # A login shell: -l, or invoked with a leading '-' in the program name
120
+ # (how login(1) execs a shell). dash accepts -l the same way; POSIX leaves
121
+ # login files unspecified, so the oracle decides.
122
+ sig { returns(T::Boolean) }
123
+ def login?
124
+ @flags.settings.fetch(:login) { @system.program_name.start_with?('-') }
125
+ end
126
+
127
+ sig { returns(String) }
128
+ def name
129
+ @input.name
130
+ end
131
+
132
+ sig { returns(T::Array[String]) }
133
+ def positionals
134
+ @input.positionals
135
+ end
136
+
137
+ # The option flags the session starts with: the explicit -/+ settings plus
138
+ # the ones POSIX derives implicitly (s for a stdin source, i when
139
+ # interactive, and monitor defaulting to interactivity — dash's unset
140
+ # mflag resolves to iflag, probed: `dash -i` on a tty reports smi), so $-
141
+ # reports them like dash does.
142
+ sig { returns(T::Hash[Symbol, T::Boolean]) }
143
+ def shell_flags
144
+ { monitor: interactive? }.merge(@flags.settings).merge(stdin: stdin_source?, interactive: interactive?)
145
+ end
146
+
147
+ # The program text (read lazily: a REPL session never gets here).
148
+ sig { returns(String) }
149
+ def source
150
+ @input.source
151
+ end
152
+
153
+ private
154
+
155
+ sig { returns(T.any(CommandInput, ScriptInput, StdinInput)) }
156
+ def build_input
157
+ return CommandInput.new(@operands) if @flags.command
158
+ return StdinInput.new(@operands, @system) if stdin_source?
159
+
160
+ ScriptInput.new(@operands, @system)
161
+ end
162
+
163
+ sig { returns(ShellState) }
164
+ def shell_state
165
+ state = ShellState.new(name: name, positional: positionals, pids: ShellProcessIds.for(@system))
166
+ shell_flags.each { |option, enabled| state.set_option(option, enabled) }
167
+ state
168
+ end
169
+
170
+ sig { returns(Startup) }
171
+ def startup
172
+ Startup.new(login: login?, interactive: interactive?)
173
+ end
174
+
175
+ # POSIX sh: with no -i, interactive only when reading stdin implicitly on a
176
+ # terminal — no operands, and stdin and stderr both ttys. (-c with no
177
+ # command string never gets to run: #source rejects it first.)
178
+ sig { returns(T::Boolean) }
179
+ def auto_interactive?
180
+ @operands.empty? && @system.tty? && @system.stderr_tty?
181
+ end
182
+
183
+ sig { returns(T::Boolean) }
184
+ def stdin_source?
185
+ return false if @flags.command
186
+
187
+ @flags.settings.fetch(:stdin) { @operands.empty? }
188
+ end
189
+ end
190
+ end