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,127 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Rush
5
+ module Builtins
6
+ # `cd [dir]` — change the working directory, maintaining a *logical* PWD
7
+ # (not Dir.pwd, which resolves symlinks) plus OLDPWD. Defaults to $HOME.
8
+ class Cd < Base
9
+ extend T::Sig
10
+
11
+ # A CDPATH resolution result: the path to chdir to, and whether POSIX
12
+ # requires printing the resolved logical directory after a successful cd.
13
+ Target = Data.define(:path, :echo)
14
+
15
+ sig { returns(Status) }
16
+ def call
17
+ target = operands.first || executor.state.variables.get('HOME')
18
+ return report('HOME not set') unless target
19
+
20
+ target == '-' ? change_back : change_to(target)
21
+ end
22
+
23
+ private
24
+
25
+ sig { params(dir: String).returns(Status) }
26
+ def change_to(dir)
27
+ target = resolve_cdpath(dir)
28
+ perform_chdir(target.path)
29
+ success_after_cdpath(target)
30
+ rescue Errno::ENOENT, Errno::ENOTDIR
31
+ report("#{dir}: No such file or directory")
32
+ end
33
+
34
+ sig { returns(Status) }
35
+ def change_back
36
+ dir = oldpwd
37
+ perform_chdir(dir)
38
+ success_with_echo
39
+ rescue Errno::ENOENT, Errno::ENOTDIR
40
+ report("#{dir}: No such file or directory")
41
+ end
42
+
43
+ sig { returns(String) }
44
+ def oldpwd
45
+ executor.state.variables.get('OLDPWD') || executor.state.variables.current_pwd
46
+ end
47
+
48
+ sig { params(dir: String).returns(Target) }
49
+ def resolve_cdpath(dir)
50
+ return Target.new(path: dir, echo: false) unless search_cdpath?(dir)
51
+
52
+ cdpath_targets(dir).find { |target| executor.system.directory?(target.path) } ||
53
+ Target.new(path: dir, echo: false)
54
+ end
55
+
56
+ sig { params(dir: String).returns(T::Boolean) }
57
+ def search_cdpath?(dir)
58
+ cdpath? && relative_cdpath_operand?(dir)
59
+ end
60
+
61
+ sig { params(dir: String).returns(T::Boolean) }
62
+ def relative_cdpath_operand?(dir)
63
+ !dir.start_with?('/') && !%w[. ..].include?(dir.split('/', 2).first)
64
+ end
65
+
66
+ sig { returns(T::Boolean) }
67
+ def cdpath?
68
+ !!executor.state.variables.get('CDPATH')
69
+ end
70
+
71
+ sig { params(dir: String).returns(T::Array[Target]) }
72
+ def cdpath_targets(dir)
73
+ cdpath_entries.map { |prefix| cdpath_target(prefix, dir) }
74
+ end
75
+
76
+ sig { returns(T::Array[String]) }
77
+ def cdpath_entries
78
+ T.must(executor.state.variables.get('CDPATH')).split(':', -1)
79
+ end
80
+
81
+ sig { params(prefix: String, dir: String).returns(Target) }
82
+ def cdpath_target(prefix, dir)
83
+ Target.new(path: cdpath_path(prefix, dir), echo: cdpath_echo?(prefix))
84
+ end
85
+
86
+ sig { params(prefix: String, dir: String).returns(String) }
87
+ def cdpath_path(prefix, dir)
88
+ prefix.empty? ? dir : "#{prefix}/#{dir}"
89
+ end
90
+
91
+ sig { params(prefix: String).returns(T::Boolean) }
92
+ def cdpath_echo?(prefix)
93
+ !prefix.empty?
94
+ end
95
+
96
+ sig { params(dir: String).void }
97
+ def perform_chdir(dir)
98
+ executor.system.chdir(dir)
99
+ update_pwd(dir)
100
+ end
101
+
102
+ sig { returns(Status) }
103
+ def success_with_echo
104
+ stdout.puts(executor.state.variables.current_pwd)
105
+ success
106
+ end
107
+
108
+ sig { params(target: Target).returns(Status) }
109
+ def success_after_cdpath(target)
110
+ target.echo ? success_with_echo : success
111
+ end
112
+
113
+ sig { params(dir: String).void }
114
+ def update_pwd(dir)
115
+ variables = executor.state.variables
116
+ variables.move_to(executor.system.expand_path(dir, variables.current_pwd))
117
+ end
118
+
119
+ # A failed cd exits 2, matching the dash oracle (POSIX only asks for >0).
120
+ sig { params(message: String).returns(Status) }
121
+ def report(message)
122
+ stderr.puts("rush: cd: #{message}")
123
+ failure(2)
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,16 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Rush
5
+ module Builtins
6
+ # `:` — the null command; expands its arguments and always succeeds.
7
+ class Colon < 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,137 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Rush
5
+ module Builtins
6
+ # `command [-p] name [arg ...]` runs name as a command, skipping shell
7
+ # functions (so a function can call the builtin it shadows); -p searches
8
+ # the system default PATH (confstr _CS_PATH) for an external instead of
9
+ # $PATH, without changing the child's environment. `command -v name`
10
+ # prints how name resolves — the name itself for a keyword / function /
11
+ # builtin, the path for an external — and -V the long `type` form; both
12
+ # exit 127 for an unknown name, 0 with no operand, and -V outranks -v.
13
+ class Command < Base
14
+ extend T::Sig
15
+
16
+ sig { returns(Status) }
17
+ def call
18
+ opts = CommandOptions.new(operands)
19
+ bad = opts.illegal
20
+ bad ? illegal_option(bad) : dispatch(opts)
21
+ end
22
+
23
+ private
24
+
25
+ sig { params(opts: CommandOptions).returns(Status) }
26
+ def dispatch(opts)
27
+ return no_name(opts) unless opts.name
28
+ return verbose(opts) if opts.verbose?
29
+ return verify(opts) if opts.verify?
30
+
31
+ run(opts)
32
+ end
33
+
34
+ # dash exits 0 for a bare `command`, `command -v` and `command -V` alike
35
+ # (probed); the bare run form is the colon builtin's no-op.
36
+ sig { params(opts: CommandOptions).returns(Status) }
37
+ def no_name(opts)
38
+ return success if opts.verbose? || opts.verify?
39
+
40
+ Colon.new(executor, [], io).call
41
+ end
42
+
43
+ sig { params(opts: CommandOptions).returns(Status) }
44
+ def verify(opts)
45
+ print_terse(lookup(opts).find(opts.name))
46
+ end
47
+
48
+ sig { params(match: CommandLookup::Match).returns(Status) }
49
+ def print_terse(match)
50
+ return failure(127) unless match.known?
51
+
52
+ stdout.puts(match.terse)
53
+ success
54
+ end
55
+
56
+ sig { params(opts: CommandOptions).returns(Status) }
57
+ def verbose(opts)
58
+ print_description(T.must(opts.name), lookup(opts).find(opts.name))
59
+ end
60
+
61
+ sig { params(name: String, match: CommandLookup::Match).returns(Status) }
62
+ def print_description(name, match)
63
+ return not_described(name) unless match.known?
64
+
65
+ stdout.puts(match.describe)
66
+ success
67
+ end
68
+
69
+ sig { params(name: String).returns(Status) }
70
+ def not_described(name)
71
+ stdout.puts("#{name}: not found")
72
+ failure(127)
73
+ end
74
+
75
+ sig { params(opts: CommandOptions).returns(CommandLookup) }
76
+ def lookup(opts)
77
+ opts.default_path? ? default_lookup : CommandLookup.new(executor)
78
+ end
79
+
80
+ sig { returns(CommandLookup) }
81
+ def default_lookup
82
+ CommandLookup.new(executor, path: executor.system.default_path)
83
+ end
84
+
85
+ sig { params(opts: CommandOptions).returns(Status) }
86
+ def run(opts)
87
+ name = T.must(opts.name)
88
+ builtin = executor.builtins.lookup(name)
89
+ builtin ? run_builtin(builtin, opts) : run_external(name, opts)
90
+ end
91
+
92
+ sig { params(builtin: T.class_of(Base), opts: CommandOptions).returns(Status) }
93
+ def run_builtin(builtin, opts)
94
+ builtin.new(executor, opts.operands, io, command_environment).call
95
+ rescue Error => e
96
+ raise unless ErrorPolicy.decision(:command, e) == :demote2
97
+
98
+ demoted_error(e)
99
+ end
100
+
101
+ sig { params(error: StandardError).returns(Status) }
102
+ def demoted_error(error)
103
+ stderr.puts("rush: #{error.message}")
104
+ failure(2)
105
+ end
106
+
107
+ sig { params(name: String, opts: CommandOptions).returns(Status) }
108
+ def run_external(name, opts)
109
+ external = External.new(executor, opts.operands, io, command_environment)
110
+ return external.call unless opts.default_path?
111
+
112
+ run_on_default_path(name, external)
113
+ end
114
+
115
+ # The default-PATH resolution happens here in the shell (dash does the
116
+ # same): the resolved file is executed while argv[0] stays the name as
117
+ # typed, and $PATH is neither consulted nor mutated.
118
+ sig { params(name: String, external: External).returns(Status) }
119
+ def run_on_default_path(name, external)
120
+ path = default_lookup.executable_path(name)
121
+ path ? external.call_file(path) : not_found(name)
122
+ end
123
+
124
+ sig { params(name: String).returns(Status) }
125
+ def not_found(name)
126
+ stderr.puts("rush: #{name}: not found")
127
+ failure(127)
128
+ end
129
+
130
+ sig { params(letter: String).returns(Status) }
131
+ def illegal_option(letter)
132
+ stderr.puts("rush: command: Illegal option #{letter}")
133
+ failure(2)
134
+ end
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,103 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Rush
5
+ module Builtins
6
+ # The parsed option prefix of the `command` builtin's argv: leading
7
+ # clusters of -p / -v / -V letters (repeats accumulate; -V outranks -v,
8
+ # as in dash), an optional `--` terminator, then the operands. A letter
9
+ # outside the set is recorded as `illegal` (e.g. '-z'), which `command`
10
+ # reports with status 2; `+x` or a lone `-` is an operand, not options.
11
+ class CommandOptions
12
+ extend T::Sig
13
+
14
+ LETTERS = T.let({ 'p' => :default_path, 'v' => :verify, 'V' => :verbose }.freeze,
15
+ T::Hash[String, Symbol])
16
+
17
+ # A dash followed by letters, except the bare `--` terminator (a lone
18
+ # dash or a +cluster is an operand — dash-probed).
19
+ CLUSTER = /\A-(?!-\z)./
20
+
21
+ sig { returns(T::Array[String]) }
22
+ attr_reader :operands
23
+
24
+ sig { returns(T.nilable(String)) }
25
+ attr_reader :illegal
26
+
27
+ sig { params(args: T::Array[String]).void }
28
+ def initialize(args)
29
+ @args = args
30
+ @flags = T.let([], T::Array[Symbol])
31
+ @illegal = T.let(nil, T.nilable(String))
32
+ @operands = T.let(consume, T::Array[String])
33
+ end
34
+
35
+ sig { returns(T.nilable(String)) }
36
+ def name
37
+ operands.first
38
+ end
39
+
40
+ sig { returns(T::Boolean) }
41
+ def default_path?
42
+ @flags.include?(:default_path)
43
+ end
44
+
45
+ sig { returns(T::Boolean) }
46
+ def verify?
47
+ @flags.include?(:verify)
48
+ end
49
+
50
+ sig { returns(T::Boolean) }
51
+ def verbose?
52
+ @flags.include?(:verbose)
53
+ end
54
+
55
+ private
56
+
57
+ sig { returns(T::Array[String]) }
58
+ def consume
59
+ index = count_clusters
60
+ @args.drop(arg_at(index) == '--' ? index + 1 : index)
61
+ end
62
+
63
+ sig { returns(Integer) }
64
+ def count_clusters
65
+ index = 0
66
+ index += 1 while take(index)
67
+ index
68
+ end
69
+
70
+ sig { params(index: Integer).returns(T::Boolean) }
71
+ def take(index)
72
+ return false unless cluster?(index)
73
+
74
+ arg_at(index).chars.drop(1).all? { |letter| record(letter) }
75
+ end
76
+
77
+ sig { params(index: Integer).returns(T::Boolean) }
78
+ def cluster?(index)
79
+ arg_at(index).match?(CLUSTER)
80
+ end
81
+
82
+ sig { params(index: Integer).returns(String) }
83
+ def arg_at(index)
84
+ @args.fetch(index, '')
85
+ end
86
+
87
+ # The letter's flag (feeding take's all?-driven cluster scan), nil once
88
+ # the letter proves illegal.
89
+ sig { params(letter: String).returns(T.nilable(Symbol)) }
90
+ def record(letter)
91
+ flag = LETTERS.fetch(letter) { record_illegal(letter) }
92
+ @flags << flag if flag
93
+ flag
94
+ end
95
+
96
+ sig { params(letter: String).returns(NilClass) }
97
+ def record_illegal(letter)
98
+ @illegal = "-#{letter}"
99
+ nil
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,19 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Rush
5
+ module Builtins
6
+ # `continue [n]` — resume the n-th enclosing loop's next iteration
7
+ # (default 1); the shared POSIX 2.9.5 semantics live in LoopJump.
8
+ class Continue < LoopJump
9
+ extend T::Sig
10
+
11
+ private
12
+
13
+ sig { returns(T.class_of(LoopControl)) }
14
+ def signal
15
+ ContinueSignal
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,27 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Rush
5
+ module Builtins
6
+ # Shared engine of `export` and `readonly`: each operand is `name[=value]` —
7
+ # the value is assigned first when one is given, then the subclass's marking
8
+ # applies on the variable table. Both always report success.
9
+ class Declare < Base
10
+ extend T::Sig
11
+
12
+ sig { returns(Status) }
13
+ def call
14
+ operands.each { |operand| declare(operand) }
15
+ success
16
+ end
17
+
18
+ private
19
+
20
+ # Apply this builtin's marking to one name[=value] operand.
21
+ sig { params(_operand: String).void }
22
+ def declare(_operand)
23
+ raise NotImplementedError
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,28 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Rush
5
+ # The shell's builtin commands and the default name -> implementation table.
6
+ module Builtins
7
+ extend T::Sig
8
+
9
+ # The builtins available in Slice 1. Later slices extend this table.
10
+ DEFAULTS = {
11
+ ':' => Colon, 'true' => True, 'false' => False,
12
+ 'echo' => Echo, 'exit' => Exit, 'pwd' => Pwd, 'cd' => Cd,
13
+ 'break' => Break, 'continue' => Continue, 'return' => Return,
14
+ 'test' => Test, '[' => Test, 'set' => Set, 'shift' => Shift,
15
+ 'export' => Export, 'unset' => Unset, 'eval' => Eval, 'getopts' => Getopts, 'read' => Read,
16
+ 'printf' => Printf, '.' => Dot, 'readonly' => Readonly, 'exec' => Exec,
17
+ 'local' => Local, 'type' => Type, 'command' => Command, 'trap' => Trap,
18
+ 'kill' => Kill, 'alias' => Alias, 'unalias' => Unalias,
19
+ 'hash' => Hash, 'times' => Times, 'umask' => Umask, 'ulimit' => Ulimit, 'wait' => Wait,
20
+ 'jobs' => Jobs, 'fg' => Fg, 'bg' => Bg
21
+ }.freeze
22
+
23
+ sig { returns(Registry) }
24
+ def self.default_registry
25
+ Registry.new.tap { |registry| DEFAULTS.each { |name, klass| registry.register(name, klass) } }
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,52 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Rush
5
+ module Builtins
6
+ # `. filename` — read the file and run it in the current shell, so its
7
+ # functions and variables persist. The file is read command by command
8
+ # (SourceRunner), so an alias or function it defines shapes its own later
9
+ # lines. Redirections apply to the file's commands (executor.with_io);
10
+ # exit/break/continue propagate to the caller, but `return` is bounded to the
11
+ # dot script — it stops the file and becomes the `.` command's status (POSIX
12
+ # 2.14), unlike eval. A missing file or a syntax error is a special-builtin
13
+ # error that aborts a non-interactive shell with status 2 (BuiltinError); a
14
+ # missing operand is a plain usage error that does not abort. PATH search for
15
+ # an unqualified name arrives later.
16
+ class Dot < Base
17
+ extend T::Sig
18
+
19
+ sig { returns(Status) }
20
+ def call
21
+ return usage if operands.empty?
22
+
23
+ source(operands.first)
24
+ rescue Errno::ENOENT
25
+ raise BuiltinError, ".: #{operands.first}: No such file or directory"
26
+ end
27
+
28
+ private
29
+
30
+ sig { params(path: T.untyped).returns(Status) }
31
+ def source(path)
32
+ text = executor.system.read_file(path)
33
+ executor.with_io(io) { run_text(text) }
34
+ rescue ParseError => e
35
+ raise BuiltinError, ".: #{e}"
36
+ end
37
+
38
+ sig { params(text: String).returns(Status) }
39
+ def run_text(text)
40
+ SourceRunner.new(executor, text).run
41
+ rescue ReturnSignal => e
42
+ Status.new(e.code)
43
+ end
44
+
45
+ sig { returns(Status) }
46
+ def usage
47
+ stderr.puts('rush: .: filename argument required')
48
+ failure(2)
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,37 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Rush
5
+ module Builtins
6
+ # `echo [-n] [arg...]` — joins arguments with spaces and adds a trailing
7
+ # newline; `-n` suppresses it. XSI escape processing (\n, \t, \c, ...) is
8
+ # added in Phase 2.
9
+ class Echo < Base
10
+ extend T::Sig
11
+
12
+ sig { returns(Status) }
13
+ def call
14
+ stdout.write(render)
15
+ success
16
+ end
17
+
18
+ private
19
+
20
+ sig { returns(String) }
21
+ def render
22
+ body = printed.join(' ')
23
+ newline? ? "#{body}\n" : body
24
+ end
25
+
26
+ sig { returns(T::Boolean) }
27
+ def newline?
28
+ !operands.first.eql?('-n')
29
+ end
30
+
31
+ sig { returns(T::Array[String]) }
32
+ def printed
33
+ newline? ? operands : operands.drop(1)
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,25 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Rush
5
+ module Builtins
6
+ # `eval [arg ...]` — join the arguments with spaces and run the result in
7
+ # the current shell, returning its status. The input is read command by
8
+ # command (SourceRunner), so an `alias` or function defined by one command
9
+ # shapes how the next is parsed. A redirection on eval applies to the parsed
10
+ # commands (executor.with_io), and exit/break/continue/return all propagate
11
+ # to the enclosing context. A syntax error is a special-builtin error: it
12
+ # aborts a non-interactive shell with status 2 (BuiltinError), after the
13
+ # complete commands before it have run.
14
+ class Eval < Base
15
+ extend T::Sig
16
+
17
+ sig { returns(Status) }
18
+ def call
19
+ executor.with_io(io) { SourceRunner.new(executor, operands.join(' ')).run }
20
+ rescue ParseError => e
21
+ raise BuiltinError, "eval: #{e}"
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,54 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Rush
5
+ module Builtins
6
+ # `exec [command [arg ...]]` — with no command, make the redirections
7
+ # permanent for the rest of the shell; with a command, replace the shell
8
+ # process with it (applying the redirections first). A failed exec aborts the
9
+ # shell with 127 (not found) or 126 (not executable), matching dash.
10
+ class Exec < Base
11
+ extend T::Sig
12
+
13
+ sig { returns(Status) }
14
+ def call
15
+ operands.empty? ? redirect_shell : replace_process
16
+ end
17
+
18
+ private
19
+
20
+ sig { returns(Status) }
21
+ def redirect_shell
22
+ executor.replace_io(io)
23
+ success
24
+ end
25
+
26
+ sig { returns(T.noreturn) }
27
+ def replace_process
28
+ executor.system.exec(command_environment, operands, options)
29
+ rescue Errno::ENOENT
30
+ abort_exec('not found', 127)
31
+ rescue Errno::EACCES
32
+ abort_exec('Permission denied', 126)
33
+ end
34
+
35
+ sig { params(reason: String, code: Integer).returns(T.noreturn) }
36
+ def abort_exec(reason, code)
37
+ report_exec_error(reason)
38
+ raise ExitSignal, code
39
+ end
40
+
41
+ sig { params(reason: String).void }
42
+ def report_exec_error(reason)
43
+ stderr.puts("rush: #{operands.first}: #{reason}")
44
+ rescue Errno::EBADF
45
+ nil
46
+ end
47
+
48
+ sig { returns(T::Hash[T.any(Integer, Symbol), T.untyped]) }
49
+ def options
50
+ io.to_spawn_options.merge(close_others: true)
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,45 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Rush
5
+ module Builtins
6
+ # `exit [n]` — unwind to the top level via ExitSignal, carrying the status n
7
+ # (the executor's exiting status when no operand is given: the last command's
8
+ # status normally, or the shell's terminating status inside the EXIT trap).
9
+ # A non-numeric n is a special-builtin error. With a stopped job in the
10
+ # table the first exit is refused — "You have stopped jobs.", status 0 —
11
+ # and an immediately repeated exit goes through (dash's stoppedjobs
12
+ # guard, probed batch and interactive alike; only the interactive prompt
13
+ # loop re-arms the warning).
14
+ class Exit < Base
15
+ extend T::Sig
16
+
17
+ sig { returns(Status) }
18
+ def call
19
+ return refuse_stopped if stopped_jobs?
20
+
21
+ raise ExitSignal, code
22
+ end
23
+
24
+ private
25
+
26
+ # Stopped jobs block the exit once (the Control's dash-job_warning
27
+ # window arms on the first refusal).
28
+ sig { returns(T::Boolean) }
29
+ def stopped_jobs?
30
+ executor.jobs.ordered.any?(&:stopped?) && executor.jobs.control.warn_exit?
31
+ end
32
+
33
+ sig { returns(Status) }
34
+ def refuse_stopped
35
+ stderr.puts('You have stopped jobs.')
36
+ success
37
+ end
38
+
39
+ sig { returns(Integer) }
40
+ def code
41
+ operands.empty? ? executor.trap_runner.exiting_status : numeric_operand(operands.fetch(0))
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,20 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Rush
5
+ module Builtins
6
+ # `export name[=value] ...` — mark each name for inclusion in the environment
7
+ # of subsequently executed commands; see Declare for the shared engine.
8
+ # (The `-p` listing form arrives with the variable-printing slice.)
9
+ class Export < Declare
10
+ extend T::Sig
11
+
12
+ private
13
+
14
+ sig { params(operand: String).void }
15
+ def declare(operand)
16
+ executor.state.variables.export_operand(operand)
17
+ end
18
+ end
19
+ end
20
+ end