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,39 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Rush
5
+ module Builtins
6
+ # `printf format [arg ...]` — write the arguments under the control of the
7
+ # format, which is reused until the arguments are exhausted. Writes no
8
+ # trailing newline of its own. A present non-numeric argument to a numeric
9
+ # conversion is reported and treated as 0 (exit status 1).
10
+ class Printf < Base
11
+ extend T::Sig
12
+
13
+ sig { returns(Status) }
14
+ def call
15
+ return usage if operands.empty?
16
+
17
+ text, ok = PrintfFormatter.new(operands.drop(1)).render(operands.first)
18
+ stdout.write(text)
19
+ report(ok)
20
+ end
21
+
22
+ private
23
+
24
+ sig { params(valid: T::Boolean).returns(Status) }
25
+ def report(valid)
26
+ return success if valid
27
+
28
+ stderr.puts('rush: printf: expected numeric value')
29
+ failure
30
+ end
31
+
32
+ sig { returns(Status) }
33
+ def usage
34
+ stderr.puts('rush: printf: usage: printf format [arguments]')
35
+ failure(2)
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,111 @@
1
+ # typed: false
2
+ # frozen_string_literal: true
3
+
4
+ require 'strscan'
5
+
6
+ module Rush
7
+ module Builtins
8
+ # Renders a printf template against its arguments: literal text (with
9
+ # backslash escapes), %% and the %[flags][width][.prec]conv conversions,
10
+ # cycling the template while arguments remain. Flags/width/precision defer to
11
+ # Ruby's format; numeric conversions coerce their argument (a present
12
+ # non-number is reported via the ok flag and treated as 0, a missing one as
13
+ # 0 silently). %b and octal escapes arrive in a later slice. Returns [text, ok].
14
+ class PrintfFormatter
15
+ NUMERIC = %w[d i o u x X].freeze
16
+
17
+ # Scans and walks one pass of a printf template: literal runs and resolved
18
+ # backslash escapes pass straight through, while each %conversion is handed
19
+ # back to the formatter to render against the next argument (double
20
+ # dispatch), so the scanning lives here and the formatting stays there.
21
+ class Template < StringScanner
22
+ ESCAPES = { '\\' => '\\', 'a' => "\a", 'b' => "\b", 'f' => "\f",
23
+ 'n' => "\n", 'r' => "\r", 't' => "\t", 'v' => "\v" }.freeze
24
+ ESCAPE_TABLE = EscapeTable.new(ESCAPES).freeze
25
+ SPEC = /\A([-+ #0]*\d*(?:\.\d+)?)([diouxXcs%])/
26
+
27
+ def emit(formatter)
28
+ out = +''
29
+ while (char = getch)
30
+ out << piece(formatter, char)
31
+ end
32
+ out
33
+ end
34
+
35
+ private
36
+
37
+ def piece(formatter, char)
38
+ return conversion(formatter) if char == '%'
39
+ return escape if char == '\\'
40
+
41
+ char
42
+ end
43
+
44
+ def conversion(formatter)
45
+ return '%' unless scan(SPEC)
46
+
47
+ flags, conv = captures
48
+ formatter.convert(T.must(flags), T.must(conv))
49
+ end
50
+
51
+ def escape
52
+ ESCAPE_TABLE.escape(getch)
53
+ end
54
+ end
55
+
56
+ def initialize(args)
57
+ @args = args
58
+ @cursor = 0
59
+ @ok = true
60
+ end
61
+
62
+ def render(template)
63
+ start = @cursor
64
+ text = Template.new(template).emit(self)
65
+ return [text, @ok] if @cursor == start || @cursor >= @args.size
66
+
67
+ rest, = render(template)
68
+ [text + rest, @ok]
69
+ end
70
+
71
+ # Render one %conversion (called back from Template): %% is a literal %, a
72
+ # numeric/char/string conversion consumes and formats the next argument.
73
+ def convert(flags, conv)
74
+ return '%' if conv == '%'
75
+
76
+ arg = take
77
+ return numeric(flags, conv, arg) if NUMERIC.include?(conv)
78
+ return format("%#{flags}s", first_char(arg)) if conv == 'c'
79
+
80
+ format("%#{flags}s", arg)
81
+ end
82
+
83
+ private
84
+
85
+ def numeric(flags, conv, arg)
86
+ format("%#{flags}#{conv}", to_int(arg))
87
+ end
88
+
89
+ def to_int(arg)
90
+ return 0 if arg.empty?
91
+
92
+ Integer(arg, exception: false) || invalid
93
+ end
94
+
95
+ def invalid
96
+ @ok = false
97
+ 0
98
+ end
99
+
100
+ def take
101
+ arg = @args.fetch(@cursor, '')
102
+ @cursor += 1
103
+ arg
104
+ end
105
+
106
+ def first_char(arg)
107
+ arg.each_char.first
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,17 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Rush
5
+ module Builtins
6
+ # `pwd` — print the shell's logical working directory.
7
+ class Pwd < Base
8
+ extend T::Sig
9
+
10
+ sig { returns(Status) }
11
+ def call
12
+ stdout.puts(executor.state.variables.pwd)
13
+ success
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,93 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Rush
5
+ module Builtins
6
+ # `read [-r] var ...` — read a logical line from stdin, split it on IFS and
7
+ # assign the fields to the named variables (the last variable takes the
8
+ # remainder). Repeated or clustered `-r` flags are accepted and `--` ends
9
+ # option parsing; any other option or an invalid variable name is a usage
10
+ # error. Without -r a backslash preserves the next character (shielding it
11
+ # from field splitting) and a trailing backslash continues onto the next
12
+ # physical line; with -r the line is taken verbatim. Returns non-zero when
13
+ # end of file arrives before a newline (assigning what was read).
14
+ class Read < Base
15
+ extend T::Sig
16
+
17
+ NAME = /\A[A-Za-z_]\w*\z/
18
+
19
+ sig { returns(Status) }
20
+ def call
21
+ parsed = parse_operands
22
+ return usage_error unless valid?(parsed)
23
+
24
+ raw, names = T.must(parsed)
25
+ read_and_assign(raw, names)
26
+ end
27
+
28
+ private
29
+
30
+ sig { returns(T.nilable([T::Boolean, T::Array[String]])) }
31
+ def parse_operands
32
+ args = operands.dup
33
+ mode = consume_options(args)
34
+ return if mode == :invalid
35
+
36
+ [mode == :raw, args]
37
+ end
38
+
39
+ sig { params(args: T::Array[String], mode: Symbol).returns(Symbol) }
40
+ def consume_options(args, mode: :cooked)
41
+ return mode unless option?(option = args.first)
42
+
43
+ args.shift
44
+ return mode if option == '--'
45
+ return :invalid unless option.to_s.delete_prefix('-').chars.all?('r')
46
+
47
+ consume_options(args, mode: :raw)
48
+ end
49
+
50
+ sig { params(arg: T.nilable(String)).returns(T::Boolean) }
51
+ def option?(arg)
52
+ !!arg&.start_with?('-') && arg != '-'
53
+ end
54
+
55
+ sig { params(parsed: T.nilable([T::Boolean, T::Array[String]])).returns(T::Boolean) }
56
+ def valid?(parsed)
57
+ !!parsed && parsed.last.any? && parsed.last.all? { |name| name.match?(NAME) }
58
+ end
59
+
60
+ sig { params(raw: T::Boolean, names: T::Array[String]).returns(Status) }
61
+ def read_and_assign(raw, names)
62
+ result = ReadInput.new(stdin, raw, pending: -> { executor.trap_runner.pending? }).call
63
+ return failure unless result
64
+
65
+ chars, complete = result
66
+ assign(chars, names)
67
+ complete ? success : failure
68
+ end
69
+
70
+ sig { params(chars: T::Array[Expansion::ReadChar], names: T::Array[String]).void }
71
+ def assign(chars, names)
72
+ fields = Expansion::ReadSplitter.new(ifs, names.size).split(chars)
73
+ names.each_with_index { |name, index| executor.state.variables.assign(name, fields.fetch(index)) }
74
+ end
75
+
76
+ sig { returns(T.nilable(String)) }
77
+ def ifs
78
+ executor.state.variables.get('IFS')
79
+ end
80
+
81
+ sig { returns(T.untyped) }
82
+ def stdin
83
+ io.get(0)
84
+ end
85
+
86
+ sig { returns(Status) }
87
+ def usage_error
88
+ stderr.puts('rush: read: arg count')
89
+ failure(2)
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,146 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ require 'io/wait'
5
+
6
+ module Rush
7
+ module Builtins
8
+ # Reads one logical line for the `read` builtin from the given stream.
9
+ # Without -r a backslash escapes the next character (annotating it as
10
+ # protected from field splitting) and a trailing unescaped backslash joins
11
+ # the next physical line. `call` returns the annotated characters plus
12
+ # whether the line was complete — terminated by a newline before end of
13
+ # file, dash's read exit-status rule.
14
+ class ReadInput
15
+ extend T::Sig
16
+
17
+ # Adds caught-signal polling only to real IO; StringIO-backed reads stay
18
+ # direct and deterministic.
19
+ class LineReader
20
+ extend T::Sig
21
+
22
+ POLL_INTERVAL = 0.01
23
+
24
+ sig { params(stream: T.untyped, pending: T.proc.returns(T::Boolean)).void }
25
+ def initialize(stream, pending)
26
+ @stream = stream
27
+ @pending = pending
28
+ end
29
+
30
+ sig { returns(T.nilable(String)) }
31
+ def gets
32
+ stream = @stream
33
+ return stream.gets unless stream.is_a?(IO)
34
+
35
+ wait_for_line(stream)
36
+ end
37
+
38
+ private
39
+
40
+ sig { params(stream: IO).returns(T.nilable(String)) }
41
+ def wait_for_line(stream)
42
+ line = +''
43
+ catch(:physical_line) { loop { read_step(stream, line) } }
44
+ end
45
+
46
+ sig { params(stream: IO, line: String).void }
47
+ def read_step(stream, line)
48
+ throw(:interrupted, line) if @pending.call
49
+ char = next_char(stream)
50
+ throw(:physical_line, line.empty? ? nil : line) unless char
51
+ append_char(line, char) if char.is_a?(String)
52
+ end
53
+
54
+ sig { params(line: String, char: String).void }
55
+ def append_char(line, char)
56
+ line << char
57
+ throw(:physical_line, line) if char == "\n"
58
+ end
59
+
60
+ sig { params(stream: IO).returns(T.untyped) }
61
+ def next_char(stream)
62
+ return :wait_readable unless stream.wait_readable(POLL_INTERVAL)
63
+
64
+ read_char(stream)
65
+ end
66
+
67
+ sig { params(stream: IO).returns(T.untyped) }
68
+ def read_char(stream)
69
+ stream.read_nonblock(1)
70
+ rescue IO::WaitReadable
71
+ :wait_readable
72
+ rescue EOFError
73
+ nil
74
+ end
75
+ end
76
+
77
+ # A line whose backslash-pairs all match, leaving one lone trailing
78
+ # backslash: a line-continuation request.
79
+ CONTINUED = /\A(?:[^\\]|\\.)*\\\z/m
80
+
81
+ # One output character: an optional escaping backslash and the character.
82
+ PAIR = /(\\)?(.)/m
83
+
84
+ sig { params(stdin: T.untyped, raw: T::Boolean, pending: T.proc.returns(T::Boolean)).void }
85
+ def initialize(stdin, raw, pending: -> { false })
86
+ @reader = LineReader.new(stdin, pending)
87
+ @raw = raw
88
+ @chars = T.let([], T::Array[Expansion::ReadChar])
89
+ @complete = T.let(false, T::Boolean)
90
+ end
91
+
92
+ sig { returns(T.nilable([T::Array[Expansion::ReadChar], T::Boolean])) }
93
+ def call
94
+ partial = catch(:interrupted) do
95
+ gather
96
+ return [@chars, @complete]
97
+ end
98
+ partial_result(partial)
99
+ end
100
+
101
+ private
102
+
103
+ sig { params(partial: String).returns(T.nilable([T::Array[Expansion::ReadChar], T::Boolean])) }
104
+ def partial_result(partial)
105
+ decode(partial)
106
+ [@chars, false] unless @chars.empty?
107
+ end
108
+
109
+ sig { void }
110
+ def gather
111
+ line = @reader.gets
112
+ return @complete = false unless line
113
+
114
+ @complete = line.end_with?("\n")
115
+ gather if decode(line.delete_suffix("\n"))
116
+ end
117
+
118
+ # Appends the line's annotated characters; true means a continuation is
119
+ # pending and the next physical line belongs to this logical one.
120
+ sig { params(text: String).returns(T::Boolean) }
121
+ def decode(text)
122
+ return cook(text) unless @raw
123
+
124
+ verbatim(text)
125
+ false
126
+ end
127
+
128
+ sig { params(text: String).void }
129
+ def verbatim(text)
130
+ text.each_char { |char| @chars << [char, false] }
131
+ end
132
+
133
+ # A continuation leaves a zero-width escaped joint (['', true]) where the
134
+ # lines meet: dash's region boundary, which ends a whitespace-delimiter
135
+ # run in the splitter without contributing a character.
136
+ sig { params(text: String).returns(T::Boolean) }
137
+ def cook(text)
138
+ pending = text.match?(CONTINUED)
139
+ body = pending ? T.must(text[0..-2]) : text
140
+ body.scan(PAIR) { |escape, char| @chars << [T.must(char), escape == '\\'] }
141
+ (@chars << ['', true]) if pending
142
+ pending
143
+ end
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,20 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Rush
5
+ module Builtins
6
+ # `readonly name[=value] ...` — mark each name read only, so a later
7
+ # assignment or unset aborts (ReadonlyError); see Declare for the shared
8
+ # engine. (The `-p` listing form arrives with the variable-printing slice.)
9
+ class Readonly < Declare
10
+ extend T::Sig
11
+
12
+ private
13
+
14
+ sig { params(operand: String).void }
15
+ def declare(operand)
16
+ executor.state.variables.readonly_operand(operand)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,36 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Rush
5
+ module Builtins
6
+ # O(1) name -> builtin class lookup, populated by Builtins.default_registry.
7
+ class Registry
8
+ extend T::Sig
9
+
10
+ sig { void }
11
+ def initialize
12
+ @builtins = {}
13
+ end
14
+
15
+ sig { params(name: String, klass: T.class_of(Base)).void }
16
+ def register(name, klass)
17
+ @builtins[name] = klass
18
+ end
19
+
20
+ sig { params(name: String).returns(T.class_of(Base)) }
21
+ def fetch(name)
22
+ @builtins.fetch(name)
23
+ end
24
+
25
+ sig { params(name: T.nilable(String)).returns(T.nilable(T.class_of(Base))) }
26
+ def lookup(name)
27
+ @builtins[name]
28
+ end
29
+
30
+ sig { params(name: T.nilable(String)).returns(T::Boolean) }
31
+ def key?(name)
32
+ @builtins.key?(name)
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,24 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Rush
5
+ module Builtins
6
+ # `return [n]` — return from the current function with status n (default: the
7
+ # last command's status). A non-numeric n is a special-builtin error.
8
+ class Return < Base
9
+ extend T::Sig
10
+
11
+ sig { returns(T.noreturn) }
12
+ def call
13
+ raise ReturnSignal, code
14
+ end
15
+
16
+ private
17
+
18
+ sig { returns(Integer) }
19
+ def code
20
+ operands.first ? numeric_operand(operands.fetch(0)) : executor.state.last_status.exitstatus
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,101 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Rush
5
+ module Builtins
6
+ # `set [-/+ options] [--] [arg ...]` — toggle shell options (-x/+x and the
7
+ # `-o name`/`+o name` long form) and, when operands follow (or after `--`),
8
+ # replace the positional parameters. With no operands the parameters are left
9
+ # unchanged; an unknown option is ignored. A bare trailing `-o` writes the
10
+ # current settings table, a bare `+o` the re-input form (POSIX XCU set).
11
+ # Options: -a/allexport, -C/noclobber, -e/errexit, -n/noexec, -u/nounset,
12
+ # -x/xtrace, -f/noglob, -v/verbose, pipefail.
13
+ class Set < Base
14
+ extend T::Sig
15
+
16
+ OPTIONS = Options::LETTERS
17
+ LONG = Options::LONG
18
+
19
+ sig { returns(Status) }
20
+ def call
21
+ rest = strip_options(operands)
22
+ executor.state.positional.replace(rest) if rest
23
+ success
24
+ end
25
+
26
+ private
27
+
28
+ sig { params(args: T::Array[String]).returns(T.nilable(T::Array[String])) }
29
+ def strip_options(args)
30
+ return if args.empty?
31
+
32
+ positionals(args, consume_options(args))
33
+ end
34
+
35
+ sig { params(args: T::Array[String]).returns(Integer) }
36
+ def consume_options(args)
37
+ index = 0
38
+ while (cluster = OptionCluster.parse(args[index]))
39
+ index += advance(cluster, args, index)
40
+ end
41
+ index
42
+ end
43
+
44
+ sig { params(cluster: OptionCluster, args: T::Array[String], index: Integer).returns(Integer) }
45
+ def advance(cluster, args, index)
46
+ return apply_long(cluster.sign, args[index + 1]) if cluster.letters.join == 'o'
47
+
48
+ apply(cluster)
49
+ 1
50
+ end
51
+
52
+ sig { params(args: T::Array[String], index: Integer).returns(T.nilable(T::Array[String])) }
53
+ def positionals(args, index)
54
+ return if index == args.size
55
+
56
+ args[index] == '--' ? args.drop(index + 1) : args.drop(index)
57
+ end
58
+
59
+ sig { params(cluster: OptionCluster).void }
60
+ def apply(cluster)
61
+ cluster.each_letter { |char, sign| toggle(OPTIONS.fetch(char, nil), sign) }
62
+ end
63
+
64
+ # With a name, toggle that option; bare `set -o` prints the option
65
+ # table (format follows dash), bare `set +o` the lines that re-create
66
+ # the settings when re-input (POSIX XCU set).
67
+ sig { params(sign: T.nilable(String), name: T.nilable(String)).returns(Integer) }
68
+ def apply_long(sign, name)
69
+ return toggle_long(sign, name) if name
70
+
71
+ options = executor.state.options
72
+ print_lines(sign == '-' ? ['Current option settings', *options.settings_rows] : options.reinput_lines)
73
+ end
74
+
75
+ sig { params(sign: T.nilable(String), name: String).returns(Integer) }
76
+ def toggle_long(sign, name)
77
+ toggle(LONG.fetch(name, nil), sign)
78
+ 2
79
+ end
80
+
81
+ # Prints a listing and consumes just the bare -o/+o flag itself.
82
+ sig { params(lines: T::Array[String]).returns(Integer) }
83
+ def print_lines(lines)
84
+ lines.each { |line| stdout.puts(line) }
85
+ 1
86
+ end
87
+
88
+ sig { params(option: T.nilable(Symbol), sign: T.nilable(String)).void }
89
+ def toggle(option, sign)
90
+ return unless option
91
+
92
+ enabled = sign == '-'
93
+ # Monitor carries side effects (SIGTSTP disposition, tty check) and can
94
+ # refuse, so it routes through the job-control policy, not bare state.
95
+ return executor.state.set_option(option, enabled) unless option == :monitor
96
+
97
+ enabled ? executor.job_control.enable(stderr) : executor.job_control.disable
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,35 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Rush
5
+ module Builtins
6
+ # `shift [n]` — discard the first n positional parameters (default 1) and
7
+ # renumber the rest. As a special builtin, two errors abort a non-interactive
8
+ # shell with status 2 (BuiltinError, firing the EXIT trap), matching dash: a
9
+ # non-numeric / negative operand ("Illegal number"), and asking to shift more
10
+ # than $# ("can't shift that many"). Extra operands past the first are ignored.
11
+ class Shift < Base
12
+ extend T::Sig
13
+
14
+ sig { returns(Status) }
15
+ def call
16
+ raise BuiltinError, "shift: can't shift that many" if count > state.positional.size
17
+
18
+ state.positional.shift(count)
19
+ success
20
+ end
21
+
22
+ private
23
+
24
+ sig { returns(ShellState) }
25
+ def state
26
+ executor.state
27
+ end
28
+
29
+ sig { returns(Integer) }
30
+ def count
31
+ operands.empty? ? 1 : numeric_operand(T.must(operands.first))
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,48 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Rush
5
+ module Builtins
6
+ # `test EXPR` / `[ EXPR ]` — evaluate a conditional expression, returning
7
+ # success (0) when true and failure (1) when false. A malformed expression
8
+ # is reported on stderr with exit status 2. Invoked as `[`, the expression
9
+ # must be terminated by a closing `]`.
10
+ class Test < Base
11
+ extend T::Sig
12
+
13
+ sig { returns(Status) }
14
+ def call
15
+ evaluate(bracketed? ? without_close(operands) : operands)
16
+ rescue TestError => e
17
+ report(e.message)
18
+ end
19
+
20
+ private
21
+
22
+ sig { returns(T::Boolean) }
23
+ def bracketed?
24
+ argv.first == '['
25
+ end
26
+
27
+ sig { params(ops: T::Array[String]).returns(T::Array[String]) }
28
+ def without_close(ops)
29
+ *body, close = ops
30
+ raise TestError, "missing `]'" unless close == ']'
31
+
32
+ body
33
+ end
34
+
35
+ sig { params(ops: T::Array[String]).returns(Status) }
36
+ def evaluate(ops)
37
+ context = TestContext.new(executor.system, FdOperand.new(io))
38
+ TestExpr.new(ops, context).true? ? success : failure
39
+ end
40
+
41
+ sig { params(message: String).returns(Status) }
42
+ def report(message)
43
+ stderr.puts("rush: #{argv.first}: #{message}")
44
+ failure(2)
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,24 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Rush
5
+ module Builtins
6
+ # The impure inputs to one test/[ evaluation: the filesystem port plus the
7
+ # resolver that projects shell-level fd bindings onto unary operands.
8
+ class TestContext
9
+ extend T::Sig
10
+
11
+ sig { params(files: SystemCalls, fd_operand: FdOperand).void }
12
+ def initialize(files, fd_operand)
13
+ @files = files
14
+ @fd_operand = fd_operand
15
+ end
16
+
17
+ sig { params(operator: String, operand: String).returns(T::Boolean) }
18
+ def unary(operator, operand)
19
+ resolved = @fd_operand.resolve(operator, operand)
20
+ TestOperators.unary(operator).call(@files, resolved)
21
+ end
22
+ end
23
+ end
24
+ end