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
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: ac76c8cbacbecad88ea54c697d25de3b9b69bf3e5397e93990da08fa01151c32
4
+ data.tar.gz: 44e3a86f79287b401c83ee5f464b2a3480dc667a2bfe48577c944adb27de2d06
5
+ SHA512:
6
+ metadata.gz: c2e1bfebaabe2a1645affc7ed369d929a5015ee3e1430548bf6f38db1a098ea0cd2bf73e8ff609b761e6750b860b48800ca3f8c417b9fcac1214550aec81f2b2
7
+ data.tar.gz: d0ce2156b115d6ebe7ec118723de1406598c3c61ec49f9e804568ab4870aa32825d44aa0aa26d66a078d8cd0b2cc43449e951f391f3b72d0bbd6ccc240f04816
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Alexey Nikitin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,94 @@
1
+ # rush
2
+
3
+ [![CI](https://github.com/tank-bohr/rush/actions/workflows/ci.yml/badge.svg)](https://github.com/tank-bohr/rush/actions/workflows/ci.yml)
4
+ [![Gem](https://img.shields.io/gem/v/rush-shell)](https://rubygems.org/gems/rush-shell)
5
+
6
+ A **POSIX shell written in pure Ruby**. rush implements the POSIX.1-2017 *Shell
7
+ Command Language* (IEEE Std 1003.1, §2) to the letter, with **dash** as its
8
+ differential oracle: a large corpus of language tests verifies that rush and
9
+ `dash -c` produce the same `[stdout, exit status]` byte for byte. Where dash
10
+ itself diverges from the standard, the standard wins and the divergence is
11
+ documented.
12
+
13
+ ## Install
14
+
15
+ ```sh
16
+ gem install rush-shell
17
+ ```
18
+
19
+ Ruby >= 4.0 is required — the floor is what CI actually proves. The gem is
20
+ `rush-shell`; the executable is `rush`.
21
+
22
+ ## Use
23
+
24
+ ```sh
25
+ rush # interactive shell
26
+ rush -c 'echo hello' # run a command string
27
+ rush script.sh arg1 arg2 # run a script
28
+ echo 'ls | wc -l' | rush # read commands from stdin
29
+ ```
30
+
31
+ rush accepts the standard `sh` invocation: `-c`, `-o option`/`+o option`, the
32
+ `set`-builtin flag letters (`-e`, `-x`, `-u`, `-m`, …), `--` to end options,
33
+ and behaves as a login shell when invoked with a leading-dash `argv[0]`.
34
+ Interactive mode (auto-detected on a terminal) has Reline line editing and
35
+ full job control: process groups, terminal handover, `Ctrl-Z`, `jobs`, `fg`,
36
+ `bg`, and pre-prompt status change reports — matching dash's picture
37
+ byte-for-byte in the pty smoke tests. Persistent history and the POSIX `fc`
38
+ builtin are on the roadmap.
39
+
40
+ The full §2 language is implemented: quoting, all parameter-expansion forms,
41
+ command substitution, arithmetic expansion, field splitting, pathname
42
+ expansion (including bracket expressions and locale collation via glibc),
43
+ redirections and here-documents, pipelines and lists, compound commands,
44
+ functions, traps, aliases, `getopts`, and the special builtins.
45
+
46
+ ## Why trust a shell?
47
+
48
+ Correctness in rush is not a claim, it is machinery:
49
+
50
+ - **Differential testing** — the language corpus runs every case against a
51
+ checksum-pinned dash 0.5.13.4 and compares `[stdout, exit status]`; CI
52
+ repeats this natively and inside a Docker oracle image.
53
+ - **2831 examples, ~99.8% line / ~98.8% branch coverage** — irreducible
54
+ fork/exec paths are pinned by out-of-process differential tests instead.
55
+ - **Mutation testing** — [mutant](https://github.com/mbj/mutant) over ~38,800
56
+ mutations gates CI at a ratcheted threshold.
57
+ - **Two independent type systems** — inline Sorbet `sig {}` and RBS checked by
58
+ Steep, kept deliberately separate so each catches what the other misses.
59
+
60
+ ## Development
61
+
62
+ ```sh
63
+ mise install # Ruby from .tool-versions
64
+ bundle install
65
+ make install-hooks # once per clone: Conventional Commits hook (cog)
66
+ bundle exec rake # the full parallel quality gate
67
+ ```
68
+
69
+ `bundle exec rake` compiles the Racc parser from `grammar/shell.y`, then runs
70
+ rubocop, reek, flay/flog, Steep, Sorbet and the sharded RSpec suite
71
+ concurrently. `bin/test-in-docker` runs the same gate plus container-only
72
+ smokes against the pinned oracle image. `lib/rush/parser.rb` is generated and
73
+ committed; regenerate via `rake compile`, audit with `rake check_parser_drift`.
74
+
75
+ The architecture is a unidirectional pipeline over shared shell state:
76
+
77
+ ```
78
+ Source → Lexer → Racc Parser → AST → Expander → Executor
79
+ ```
80
+
81
+ with one feedback wire (the parser nudges lexer state for POSIX grammar rules
82
+ 1–9) and all OS access funneled through one injectable port
83
+ (`Rush::SystemCalls`). Design decisions and per-slice lessons live in
84
+ [docs/architecture/](docs/architecture) and [docs/journal.md](docs/journal.md);
85
+ agent/contributor workflow lives in [AGENTS.md](AGENTS.md). Notable research:
86
+ [Steep vs Sorbet under extreme quality pressure](docs/steep-vs-sorbet.md).
87
+
88
+ Releases are cut from the `live` branch: semantic-release computes the version
89
+ from Conventional Commits and the gem is published to RubyGems via OIDC
90
+ trusted publishing with sigstore attestations — no long-lived credentials.
91
+
92
+ ## License
93
+
94
+ [MIT](LICENSE)
data/exe/rush ADDED
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env ruby
2
+ # typed: true
3
+ # frozen_string_literal: true
4
+
5
+ require_relative '../lib/rush/runtime_type_checks'
6
+
7
+ Rush::RuntimeTypeChecks.configure
8
+ require_relative '../lib/rush'
9
+
10
+ exit Rush::CLI.run(ARGV)
data/grammar/shell.y ADDED
@@ -0,0 +1,272 @@
1
+ # rush — POSIX sh grammar (Racc), transcribed from POSIX.1-2017 §2.10.2.
2
+ #
3
+ # Phase 1: lists / and_or / pipelines (with ! negation) / simple commands with
4
+ # assignments and file redirections / multi-stage pipes, the compound commands
5
+ # (brace groups, subshells, if, while/until, for, case) and function
6
+ # definitions. Action bodies are one-liners calling Rush::ParserSupport
7
+ # factories; the grammar currently compiles with zero conflicts.
8
+
9
+ class Rush::Parser
10
+
11
+ token WORD ASSIGNMENT_WORD IO_NUMBER NEWLINE
12
+ token AND_IF OR_IF
13
+ token DGREAT LESSGREAT CLOBBER DLESS DLESSDASH GREATAND LESSAND
14
+ token If Then Else Elif Fi Lbrace Rbrace Bang
15
+ token While Until Do Done
16
+ token For In NAME
17
+ token Case Esac DSEMI
18
+
19
+ start program
20
+
21
+ rule
22
+
23
+ program
24
+ : linebreak { result = make_list([]) }
25
+ | linebreak complete_commands linebreak { result = make_list(val[1]) }
26
+ ;
27
+
28
+ complete_commands
29
+ : complete_command { result = val[0] }
30
+ | complete_commands newline_list complete_command { result = val[0] + val[2] }
31
+ ;
32
+
33
+ complete_command
34
+ : list separator_op { result = terminate_list(val[0], val[1]) }
35
+ | list { result = terminate_list(val[0], ';') }
36
+ ;
37
+
38
+ list
39
+ : list separator_op and_or { result = append_and_or(val[0], val[1], val[2]) }
40
+ | and_or { result = [pending_entry(val[0])] }
41
+ ;
42
+
43
+ and_or
44
+ : pipeline { result = val[0] }
45
+ | and_or AND_IF linebreak pipeline { result = make_and_or(val[0], :and, val[3]) }
46
+ | and_or OR_IF linebreak pipeline { result = make_and_or(val[0], :or, val[3]) }
47
+ ;
48
+
49
+ pipeline
50
+ : pipe_sequence { result = make_pipeline(val[0], false) }
51
+ | Bang pipe_sequence { result = make_pipeline(val[1], true) }
52
+ ;
53
+
54
+ pipe_sequence
55
+ : command { result = [val[0]] }
56
+ | pipe_sequence '|' linebreak command { result = val[0] << val[3] }
57
+ ;
58
+
59
+ command
60
+ : simple_command { result = val[0] }
61
+ | compound_command { result = val[0] }
62
+ | compound_command redirect_list { result = make_redirected(val[0], val[1]) }
63
+ | function_definition { result = val[0] }
64
+ ;
65
+
66
+ redirect_list
67
+ : io_redirect { result = [val[0]] }
68
+ | redirect_list io_redirect { result = val[0] << val[1] }
69
+ ;
70
+
71
+ # fname '(' ')' — cmd_name vs fname is resolved by the LALR lookahead on '('
72
+ # (no '(' follows a command name), keeping the lexer unchanged. The body is a
73
+ # compound command per POSIX Rule 9.
74
+ function_definition
75
+ : WORD '(' ')' linebreak compound_command { result = make_function(val[0], val[4]) }
76
+ ;
77
+
78
+ compound_command
79
+ : brace_group { result = val[0] }
80
+ | subshell { result = val[0] }
81
+ | if_clause { result = val[0] }
82
+ | while_clause { result = val[0] }
83
+ | until_clause { result = val[0] }
84
+ | for_clause { result = val[0] }
85
+ | case_clause { result = val[0] }
86
+ ;
87
+
88
+ brace_group
89
+ : Lbrace compound_list Rbrace { result = make_brace_group(val[1]) }
90
+ ;
91
+
92
+ subshell
93
+ : '(' compound_list ')' { result = make_subshell(val[1]) }
94
+ ;
95
+
96
+ while_clause
97
+ : While compound_list do_group { result = make_while(val[1], val[2]) }
98
+ ;
99
+
100
+ until_clause
101
+ : Until compound_list do_group { result = make_until(val[1], val[2]) }
102
+ ;
103
+
104
+ do_group
105
+ : Do compound_list Done { result = val[1] }
106
+ ;
107
+
108
+ for_clause
109
+ : For name do_group { result = make_for(val[1], nil, val[2]) }
110
+ | For name sequential_sep do_group { result = make_for(val[1], nil, val[3]) }
111
+ | For name linebreak In sequential_sep do_group { result = make_for(val[1], [], val[5]) }
112
+ | For name linebreak In wordlist sequential_sep do_group { result = make_for(val[1], val[4], val[6]) }
113
+ ;
114
+
115
+ name
116
+ : NAME { result = val[0].literal_text }
117
+ ;
118
+
119
+ wordlist
120
+ : WORD { result = [val[0]] }
121
+ | wordlist WORD { result = val[0] << val[1] }
122
+ ;
123
+
124
+ sequential_sep
125
+ : ';' linebreak
126
+ | newline_list
127
+ ;
128
+
129
+ case_clause
130
+ : Case WORD linebreak In linebreak case_list Esac { result = make_case(val[1], val[5]) }
131
+ | Case WORD linebreak In linebreak case_list_ns Esac { result = make_case(val[1], val[5]) }
132
+ | Case WORD linebreak In linebreak Esac { result = make_case(val[1], []) }
133
+ ;
134
+
135
+ case_list
136
+ : case_item { result = [val[0]] }
137
+ | case_list case_item { result = val[0] << val[1] }
138
+ ;
139
+
140
+ /* POSIX 2.10.2: the last case item may omit its DSEMI (`ns` = no separator). */
141
+ case_list_ns
142
+ : case_item_ns { result = [val[0]] }
143
+ | case_list case_item_ns { result = val[0] << val[1] }
144
+ ;
145
+
146
+ case_item
147
+ : patterns ')' compound_list DSEMI linebreak { result = make_case_item(val[0], val[2]) }
148
+ | patterns ')' linebreak DSEMI linebreak { result = make_case_item(val[0], make_list([])) }
149
+ | '(' patterns ')' compound_list DSEMI linebreak { result = make_case_item(val[1], val[3]) }
150
+ | '(' patterns ')' linebreak DSEMI linebreak { result = make_case_item(val[1], make_list([])) }
151
+ ;
152
+
153
+ case_item_ns
154
+ : patterns ')' compound_list { result = make_case_item(val[0], val[2]) }
155
+ | patterns ')' linebreak { result = make_case_item(val[0], make_list([])) }
156
+ | '(' patterns ')' compound_list { result = make_case_item(val[1], val[3]) }
157
+ | '(' patterns ')' linebreak { result = make_case_item(val[1], make_list([])) }
158
+ ;
159
+
160
+ patterns
161
+ : WORD { result = [val[0]] }
162
+ | patterns '|' WORD { result = val[0] << val[2] }
163
+ ;
164
+
165
+ if_clause
166
+ : If compound_list Then compound_list else_part Fi { result = make_if(val[1], val[3], val[4]) }
167
+ | If compound_list Then compound_list Fi { result = make_if(val[1], val[3], nil) }
168
+ ;
169
+
170
+ else_part
171
+ : Elif compound_list Then compound_list { result = make_if(val[1], val[3], nil) }
172
+ | Elif compound_list Then compound_list else_part { result = make_if(val[1], val[3], val[4]) }
173
+ | Else compound_list { result = val[1] }
174
+ ;
175
+
176
+ compound_list
177
+ : linebreak term { result = make_list(val[1]) }
178
+ | linebreak term separator { result = make_list(terminate_list(val[1], val[2])) }
179
+ ;
180
+
181
+ term
182
+ : term separator and_or { result = append_and_or(val[0], val[1], val[2]) }
183
+ | and_or { result = [pending_entry(val[0])] }
184
+ ;
185
+
186
+ separator
187
+ : separator_op linebreak { result = val[0] }
188
+ | newline_list { result = ';' }
189
+ ;
190
+
191
+ simple_command
192
+ : cmd_prefix cmd_word cmd_suffix { result = make_simple_command(val[0], val[1], val[2]) }
193
+ | cmd_prefix cmd_word { result = make_simple_command(val[0], val[1], []) }
194
+ | cmd_prefix { result = make_simple_command(val[0], nil, []) }
195
+ | cmd_name cmd_suffix { result = make_simple_command([], val[0], val[1]) }
196
+ | cmd_name { result = make_simple_command([], val[0], []) }
197
+ ;
198
+
199
+ cmd_name
200
+ : WORD { result = val[0] }
201
+ ;
202
+
203
+ cmd_word
204
+ : WORD { result = val[0] }
205
+ ;
206
+
207
+ cmd_prefix
208
+ : io_redirect { result = [val[0]] }
209
+ | cmd_prefix io_redirect { result = val[0] << val[1] }
210
+ | ASSIGNMENT_WORD { result = [val[0]] }
211
+ | cmd_prefix ASSIGNMENT_WORD { result = val[0] << val[1] }
212
+ ;
213
+
214
+ cmd_suffix
215
+ : io_redirect { result = [val[0]] }
216
+ | cmd_suffix io_redirect { result = val[0] << val[1] }
217
+ | WORD { result = [val[0]] }
218
+ | cmd_suffix WORD { result = val[0] << val[1] }
219
+ ;
220
+
221
+ io_redirect
222
+ : io_file { result = val[0] }
223
+ | IO_NUMBER io_file { result = with_io_number(val[1], val[0]) }
224
+ | io_here { result = val[0] }
225
+ | IO_NUMBER io_here { result = with_io_number(val[1], val[0]) }
226
+ ;
227
+
228
+ # The WORD after DLESS is the here-doc delimiter; the lexer has replaced it
229
+ # with the HereDoc holder whose body it fills at the following newline.
230
+ io_here
231
+ : DLESS WORD { result = make_heredoc(val[1]) }
232
+ | DLESSDASH WORD { result = make_heredoc(val[1]) }
233
+ ;
234
+
235
+ io_file
236
+ : '<' filename { result = make_redirect(:in, val[1]) }
237
+ | '>' filename { result = make_redirect(:out, val[1]) }
238
+ | DGREAT filename { result = make_redirect(:append, val[1]) }
239
+ | LESSGREAT filename { result = make_redirect(:readwrite, val[1]) }
240
+ | CLOBBER filename { result = make_redirect(:clobber, val[1]) }
241
+ | GREATAND filename { result = make_redirect(:dup_out, val[1]) }
242
+ | LESSAND filename { result = make_redirect(:dup_in, val[1]) }
243
+ ;
244
+
245
+ filename
246
+ : WORD { result = val[0] }
247
+ ;
248
+
249
+ separator_op
250
+ : '&' { result = '&' }
251
+ | ';' { result = ';' }
252
+ ;
253
+
254
+ newline_list
255
+ : NEWLINE
256
+ | newline_list NEWLINE
257
+ ;
258
+
259
+ linebreak
260
+ : newline_list
261
+ | # empty
262
+ ;
263
+
264
+ end
265
+
266
+ ---- header
267
+
268
+ require_relative 'parser_support'
269
+
270
+ ---- inner
271
+
272
+ include Rush::ParserSupport