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,74 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Rush
5
+ module Builtins
6
+ # Pure evaluator for `test`/`[` expressions. POSIX specifies the result
7
+ # by argument count (XCU test), so #evaluate applies that table in dash's
8
+ # order: at three arguments a binary primary outranks any other reading;
9
+ # a leading `!` at three or four arguments negates the rest (re-checked,
10
+ # so `! a = b` reads as the negated compare); a full `( ... )` wrapper at
11
+ # three or four arguments drops to its contents. Everything else — zero
12
+ # to two arguments and the shapes POSIX leaves unspecified — is handed to
13
+ # TestGrammar, the transcription of dash's recursive descent. A malformed
14
+ # expression raises TestError, which the builtin maps to exit 2.
15
+ #
16
+ # One deliberate divergence, on POSIX's side: dash zeroes (rather than
17
+ # toggles) its negation parity when it peels `!` twice, so `[ ! ! ! x ]`
18
+ # is true and `[ ! ! -n x ]` false there; POSIX defines the four-argument
19
+ # `!` form as the negated three-argument test, so rush negates honestly.
20
+ # Pinned in the unit specs.
21
+ class TestExpr
22
+ def initialize(args, context)
23
+ @args = args
24
+ @context = context
25
+ end
26
+
27
+ def true?
28
+ evaluate(@args)
29
+ end
30
+
31
+ private
32
+
33
+ # A binary primary at exactly three arguments outranks every other
34
+ # reading; only then do the `!` and `( ... )` rows apply.
35
+ def evaluate(args)
36
+ return binary(args) if shortcut?(args)
37
+
38
+ peel(args)
39
+ end
40
+
41
+ def shortcut?(args)
42
+ args.size == 3 && TestOperators.binary?(args[1])
43
+ end
44
+
45
+ def peel(args)
46
+ return !evaluate(args.drop(1)) if negation?(args)
47
+ return grammar(args[1...-1].to_a) if wrapped?(args)
48
+
49
+ grammar(args)
50
+ end
51
+
52
+ # `!` peels only at three or four arguments (POSIX's 3- and 4-argument
53
+ # rows); at other lengths the grammar owns `!` with tighter binding.
54
+ def negation?(args)
55
+ (3..4).cover?(args.size) && args.first == '!'
56
+ end
57
+
58
+ # A full `( ... )` wrapper likewise drops to its contents only at three
59
+ # or four arguments; longer groupings are parsed as grammar primaries.
60
+ def wrapped?(args)
61
+ (3..4).cover?(args.size) && args.first == '(' && args.last == ')'
62
+ end
63
+
64
+ def binary(args)
65
+ args => [lhs, op, rhs]
66
+ TestOperators.apply_binary(op, lhs, rhs)
67
+ end
68
+
69
+ def grammar(args)
70
+ TestGrammar.new(args, @context).truth
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,117 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Rush
5
+ module Builtins
6
+ # The historical `test`/`[` grammar, transcribed from dash's recursive
7
+ # descent (oexpr/aexpr/nexpr/primary). POSIX leaves test beyond four
8
+ # arguments unspecified and marks the -a/-o connectives obsolescent, but
9
+ # countless real scripts use them, so rush reproduces dash here:
10
+ #
11
+ # or ::= and { "-o" and } (both arms always evaluated)
12
+ # and ::= not { "-a" not }
13
+ # not ::= "!" not | primary
14
+ # primary ::= "(" or ")" | unary-op word | word binary-op word | word
15
+ #
16
+ # The cursor mirrors dash's t_wp pointer discipline: it rests on the last
17
+ # consumed word, and a word left over after the parse is the "unexpected
18
+ # operator" syntax error (TestError, exit 2). A missing operand after a
19
+ # connective parses as the missing expression, which is false, so
20
+ # `[ x -o ]` is true and `[ x -a ]` is false, exactly as in dash.
21
+ class TestGrammar
22
+ def initialize(args, context)
23
+ @args = args
24
+ @context = context
25
+ @tokens = TestTokens.new(args)
26
+ @pos = 0
27
+ end
28
+
29
+ def truth
30
+ result = or_expr(kind_at(@pos))
31
+ trailing = @args[@pos + 1]
32
+ raise TestError, "#{trailing}: unexpected operator" if @args[@pos] && trailing
33
+
34
+ result
35
+ end
36
+
37
+ private
38
+
39
+ def kind_at(index)
40
+ @tokens.kind_at(index)
41
+ end
42
+
43
+ def or_expr(kind)
44
+ result = and_expr(kind)
45
+ result = and_expr(kind_at(@pos += 2)) || result while connective == :bor
46
+ result
47
+ end
48
+
49
+ def and_expr(kind)
50
+ result = not_expr(kind)
51
+ result = not_expr(kind_at(@pos += 2)) && result while connective == :band
52
+ result
53
+ end
54
+
55
+ # The kind of the word after the cursor, or :eoi once the current word
56
+ # is exhausted — dash's `if (!*t_wp) break; t_lex(t_wp + 1)`.
57
+ def connective
58
+ @args.fetch(@pos) { return :eoi }
59
+ kind_at(@pos + 1)
60
+ end
61
+
62
+ def not_expr(kind)
63
+ return primary(kind) unless kind == :bunop
64
+
65
+ negand = kind_at(@pos + 1)
66
+ @pos += 1 unless negand == :eoi
67
+ !not_expr(negand)
68
+ end
69
+
70
+ # The missing expression is false, `(` opens a grouping, a unary
71
+ # primary applies, and any other kind reads as a word primary.
72
+ def primary(kind)
73
+ case kind
74
+ in :eoi then false
75
+ in :lparen then group
76
+ else kind == :unop ? unary : word
77
+ end
78
+ end
79
+
80
+ def group
81
+ inner = kind_at(@pos += 1)
82
+ return false if inner == :rparen
83
+
84
+ result = or_expr(inner)
85
+ raise TestError, 'closing paren expected' unless kind_at(@pos += 1) == :rparen
86
+
87
+ result
88
+ end
89
+
90
+ # The operand always exists: a unary primary with nothing after it is
91
+ # demoted to a plain operand by TestTokens, never classified :unop.
92
+ def unary
93
+ op = current
94
+ @context.unary(op, @args.fetch(@pos += 1))
95
+ end
96
+
97
+ # A word primary: a binary comparison when a binary primary follows,
98
+ # otherwise the plain non-empty test of the word itself.
99
+ def word
100
+ return binary if kind_at(@pos + 1) == :binop
101
+
102
+ !current.empty?
103
+ end
104
+
105
+ def binary
106
+ lhs = current
107
+ op = @args.fetch(@pos += 1)
108
+ rhs = @args.fetch(@pos += 1) { raise TestError, "#{op}: argument expected" }
109
+ TestOperators.apply_binary(op, lhs, rhs)
110
+ end
111
+
112
+ def current
113
+ @args.fetch(@pos)
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,94 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Rush
5
+ module Builtins
6
+ # The operator vocabulary of `test`/`[`, shared by the argument-count
7
+ # front end (TestExpr) and the recursive-descent grammar (TestGrammar).
8
+ # The tables map to lambdas, not method symbols, so each primary carries
9
+ # its operand types past both checkers (the #: types each lambda for
10
+ # Steep, the T.let the table for Sorbet). File primaries ask the
11
+ # SystemCalls port; -t parses its operand as a file-descriptor number
12
+ # first, rejecting non-numbers like dash's "Illegal number" (exit 2).
13
+ module TestOperators
14
+ UNARY = T.let({
15
+ '-n' => ->(_files, val) { !val.empty? }, #: ^(SystemCalls, String) -> bool
16
+ '-z' => ->(_files, val) { val.empty? }, #: ^(SystemCalls, String) -> bool
17
+ '-t' => ->(files, val) { files.tty_fd?(fd_number(val)) }, #: ^(SystemCalls, String) -> bool
18
+ '-e' => ->(files, val) { files.exist?(val) }, #: ^(SystemCalls, String) -> bool
19
+ '-f' => ->(files, val) { files.file?(val) }, #: ^(SystemCalls, String) -> bool
20
+ '-d' => ->(files, val) { files.directory?(val) }, #: ^(SystemCalls, String) -> bool
21
+ '-r' => ->(files, val) { files.readable?(val) }, #: ^(SystemCalls, String) -> bool
22
+ '-w' => ->(files, val) { files.writable?(val) }, #: ^(SystemCalls, String) -> bool
23
+ '-x' => ->(files, val) { files.executable?(val) }, #: ^(SystemCalls, String) -> bool
24
+ '-s' => ->(files, val) { files.file_nonempty?(val) }, #: ^(SystemCalls, String) -> bool
25
+ '-h' => ->(files, val) { files.symlink?(val) }, #: ^(SystemCalls, String) -> bool
26
+ '-L' => ->(files, val) { files.symlink?(val) }, #: ^(SystemCalls, String) -> bool
27
+ '-p' => ->(files, val) { files.pipe?(val) }, #: ^(SystemCalls, String) -> bool
28
+ '-b' => ->(files, val) { files.blockdev?(val) }, #: ^(SystemCalls, String) -> bool
29
+ '-c' => ->(files, val) { files.chardev?(val) }, #: ^(SystemCalls, String) -> bool
30
+ '-S' => ->(files, val) { files.socket?(val) }, #: ^(SystemCalls, String) -> bool
31
+ '-g' => ->(files, val) { files.setgid?(val) }, #: ^(SystemCalls, String) -> bool
32
+ '-u' => ->(files, val) { files.setuid?(val) } #: ^(SystemCalls, String) -> bool
33
+ }.freeze, T::Hash[String, T.proc.params(files: SystemCalls, val: String).returns(T::Boolean)])
34
+ STRING = T.let({
35
+ '=' => ->(lhs, rhs) { lhs == rhs }, #: ^(String, String) -> bool
36
+ '!=' => ->(lhs, rhs) { lhs != rhs } #: ^(String, String) -> bool
37
+ }.freeze, T::Hash[String, T.proc.params(lhs: String, rhs: String).returns(T::Boolean)])
38
+ INTEGER = T.let({
39
+ '-eq' => ->(lhs, rhs) { lhs == rhs }, #: ^(Integer, Integer) -> bool
40
+ '-ne' => ->(lhs, rhs) { lhs != rhs }, #: ^(Integer, Integer) -> bool
41
+ '-gt' => ->(lhs, rhs) { lhs > rhs }, #: ^(Integer, Integer) -> bool
42
+ '-ge' => ->(lhs, rhs) { lhs >= rhs }, #: ^(Integer, Integer) -> bool
43
+ '-lt' => ->(lhs, rhs) { lhs < rhs }, #: ^(Integer, Integer) -> bool
44
+ '-le' => ->(lhs, rhs) { lhs <= rhs } #: ^(Integer, Integer) -> bool
45
+ }.freeze, T::Hash[String, T.proc.params(lhs: Integer, rhs: Integer).returns(T::Boolean)])
46
+
47
+ # A string that may name an integer for the numeric primaries: #value is its
48
+ # integer when it is a valid (optionally signed, blank-padded) decimal, else
49
+ # nil. Underscores and 0x are rejected, matching dash's strtol-strictness.
50
+ class MaybeInteger
51
+ PATTERN = /\A\s*[+-]?\d+\s*\z/
52
+
53
+ def initialize(text)
54
+ @text = text
55
+ end
56
+
57
+ def valid?
58
+ @text.match?(PATTERN)
59
+ end
60
+
61
+ def value
62
+ Integer(@text, 10) if valid?
63
+ end
64
+ end
65
+
66
+ def self.unary?(word)
67
+ UNARY.key?(word)
68
+ end
69
+
70
+ def self.binary?(word)
71
+ STRING.key?(word) || INTEGER.key?(word)
72
+ end
73
+
74
+ def self.unary(op)
75
+ UNARY.fetch(op)
76
+ end
77
+
78
+ def self.apply_binary(op, lhs, rhs)
79
+ string = STRING[op]
80
+ return string.call(lhs, rhs) if string
81
+
82
+ INTEGER.fetch(op).call(to_int(lhs), to_int(rhs))
83
+ end
84
+
85
+ def self.to_int(text)
86
+ MaybeInteger.new(text).value || raise(TestError, "#{text}: integer expected")
87
+ end
88
+
89
+ def self.fd_number(text)
90
+ MaybeInteger.new(text).value || raise(TestError, "Illegal number: #{text}")
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,69 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Rush
5
+ module Builtins
6
+ # Positional word classification for `test`/`[`, mirroring dash's t_lex:
7
+ # a word is an operator only in operator position. A unary primary is
8
+ # demoted to a plain operand when it is the last word (`[ -n ]` is the
9
+ # non-empty test of "-n") or when a binary primary follows with more
10
+ # beyond (`[ -t = -t ]` compares strings); a `(` closing the argument
11
+ # list is likewise a word. Everything else keeps its table kind: unary
12
+ # and binary primaries, `!`, the XSI-obsolescent -a/-o connectives, and
13
+ # the parentheses.
14
+ class TestTokens
15
+ extend T::Sig
16
+
17
+ KINDS = T.let({ '!' => :bunop, '-a' => :band, '-o' => :bor,
18
+ '(' => :lparen, ')' => :rparen }.freeze, T::Hash[String, Symbol])
19
+
20
+ # The t_lex demotion rules, keyed by table kind (a registry, so the
21
+ # classifier itself never branches on the kind it is computing).
22
+ DEMOTIONS = T.let({
23
+ unop: ->(args, index) { operand_position?(args, index) }, #: ^(Array[String], Integer) -> bool
24
+ lparen: ->(args, index) { args[index + 1].nil? } #: ^(Array[String], Integer) -> bool
25
+ }.freeze, T::Hash[Symbol, T.proc.params(args: T::Array[String], index: Integer).returns(T::Boolean)])
26
+
27
+ NEVER = T.let(->(_args, _index) { false },
28
+ T.proc.params(args: T::Array[String], index: Integer).returns(T::Boolean))
29
+
30
+ sig { params(args: T::Array[String]).void }
31
+ def initialize(args)
32
+ @args = args
33
+ end
34
+
35
+ sig { params(index: Integer).returns(Symbol) }
36
+ def kind_at(index)
37
+ positional(@args.fetch(index) { return :eoi }, index)
38
+ end
39
+
40
+ # dash's isoperand: a unary primary reads as an operand when nothing
41
+ # follows, or when the next word is a binary primary and more follows.
42
+ sig { params(args: T::Array[String], index: Integer).returns(T::Boolean) }
43
+ def self.operand_position?(args, index)
44
+ following = args.fetch(index + 1) { return true }
45
+ args.fetch(index + 2) { return false }
46
+ TestOperators.binary?(following)
47
+ end
48
+ private_class_method :operand_position?
49
+
50
+ private
51
+
52
+ sig { params(word: String, index: Integer).returns(Symbol) }
53
+ def positional(word, index)
54
+ kind = KINDS[word] || primary_kind(word)
55
+ return :operand if DEMOTIONS.fetch(kind, NEVER).call(@args, index)
56
+
57
+ kind
58
+ end
59
+
60
+ sig { params(word: String).returns(Symbol) }
61
+ def primary_kind(word)
62
+ return :unop if TestOperators.unary?(word)
63
+ return :binop if TestOperators.binary?(word)
64
+
65
+ :operand
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,35 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Rush
5
+ module Builtins
6
+ # `times` — write the accumulated CPU times: line 1 the shell's own user and
7
+ # system time, line 2 the user and system time of its children, each formatted
8
+ # `<min>m<sec>s <min>m<sec>s` with six-decimal seconds (POSIX, matching dash).
9
+ # The values are non-deterministic, so behaviour is verified by format.
10
+ class Times < Base
11
+ extend T::Sig
12
+
13
+ sig { returns(Status) }
14
+ def call
15
+ tms = executor.system.times
16
+ write_times(tms)
17
+ success
18
+ end
19
+
20
+ private
21
+
22
+ sig { params(tms: Process::Tms).void }
23
+ def write_times(tms)
24
+ stdout.puts("#{clock(tms.utime)} #{clock(tms.stime)}")
25
+ stdout.puts("#{clock(tms.cutime)} #{clock(tms.cstime)}")
26
+ end
27
+
28
+ sig { params(seconds: Float).returns(String) }
29
+ def clock(seconds)
30
+ minutes, secs = seconds.divmod(60)
31
+ format('%<min>dm%<sec>fs', min: minutes, sec: secs)
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,85 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Rush
5
+ module Builtins
6
+ # `trap [action] signal...` sets, ignores ("" action) or resets ("-" action)
7
+ # the handler for each signal; EXIT (or 0) runs when the shell exits. With no
8
+ # operands it lists the active traps, one `trap -- 'action' NAME` line each in
9
+ # signal-number order. The action word is consumed only when a signal follows
10
+ # it, so `trap INT` resets INT (matching dash). A spec that names no signal is
11
+ # reported as "trap: SPEC: bad trap" and stops processing with status 1.
12
+ class Trap < Base
13
+ extend T::Sig
14
+
15
+ sig { returns(Status) }
16
+ def call
17
+ return list if operands.empty?
18
+
19
+ action, signals = split
20
+ apply(action, signals)
21
+ end
22
+
23
+ private
24
+
25
+ # tuple, not Array: split feeds `apply(*split)`, a fixed-arity call.
26
+ sig { returns([T.nilable(String), T::Array[String]]) }
27
+ def split
28
+ return [nil, operands] if operands.size < 2
29
+
30
+ [operands.first, operands.drop(1)]
31
+ end
32
+
33
+ # Apply left to right, stopping at (but keeping the work before) the first
34
+ # spec that names no signal — dash's behaviour for `trap x INT BADD TERM`.
35
+ sig { params(action: T.nilable(String), signals: T::Array[String]).returns(Status) }
36
+ def apply(action, signals)
37
+ bad = signals.find { |spec| !place(action, spec) }
38
+ bad ? bad_trap(bad) : success
39
+ end
40
+
41
+ sig { params(action: T.nilable(String), spec: String).returns(T.nilable(String)) }
42
+ def place(action, spec)
43
+ name = Signals.decode(spec)
44
+ change(name, action) if name
45
+ name
46
+ end
47
+
48
+ sig { params(name: String, action: T.nilable(String)).void }
49
+ def change(name, action)
50
+ if action && action != '-'
51
+ executor.trap_runner.set(name, action)
52
+ else
53
+ executor.trap_runner.reset(name)
54
+ end
55
+ end
56
+
57
+ sig { params(spec: String).returns(Status) }
58
+ def bad_trap(spec)
59
+ stderr.puts("trap: #{spec}: bad trap")
60
+ failure(1)
61
+ end
62
+
63
+ sig { returns(Status) }
64
+ def list
65
+ traps.listing.each { |name, action| stdout.puts(line(name, action)) }
66
+ success
67
+ end
68
+
69
+ sig { params(name: String, action: String).returns(String) }
70
+ def line(name, action)
71
+ "trap -- #{quote(action)} #{name}"
72
+ end
73
+
74
+ sig { params(action: String).returns(String) }
75
+ def quote(action)
76
+ "'#{action.gsub("'", %q('"'"'))}'"
77
+ end
78
+
79
+ sig { returns(TrapTable) }
80
+ def traps
81
+ executor.state.traps
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,16 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Rush
5
+ module Builtins
6
+ # `true` — does nothing, succeeds (status 0).
7
+ class True < Base
8
+ extend T::Sig
9
+
10
+ sig { returns(Status) }
11
+ def call
12
+ success
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,28 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Rush
5
+ module Builtins
6
+ # `type name ...` — report how each name would be used: a shell keyword,
7
+ # function, special or regular builtin, or the executable found in PATH.
8
+ # Exit status 127 if any name is unknown (the message still goes to stdout).
9
+ class Type < Base
10
+ extend T::Sig
11
+
12
+ sig { returns(Status) }
13
+ def call
14
+ unknown = operands.reject { |name| report(name) }
15
+ unknown.empty? ? success : failure(127)
16
+ end
17
+
18
+ private
19
+
20
+ sig { params(name: String).returns(T.nilable(String)) }
21
+ def report(name)
22
+ line = CommandLookup.new(executor).describe(name)
23
+ stdout.puts(line || "#{name}: not found")
24
+ line
25
+ end
26
+ end
27
+ end
28
+ end