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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +94 -0
- data/exe/rush +10 -0
- data/grammar/shell.y +272 -0
- data/grammar/shell.y.output +2354 -0
- data/lib/rush/alias_table.rb +48 -0
- data/lib/rush/assignments.rb +48 -0
- data/lib/rush/ast/and_or.rb +37 -0
- data/lib/rush/ast/assignment.rb +20 -0
- data/lib/rush/ast/brace_group.rb +25 -0
- data/lib/rush/ast/case_node.rb +41 -0
- data/lib/rush/ast/condition_loop.rb +34 -0
- data/lib/rush/ast/for_node.rb +34 -0
- data/lib/rush/ast/function_def.rb +26 -0
- data/lib/rush/ast/if_node.rb +29 -0
- data/lib/rush/ast/list.rb +47 -0
- data/lib/rush/ast/node.rb +17 -0
- data/lib/rush/ast/param_ref.rb +59 -0
- data/lib/rush/ast/pipeline.rb +45 -0
- data/lib/rush/ast/redirect.rb +21 -0
- data/lib/rush/ast/redirected.rb +26 -0
- data/lib/rush/ast/simple_command.rb +62 -0
- data/lib/rush/ast/subshell.rb +24 -0
- data/lib/rush/ast/until_node.rb +18 -0
- data/lib/rush/ast/while_node.rb +18 -0
- data/lib/rush/ast/word.rb +44 -0
- data/lib/rush/ast/word_segment.rb +203 -0
- data/lib/rush/background_runner.rb +68 -0
- data/lib/rush/bracket_expression.rb +107 -0
- data/lib/rush/bracket_scanner.rb +64 -0
- data/lib/rush/builtins/alias_.rb +74 -0
- data/lib/rush/builtins/base.rb +96 -0
- data/lib/rush/builtins/break_.rb +19 -0
- data/lib/rush/builtins/cd.rb +127 -0
- data/lib/rush/builtins/colon.rb +16 -0
- data/lib/rush/builtins/command.rb +137 -0
- data/lib/rush/builtins/command_options.rb +103 -0
- data/lib/rush/builtins/continue_.rb +19 -0
- data/lib/rush/builtins/declare.rb +27 -0
- data/lib/rush/builtins/defaults.rb +28 -0
- data/lib/rush/builtins/dot.rb +52 -0
- data/lib/rush/builtins/echo.rb +37 -0
- data/lib/rush/builtins/eval.rb +25 -0
- data/lib/rush/builtins/exec.rb +54 -0
- data/lib/rush/builtins/exit.rb +45 -0
- data/lib/rush/builtins/export.rb +20 -0
- data/lib/rush/builtins/false_.rb +16 -0
- data/lib/rush/builtins/fd_operand.rb +87 -0
- data/lib/rush/builtins/getopts.rb +96 -0
- data/lib/rush/builtins/hash.rb +64 -0
- data/lib/rush/builtins/job_resume.rb +107 -0
- data/lib/rush/builtins/jobs.rb +86 -0
- data/lib/rush/builtins/kill.rb +152 -0
- data/lib/rush/builtins/local.rb +40 -0
- data/lib/rush/builtins/loop_jump.rb +45 -0
- data/lib/rush/builtins/printf.rb +39 -0
- data/lib/rush/builtins/printf_formatter.rb +111 -0
- data/lib/rush/builtins/pwd.rb +17 -0
- data/lib/rush/builtins/read.rb +93 -0
- data/lib/rush/builtins/read_input.rb +146 -0
- data/lib/rush/builtins/readonly.rb +20 -0
- data/lib/rush/builtins/registry.rb +36 -0
- data/lib/rush/builtins/return_.rb +24 -0
- data/lib/rush/builtins/set.rb +101 -0
- data/lib/rush/builtins/shift.rb +35 -0
- data/lib/rush/builtins/test_.rb +48 -0
- data/lib/rush/builtins/test_context.rb +24 -0
- data/lib/rush/builtins/test_expr.rb +74 -0
- data/lib/rush/builtins/test_grammar.rb +117 -0
- data/lib/rush/builtins/test_operators.rb +94 -0
- data/lib/rush/builtins/test_tokens.rb +69 -0
- data/lib/rush/builtins/times.rb +35 -0
- data/lib/rush/builtins/trap.rb +85 -0
- data/lib/rush/builtins/true_.rb +16 -0
- data/lib/rush/builtins/type_.rb +28 -0
- data/lib/rush/builtins/ulimit.rb +258 -0
- data/lib/rush/builtins/umask.rb +81 -0
- data/lib/rush/builtins/unalias.rb +46 -0
- data/lib/rush/builtins/unset.rb +40 -0
- data/lib/rush/builtins/wait.rb +127 -0
- data/lib/rush/cli.rb +31 -0
- data/lib/rush/command_assignments.rb +56 -0
- data/lib/rush/command_lookup.rb +228 -0
- data/lib/rush/command_resolution.rb +115 -0
- data/lib/rush/command_runner.rb +152 -0
- data/lib/rush/command_text.rb +140 -0
- data/lib/rush/environment.rb +134 -0
- data/lib/rush/errexit_context.rb +68 -0
- data/lib/rush/error_policy.rb +64 -0
- data/lib/rush/errors.rb +100 -0
- data/lib/rush/escape_table.rb +22 -0
- data/lib/rush/escaped_bracket.rb +49 -0
- data/lib/rush/executor.rb +164 -0
- data/lib/rush/exit_trap.rb +109 -0
- data/lib/rush/expansion/arithmetic/evaluator.rb +43 -0
- data/lib/rush/expansion/arithmetic/nodes.rb +180 -0
- data/lib/rush/expansion/arithmetic/number.rb +104 -0
- data/lib/rush/expansion/arithmetic/parser.rb +155 -0
- data/lib/rush/expansion/arithmetic/tokenizer.rb +56 -0
- data/lib/rush/expansion/arithmetic_expander.rb +38 -0
- data/lib/rush/expansion/command_substitution.rb +87 -0
- data/lib/rush/expansion/field_part.rb +12 -0
- data/lib/rush/expansion/field_splitter.rb +23 -0
- data/lib/rush/expansion/glob_expander.rb +48 -0
- data/lib/rush/expansion/ifs.rb +62 -0
- data/lib/rush/expansion/ifs_scanner.rb +168 -0
- data/lib/rush/expansion/parameter_expander.rb +204 -0
- data/lib/rush/expansion/parameter_forms.rb +22 -0
- data/lib/rush/expansion/pattern_removal.rb +63 -0
- data/lib/rush/expansion/pipeline.rb +124 -0
- data/lib/rush/expansion/read_char.rb +13 -0
- data/lib/rush/expansion/read_field_scanner.rb +159 -0
- data/lib/rush/expansion/read_splitter.rb +40 -0
- data/lib/rush/expansion/resolver.rb +23 -0
- data/lib/rush/expansion/tilde_expander.rb +102 -0
- data/lib/rush/external.rb +75 -0
- data/lib/rush/fd_entry.rb +60 -0
- data/lib/rush/for_runner.rb +50 -0
- data/lib/rush/function_frame.rb +30 -0
- data/lib/rush/function_runner.rb +37 -0
- data/lib/rush/function_table.rb +35 -0
- data/lib/rush/getopts_parser.rb +195 -0
- data/lib/rush/getopts_state.rb +104 -0
- data/lib/rush/here_doc.rb +57 -0
- data/lib/rush/interactive_signals.rb +26 -0
- data/lib/rush/invocation.rb +190 -0
- data/lib/rush/io_table.rb +94 -0
- data/lib/rush/job_control.rb +197 -0
- data/lib/rush/job_report.rb +45 -0
- data/lib/rush/job_spec.rb +51 -0
- data/lib/rush/job_table/control.rb +103 -0
- data/lib/rush/job_table/interruptible_wait.rb +70 -0
- data/lib/rush/job_table/job.rb +181 -0
- data/lib/rush/job_table.rb +194 -0
- data/lib/rush/lexer/alias_expander.rb +129 -0
- data/lib/rush/lexer/braced_reader.rb +86 -0
- data/lib/rush/lexer/case_frame.rb +114 -0
- data/lib/rush/lexer/case_tracker.rb +146 -0
- data/lib/rush/lexer/dollar_scanner.rb +53 -0
- data/lib/rush/lexer/double_quote_scanner.rb +71 -0
- data/lib/rush/lexer/heredoc_body.rb +91 -0
- data/lib/rush/lexer/heredoc_reader.rb +93 -0
- data/lib/rush/lexer/lex_state.rb +128 -0
- data/lib/rush/lexer/operator_table.rb +32 -0
- data/lib/rush/lexer/param_scanner.rb +43 -0
- data/lib/rush/lexer/paren_reader.rb +113 -0
- data/lib/rush/lexer/paren_regions.rb +74 -0
- data/lib/rush/lexer/quote_skips.rb +77 -0
- data/lib/rush/lexer/quoted_word.rb +67 -0
- data/lib/rush/lexer/scanner_predicates.rb +20 -0
- data/lib/rush/lexer/source_lines.rb +43 -0
- data/lib/rush/lexer/substitution_reader.rb +77 -0
- data/lib/rush/lexer/token_classifier.rb +144 -0
- data/lib/rush/lexer/token_predicates.rb +23 -0
- data/lib/rush/lexer/word_scanner.rb +134 -0
- data/lib/rush/lexer.rb +174 -0
- data/lib/rush/loop_control_handling.rb +29 -0
- data/lib/rush/loop_nesting.rb +50 -0
- data/lib/rush/loop_runner.rb +55 -0
- data/lib/rush/option_cluster.rb +39 -0
- data/lib/rush/options.rb +76 -0
- data/lib/rush/param_text.rb +39 -0
- data/lib/rush/parser.rb +1213 -0
- data/lib/rush/parser_support.rb +112 -0
- data/lib/rush/pattern_scanner.rb +35 -0
- data/lib/rush/pending_signals.rb +57 -0
- data/lib/rush/pipeline_runner.rb +173 -0
- data/lib/rush/pipeline_statuses.rb +45 -0
- data/lib/rush/positional.rb +45 -0
- data/lib/rush/posix_pattern.rb +78 -0
- data/lib/rush/program_input.rb +89 -0
- data/lib/rush/program_reader.rb +69 -0
- data/lib/rush/program_session.rb +83 -0
- data/lib/rush/prompt.rb +52 -0
- data/lib/rush/redirect_scope.rb +64 -0
- data/lib/rush/redirection/dup_redirect.rb +58 -0
- data/lib/rush/redirection/file_redirect.rb +50 -0
- data/lib/rush/redirection/here_doc_redirect.rb +17 -0
- data/lib/rush/redirection/registry.rb +53 -0
- data/lib/rush/repl.rb +133 -0
- data/lib/rush/runtime_type_checks.rb +16 -0
- data/lib/rush/scope.rb +79 -0
- data/lib/rush/segment_buffer.rb +47 -0
- data/lib/rush/shell_parameters.rb +77 -0
- data/lib/rush/shell_pattern.rb +80 -0
- data/lib/rush/shell_state.rb +163 -0
- data/lib/rush/shell_variables.rb +67 -0
- data/lib/rush/signal_report.rb +41 -0
- data/lib/rush/signals.rb +68 -0
- data/lib/rush/source.rb +82 -0
- data/lib/rush/source_line_counter.rb +41 -0
- data/lib/rush/source_runner.rb +47 -0
- data/lib/rush/startup.rb +75 -0
- data/lib/rush/status.rb +93 -0
- data/lib/rush/stop_relay.rb +32 -0
- data/lib/rush/subshell_runner.rb +70 -0
- data/lib/rush/system_calls/collation.rb +155 -0
- data/lib/rush/system_calls/file_tests.rb +95 -0
- data/lib/rush/system_calls/process_control.rb +165 -0
- data/lib/rush/system_calls/process_identity.rb +43 -0
- data/lib/rush/system_calls/resource_limits.rb +51 -0
- data/lib/rush/system_calls.rb +206 -0
- data/lib/rush/terminal.rb +126 -0
- data/lib/rush/trap_runner.rb +169 -0
- data/lib/rush/trap_table.rb +49 -0
- data/lib/rush/umask_mode.rb +133 -0
- data/lib/rush/version.rb +6 -0
- data/lib/rush.rb +190 -0
- metadata +527 -0
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
# Resolves a command name for `type` and `command -v`: a reserved word, a
|
|
6
|
+
# function, a special or regular builtin, or an executable file found by
|
|
7
|
+
# searching PATH (or used directly when the name contains a slash). #find runs
|
|
8
|
+
# an ordered list of resolvers and returns the first Match (Unknown if none).
|
|
9
|
+
# An explicit `path:` replaces $PATH for the file search — `command -p` passes
|
|
10
|
+
# the system default PATH through it.
|
|
11
|
+
class CommandLookup
|
|
12
|
+
extend T::Sig
|
|
13
|
+
|
|
14
|
+
KEYWORDS = %w[! { } case do done elif else esac fi for if in then until while].freeze
|
|
15
|
+
RESOLVERS = %i[as_keyword as_alias as_function as_special as_builtin as_file].freeze
|
|
16
|
+
|
|
17
|
+
# A resolved lookup result: it is "known", and subclasses render the line
|
|
18
|
+
# `type` (#describe) and `command -v` (#terse) print. Only an executable
|
|
19
|
+
# caches its own location; the others leave the cache untouched.
|
|
20
|
+
class Match
|
|
21
|
+
extend T::Sig
|
|
22
|
+
|
|
23
|
+
# name is nilable: `command -v` with no operand resolves the nil name, which
|
|
24
|
+
# flows through #find to Unknown.new(nil) (the Sorbet sig set tolerates this;
|
|
25
|
+
# the RBS keeps the narrower String — the two checkers are independent).
|
|
26
|
+
sig { params(name: T.nilable(String), detail: T.nilable(String)).void }
|
|
27
|
+
def initialize(name, detail = nil)
|
|
28
|
+
@name = name
|
|
29
|
+
@detail = detail
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
sig { returns(T::Boolean) }
|
|
33
|
+
def known?
|
|
34
|
+
true
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
sig { params(_cache: T::Hash[T.untyped, T.untyped]).void }
|
|
38
|
+
def cache_into(_cache)
|
|
39
|
+
nil
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# describe/terse are the Match protocol #find's result is used through; the
|
|
43
|
+
# subclasses provide them (the base is never instantiated, like AST::Node).
|
|
44
|
+
sig { returns(T.nilable(String)) }
|
|
45
|
+
def describe
|
|
46
|
+
raise NotImplementedError
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
sig { returns(T.nilable(String)) }
|
|
50
|
+
def terse
|
|
51
|
+
raise NotImplementedError
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
sig { returns(T.nilable(String)) }
|
|
57
|
+
attr_reader :name
|
|
58
|
+
|
|
59
|
+
sig { returns(T.nilable(String)) }
|
|
60
|
+
attr_reader :detail
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# An unresolved name: not known, and nothing to describe.
|
|
64
|
+
class Unknown < Match
|
|
65
|
+
extend T::Sig
|
|
66
|
+
|
|
67
|
+
sig { returns(T::Boolean) }
|
|
68
|
+
def known?
|
|
69
|
+
false
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
sig { returns(NilClass) }
|
|
73
|
+
def describe
|
|
74
|
+
nil
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# A keyword / function / special / regular builtin, named by a fixed label.
|
|
79
|
+
class Labelled < Match
|
|
80
|
+
extend T::Sig
|
|
81
|
+
|
|
82
|
+
sig { returns(String) }
|
|
83
|
+
def describe
|
|
84
|
+
"#{name} is #{detail}"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
sig { returns(String) }
|
|
88
|
+
def terse
|
|
89
|
+
T.must(name)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# A shell alias; `command -v` prints its definition.
|
|
94
|
+
class Aliased < Match
|
|
95
|
+
extend T::Sig
|
|
96
|
+
|
|
97
|
+
sig { returns(String) }
|
|
98
|
+
def describe
|
|
99
|
+
"#{name} is an alias for #{detail}"
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
sig { returns(String) }
|
|
103
|
+
def terse
|
|
104
|
+
"alias '#{"#{name}=#{detail}".gsub("'", %q('"'"'))}'"
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# An external executable found on PATH (or used directly via a slash path).
|
|
109
|
+
class Executable < Match
|
|
110
|
+
extend T::Sig
|
|
111
|
+
|
|
112
|
+
sig { returns(String) }
|
|
113
|
+
def describe
|
|
114
|
+
"#{name} is #{detail}"
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
sig { returns(T.nilable(String)) }
|
|
118
|
+
def terse
|
|
119
|
+
detail
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
sig { params(cache: T::Hash[T.untyped, T.untyped]).void }
|
|
123
|
+
def cache_into(cache)
|
|
124
|
+
cache[name] = detail
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
sig { params(executor: Executor, path: T.nilable(String)).void }
|
|
129
|
+
def initialize(executor, path: nil)
|
|
130
|
+
@executor = executor
|
|
131
|
+
@path = path
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# The Match for a name (Unknown if it resolves to nothing). An alias outranks
|
|
135
|
+
# a function/builtin but not a reserved word; a PATH/slash file is the last
|
|
136
|
+
# resort.
|
|
137
|
+
sig { params(name: T.nilable(String)).returns(Match) }
|
|
138
|
+
def find(name)
|
|
139
|
+
RESOLVERS.lazy.filter_map { |resolver| send(resolver, name) }.first || Unknown.new(name)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# The `type`/`command -V` description line for a name, or nil if unknown.
|
|
143
|
+
sig { params(name: T.nilable(String)).returns(T.nilable(String)) }
|
|
144
|
+
def describe(name)
|
|
145
|
+
find(name).describe
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# The executable file for a name on the search path (the name itself when
|
|
149
|
+
# it contains a slash), or nil. Public as well as feeding #find's file
|
|
150
|
+
# resolver: `command -p` resolves an external through it without the
|
|
151
|
+
# keyword/function/builtin layers, which that builtin handles separately.
|
|
152
|
+
sig { params(name: String).returns(T.nilable(String)) }
|
|
153
|
+
def executable_path(name)
|
|
154
|
+
return name if name.include?('/') && executable?(name)
|
|
155
|
+
return if name.include?('/')
|
|
156
|
+
|
|
157
|
+
dirs.map { |dir| join(dir, name) }.find { |candidate| executable?(candidate) }
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
private
|
|
161
|
+
|
|
162
|
+
sig { params(name: T.nilable(String)).returns(T.nilable(Match)) }
|
|
163
|
+
def as_keyword(name)
|
|
164
|
+
name && KEYWORDS.include?(name) ? Labelled.new(name, 'a shell keyword') : nil
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
sig { params(name: T.nilable(String)).returns(T.nilable(Match)) }
|
|
168
|
+
def as_alias(name)
|
|
169
|
+
value = name && aliases.value(name)
|
|
170
|
+
value ? Aliased.new(name, value) : nil
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
sig { params(name: T.nilable(String)).returns(T.nilable(Match)) }
|
|
174
|
+
def as_function(name)
|
|
175
|
+
name && functions.key?(name) ? Labelled.new(name, 'a shell function') : nil
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
sig { params(name: T.nilable(String)).returns(T.nilable(Match)) }
|
|
179
|
+
def as_special(name)
|
|
180
|
+
return unless name && CommandResolution.special_builtin?(name, @executor.builtins)
|
|
181
|
+
|
|
182
|
+
Labelled.new(name, 'a special shell builtin')
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
sig { params(name: T.nilable(String)).returns(T.nilable(Match)) }
|
|
186
|
+
def as_builtin(name)
|
|
187
|
+
name && @executor.builtins.key?(name) ? Labelled.new(name, 'a shell builtin') : nil
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
sig { params(name: T.nilable(String)).returns(T.nilable(Match)) }
|
|
191
|
+
def as_file(name)
|
|
192
|
+
return unless name
|
|
193
|
+
|
|
194
|
+
path = executable_path(name)
|
|
195
|
+
path ? Executable.new(name, path) : nil
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
sig { returns(AliasTable) }
|
|
199
|
+
def aliases
|
|
200
|
+
@executor.state.aliases
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
sig { returns(FunctionTable) }
|
|
204
|
+
def functions
|
|
205
|
+
@executor.state.functions
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
sig { params(dir: String, name: String).returns(String) }
|
|
209
|
+
def join(dir, name)
|
|
210
|
+
dir.empty? ? name : "#{dir}/#{name}"
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
sig { params(path: String).returns(T::Boolean) }
|
|
214
|
+
def executable?(path)
|
|
215
|
+
@executor.system.file?(path) && @executor.system.executable?(path)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
sig { returns(T::Array[String]) }
|
|
219
|
+
def dirs
|
|
220
|
+
search_path.split(':', -1)
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
sig { returns(String) }
|
|
224
|
+
def search_path
|
|
225
|
+
@path || @executor.state.variables.get('PATH') || ''
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
end
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
# Classifies a command for ordinary execution and carries the policies that
|
|
6
|
+
# follow from that classification. This deliberately does not resolve aliases,
|
|
7
|
+
# keywords or PATH: `type`/`command -v`, `command`, and execution have distinct
|
|
8
|
+
# POSIX search modes and share only builtin metadata/vocabulary.
|
|
9
|
+
class CommandResolution
|
|
10
|
+
extend T::Sig
|
|
11
|
+
|
|
12
|
+
# Ordinary execution's four mutually exclusive target kinds.
|
|
13
|
+
class Kind < T::Enum
|
|
14
|
+
enums do
|
|
15
|
+
SPECIAL_BUILTIN = new('special_builtin')
|
|
16
|
+
FUNCTION = new('function')
|
|
17
|
+
BUILTIN = new('builtin')
|
|
18
|
+
EXTERNAL = new('external')
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# How prefix assignments live for the selected target kind.
|
|
23
|
+
class AssignmentLifetime < T::Enum
|
|
24
|
+
enums do
|
|
25
|
+
PERSISTENT = new('persistent')
|
|
26
|
+
TEMPORARY = new('temporary')
|
|
27
|
+
ENVIRONMENT = new('environment')
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Whether redirect setup aborts the shell or fails only this command.
|
|
32
|
+
class RedirectFailure < T::Enum
|
|
33
|
+
enums do
|
|
34
|
+
FATAL = new('fatal')
|
|
35
|
+
ORDINARY = new('ordinary')
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
Policy = T.type_alias { [AssignmentLifetime, RedirectFailure] }
|
|
40
|
+
SPECIAL_BUILTINS = %w[: . break continue eval exec exit export readonly return set shift times trap unset].freeze
|
|
41
|
+
POLICIES = T.let(
|
|
42
|
+
{
|
|
43
|
+
Kind::SPECIAL_BUILTIN => [AssignmentLifetime::PERSISTENT, RedirectFailure::FATAL].freeze,
|
|
44
|
+
Kind::FUNCTION => [AssignmentLifetime::TEMPORARY, RedirectFailure::ORDINARY].freeze,
|
|
45
|
+
Kind::BUILTIN => [AssignmentLifetime::TEMPORARY, RedirectFailure::ORDINARY].freeze,
|
|
46
|
+
Kind::EXTERNAL => [AssignmentLifetime::ENVIRONMENT, RedirectFailure::ORDINARY].freeze
|
|
47
|
+
}.freeze,
|
|
48
|
+
T::Hash[Kind, Policy]
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
sig { returns(Kind) }
|
|
52
|
+
attr_reader :kind
|
|
53
|
+
|
|
54
|
+
sig { returns(AssignmentLifetime) }
|
|
55
|
+
attr_reader :assignment_lifetime
|
|
56
|
+
|
|
57
|
+
sig { returns(RedirectFailure) }
|
|
58
|
+
attr_reader :redirect_failure
|
|
59
|
+
|
|
60
|
+
sig { params(kind: Kind).void }
|
|
61
|
+
def initialize(kind)
|
|
62
|
+
@kind = kind
|
|
63
|
+
@assignment_lifetime, @redirect_failure = POLICIES.fetch(kind)
|
|
64
|
+
end
|
|
65
|
+
private :initialize
|
|
66
|
+
|
|
67
|
+
sig do
|
|
68
|
+
params(name: String, functions: FunctionTable, builtins: Builtins::Registry).returns(CommandResolution)
|
|
69
|
+
end
|
|
70
|
+
def self.for_execution(name, functions, builtins)
|
|
71
|
+
new(execution_kind(name, functions, builtins))
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
sig { params(name: String, builtins: Builtins::Registry).returns(T::Boolean) }
|
|
75
|
+
def self.special_builtin?(name, builtins)
|
|
76
|
+
SPECIAL_BUILTINS.include?(name) && builtins.key?(name)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
sig { returns(T::Boolean) }
|
|
80
|
+
def builtin?
|
|
81
|
+
[Kind::SPECIAL_BUILTIN, Kind::BUILTIN].include?(kind)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
sig { returns(T::Boolean) }
|
|
85
|
+
def function?
|
|
86
|
+
kind == Kind::FUNCTION
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
sig { returns(T::Boolean) }
|
|
90
|
+
def persistent_assignments?
|
|
91
|
+
assignment_lifetime == AssignmentLifetime::PERSISTENT
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
sig { returns(T::Boolean) }
|
|
95
|
+
def temporary_assignments?
|
|
96
|
+
assignment_lifetime == AssignmentLifetime::TEMPORARY
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
sig { returns(T::Boolean) }
|
|
100
|
+
def fatal_redirect?
|
|
101
|
+
redirect_failure == RedirectFailure::FATAL
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
sig do
|
|
105
|
+
params(name: String, functions: FunctionTable, builtins: Builtins::Registry).returns(Kind)
|
|
106
|
+
end
|
|
107
|
+
private_class_method def self.execution_kind(name, functions, builtins)
|
|
108
|
+
return Kind::SPECIAL_BUILTIN if special_builtin?(name, builtins)
|
|
109
|
+
return Kind::FUNCTION if functions.key?(name)
|
|
110
|
+
return Kind::BUILTIN if builtins.key?(name)
|
|
111
|
+
|
|
112
|
+
Kind::EXTERNAL
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
# Runs a SimpleCommand: expand argv, classify it for ordinary execution,
|
|
6
|
+
# evaluate redirections into a per-command IoTable, then apply the resolution's
|
|
7
|
+
# assignment and redirect policies while dispatching the selected command kind.
|
|
8
|
+
class CommandRunner
|
|
9
|
+
extend T::Sig
|
|
10
|
+
|
|
11
|
+
sig { params(executor: Executor, command: AST::SimpleCommand, base_io: IoTable).void }
|
|
12
|
+
def initialize(executor, command, base_io = executor.io)
|
|
13
|
+
@executor = executor
|
|
14
|
+
@command = command
|
|
15
|
+
@base_io = base_io
|
|
16
|
+
@assignments = CommandAssignments.new(command.assignments, executor.expander)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
sig { returns(Status) }
|
|
20
|
+
def call
|
|
21
|
+
@executor.reset_cmd_sub_status
|
|
22
|
+
argv = @executor.expander.expand(@command.words)
|
|
23
|
+
return run_bare if argv.empty?
|
|
24
|
+
|
|
25
|
+
trace(argv)
|
|
26
|
+
run_command(argv)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
# POSIX 2.5.3: each xtrace line goes to stderr prefixed with the expanded
|
|
32
|
+
# PS4 (parameter expansion only — see Prompt). The standard repeats PS4's
|
|
33
|
+
# first character per level of indirection; rush's tracer has no nesting
|
|
34
|
+
# depth (and dash never repeats), so a single prefix is correct here.
|
|
35
|
+
sig { params(argv: T::Array[String]).void }
|
|
36
|
+
def trace(argv)
|
|
37
|
+
return unless @executor.state.options.on?(:xtrace)
|
|
38
|
+
|
|
39
|
+
@executor.io.get(2).puts("#{Prompt.new(@executor).trace}#{argv.join(' ')}")
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# No command word: perform redirections then assignments (POSIX order), and
|
|
43
|
+
# take the status of the last command substitution either ran (Status.success
|
|
44
|
+
# when none did), as published via the executor's cmd-sub channel.
|
|
45
|
+
# with_redirects opens/truncates the targets for their side effects, then
|
|
46
|
+
# flushes+closes them after the assignments so a later command sees the data.
|
|
47
|
+
sig { returns(Status) }
|
|
48
|
+
def run_bare
|
|
49
|
+
@executor.redirect_scope.with_redirects(@command.redirects, @base_io) { |io| persist_assignments(io) }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
sig { params(io: IoTable).returns(Status) }
|
|
53
|
+
def persist_assignments(io)
|
|
54
|
+
@executor.with_io(io) do
|
|
55
|
+
@assignments.persist_to(@executor.state.variables)
|
|
56
|
+
@executor.cmd_sub_status
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# POSIX command search: special builtin, then function (so a function may
|
|
61
|
+
# override a regular builtin), then regular builtin, then PATH. A redirect
|
|
62
|
+
# error leaves a regular command unrun with status 2 (RedirectError reaches
|
|
63
|
+
# Executor#run), but on a special builtin it aborts the shell (POSIX 2.8.1),
|
|
64
|
+
# so it is re-raised as a fatal BuiltinError.
|
|
65
|
+
sig { params(argv: T::Array[String]).returns(Status) }
|
|
66
|
+
def run_command(argv)
|
|
67
|
+
resolution = CommandResolution.for_execution(argv.fetch(0), @executor.state.functions, @executor.builtins)
|
|
68
|
+
dispatch_with_redirects(resolution, argv)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
sig { params(resolution: CommandResolution, argv: T::Array[String]).returns(Status) }
|
|
72
|
+
def dispatch_with_redirects(resolution, argv)
|
|
73
|
+
@executor.redirect_scope.with_redirects(@command.redirects, @base_io) { |io| dispatch(resolution, argv, io) }
|
|
74
|
+
rescue RedirectError => e
|
|
75
|
+
raise unless resolution.fatal_redirect?
|
|
76
|
+
|
|
77
|
+
raise BuiltinError, e.message
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
sig { params(resolution: CommandResolution, argv: T::Array[String], io: IoTable).returns(Status) }
|
|
81
|
+
def dispatch(resolution, argv, io)
|
|
82
|
+
environment = command_env(io)
|
|
83
|
+
apply_assignment_policy(resolution, argv, io, environment)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
sig do
|
|
87
|
+
params(resolution: CommandResolution, argv: T::Array[String], io: IoTable,
|
|
88
|
+
environment: T::Hash[String, String]).returns(Status)
|
|
89
|
+
end
|
|
90
|
+
def apply_assignment_policy(resolution, argv, io, environment)
|
|
91
|
+
return persistent_dispatch(argv, io, environment) if resolution.persistent_assignments?
|
|
92
|
+
return temporary_dispatch(resolution, argv, io, environment) if resolution.temporary_assignments?
|
|
93
|
+
|
|
94
|
+
external(argv, io, environment)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
sig { params(argv: T::Array[String], io: IoTable, environment: T::Hash[String, String]).returns(Status) }
|
|
98
|
+
def persistent_dispatch(argv, io, environment)
|
|
99
|
+
@assignments.persist_environment(environment, @executor.state.variables)
|
|
100
|
+
invoke_builtin(argv, io, environment)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
sig do
|
|
104
|
+
params(resolution: CommandResolution, argv: T::Array[String], io: IoTable,
|
|
105
|
+
environment: T::Hash[String, String]).returns(Status)
|
|
106
|
+
end
|
|
107
|
+
def temporary_dispatch(resolution, argv, io, environment)
|
|
108
|
+
values = @assignments.temporary_environment(environment)
|
|
109
|
+
@executor.state.variables.with_temporary(values) { invoke_temporary(resolution, argv, io, environment) }
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
sig do
|
|
113
|
+
params(resolution: CommandResolution, argv: T::Array[String], io: IoTable,
|
|
114
|
+
environment: T::Hash[String, String]).returns(Status)
|
|
115
|
+
end
|
|
116
|
+
def invoke_temporary(resolution, argv, io, environment)
|
|
117
|
+
resolution.function? ? invoke_function(argv, io) : invoke_builtin(argv, io, environment)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
sig { params(argv: T::Array[String], io: IoTable, environment: T::Hash[String, String]).returns(Status) }
|
|
121
|
+
def external(argv, io, environment)
|
|
122
|
+
@assignments.validate(@executor.state.variables)
|
|
123
|
+
External.new(@executor, argv, io, environment).call(@executor.job_control.job_text(@command))
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
sig { params(argv: T::Array[String], io: IoTable, environment: T::Hash[String, String]).returns(Status) }
|
|
127
|
+
# A builtin reading from or writing to a fd closed by n>&- raises EBADF; like
|
|
128
|
+
# dash, that fails the command (status 1) without killing the shell.
|
|
129
|
+
def invoke_builtin(argv, io, environment)
|
|
130
|
+
@executor.builtins.fetch(argv.fetch(0)).new(@executor, argv, io, environment).call
|
|
131
|
+
rescue Errno::EBADF
|
|
132
|
+
Status.new(1)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# A function runs in the current shell, not a subshell. The call's redirects
|
|
136
|
+
# (if any) bind the whole body and are torn down when it returns — even an
|
|
137
|
+
# `exec` inside is scoped to them, as dash does. With no redirects the body
|
|
138
|
+
# shares the shell's io table so an `exec` inside *persists*, so wrap in
|
|
139
|
+
# with_io only when a redirect actually layered a new table over the base.
|
|
140
|
+
sig { params(argv: T::Array[String], io: IoTable).returns(Status) }
|
|
141
|
+
def invoke_function(argv, io)
|
|
142
|
+
body = @executor.state.functions.fetch(argv.fetch(0))
|
|
143
|
+
run = -> { FunctionRunner.new(@executor, body, argv.drop(1)).call }
|
|
144
|
+
io.equal?(@executor.io) ? run.call : @executor.with_io(io, &run)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
sig { params(io: IoTable).returns(T::Hash[String, String]) }
|
|
148
|
+
def command_env(io)
|
|
149
|
+
@executor.with_io(io) { @assignments.environment_for(@executor.state.variables) }
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
# Renders an AST back into dash's canonical job text — the command column
|
|
6
|
+
# of the jobs listing, fg's echoed line and bg's "[n] ..." (dash's cmdtxt,
|
|
7
|
+
# probed shape by shape off-tty): words re-quoted with double quotes when
|
|
8
|
+
# they carried quoting ($ escaped as \$), parameter expansions always
|
|
9
|
+
# braced (${T}, "${@}", ${T:-9} with the raw default), command
|
|
10
|
+
# substitutions and here-document bodies elided to $(...) and <<...,
|
|
11
|
+
# redirect fds explicit (1>/dev/null, 0<f), assignments dropped from a
|
|
12
|
+
# command that has words — an assignment-only command renders as `set`, a
|
|
13
|
+
# dash quirk kept for parity — group braces dropped, elif spelled as a
|
|
14
|
+
# nested else-if, a negated pipeline prefixed with a spaceless !, and the
|
|
15
|
+
# implicit for-loop list spelled out as in "${@}".
|
|
16
|
+
module CommandText
|
|
17
|
+
extend T::Sig
|
|
18
|
+
|
|
19
|
+
TABLE = T.let({
|
|
20
|
+
AST::List => :list, AST::AndOr => :and_or, AST::Pipeline => :pipeline,
|
|
21
|
+
AST::SimpleCommand => :simple, AST::Subshell => :subshell, AST::BraceGroup => :group,
|
|
22
|
+
AST::If => :if_node, AST::While => :while_node, AST::Until => :until_node,
|
|
23
|
+
AST::For => :for_node, AST::Case => :case_node, AST::FunctionDef => :function_def,
|
|
24
|
+
AST::Redirected => :redirected
|
|
25
|
+
}.freeze, T::Hash[T.untyped, Symbol])
|
|
26
|
+
|
|
27
|
+
sig { params(node: AST::Node).returns(String) }
|
|
28
|
+
def self.render(node)
|
|
29
|
+
method(TABLE.fetch(node.class)).call(node)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
sig { params(node: AST::List).returns(String) }
|
|
33
|
+
def self.list(node)
|
|
34
|
+
node.entries.map { |entry| "#{render(entry.and_or)}#{entry.async ? ' &' : ';'}" }
|
|
35
|
+
.join(' ').delete_suffix(';')
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
sig { params(node: AST::AndOr).returns(String) }
|
|
39
|
+
def self.and_or(node)
|
|
40
|
+
"#{render(node.left)} #{node.op == :and ? '&&' : '||'} #{render(node.right)}"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
sig { params(node: AST::Pipeline).returns(String) }
|
|
44
|
+
def self.pipeline(node)
|
|
45
|
+
"#{'!' if node.negate}#{stages(node.commands)}"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# A pipeline's stages, also reachable for PipelineRunner's already-split
|
|
49
|
+
# command list (the adopted-job text of a stopped foreground pipeline).
|
|
50
|
+
sig { params(commands: T::Array[AST::Node]).returns(String) }
|
|
51
|
+
def self.stages(commands)
|
|
52
|
+
commands.map { |command| render(command) }.join(' | ')
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
sig { params(node: AST::SimpleCommand).returns(String) }
|
|
56
|
+
def self.simple(node)
|
|
57
|
+
words = node.words.map { |word| word(word) }
|
|
58
|
+
words = ['set'] if words.empty?
|
|
59
|
+
(words + node.redirects.map { |redirect| redirect(redirect) }).join(' ')
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
sig { params(node: AST::Subshell).returns(String) }
|
|
63
|
+
def self.subshell(node)
|
|
64
|
+
"(#{render(node.body)})"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
sig { params(node: AST::BraceGroup).returns(String) }
|
|
68
|
+
def self.group(node)
|
|
69
|
+
render(node.body)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
sig { params(node: AST::If).returns(String) }
|
|
73
|
+
def self.if_node(node)
|
|
74
|
+
tail = node.alternative ? " else #{render(T.must(node.alternative))};" : ''
|
|
75
|
+
"if #{render(node.condition)}; then #{render(node.consequent)};#{tail} fi"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
sig { params(node: AST::While).returns(String) }
|
|
79
|
+
def self.while_node(node)
|
|
80
|
+
"while #{render(node.condition)}; do #{render(node.body)}; done"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
sig { params(node: AST::Until).returns(String) }
|
|
84
|
+
def self.until_node(node)
|
|
85
|
+
"until #{render(node.condition)}; do #{render(node.body)}; done"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
sig { params(node: AST::For).returns(String) }
|
|
89
|
+
def self.for_node(node)
|
|
90
|
+
list = node.words ? T.must(node.words).map { |word| word(word) }.join(' ') : '"${@}"'
|
|
91
|
+
"for #{node.name} in #{list}; do #{render(node.body)}; done"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
sig { params(node: AST::Case).returns(String) }
|
|
95
|
+
def self.case_node(node)
|
|
96
|
+
items = node.items.map { |item| "#{word(item.patterns.fetch(0))}) #{render(item.body)};;" }
|
|
97
|
+
"case #{word(node.word)} in #{items.join(' ')} esac"
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
sig { params(node: AST::FunctionDef).returns(String) }
|
|
101
|
+
def self.function_def(node)
|
|
102
|
+
"#{node.name}() { #{render(node.body)}; }"
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
sig { params(node: AST::Redirected).returns(String) }
|
|
106
|
+
def self.redirected(node)
|
|
107
|
+
([render(node.command)] + node.redirects.map { |redirect| redirect(redirect) }).join(' ')
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# dash prints every fd explicitly (1>/dev/null, 0<f, 2>&1) and elides a
|
|
111
|
+
# here-document to <<... .
|
|
112
|
+
sig { params(redirect: AST::Redirect).returns(String) }
|
|
113
|
+
def self.redirect(redirect)
|
|
114
|
+
return '<<...' if redirect.kind == :heredoc
|
|
115
|
+
|
|
116
|
+
op, fd = T.must(OPERATORS[redirect.kind])
|
|
117
|
+
"#{redirect.io_number || fd}#{op}#{word(T.cast(redirect.target, AST::Word))}"
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
OPERATORS = T.let({
|
|
121
|
+
in: ['<', 0], out: ['>', 1], append: ['>>', 1], clobber: ['>|', 1],
|
|
122
|
+
readwrite: ['<>', 0], dup_out: ['>&', 1], dup_in: ['<&', 0]
|
|
123
|
+
}.freeze, T::Hash[Symbol, [String, Integer]])
|
|
124
|
+
|
|
125
|
+
# Consecutive quoted segments share one pair of double quotes (dash
|
|
126
|
+
# renders "x$T.y" as one quoted run); each segment canonicalises itself
|
|
127
|
+
# (WordSegment#canon).
|
|
128
|
+
sig { params(word: AST::Word).returns(String) }
|
|
129
|
+
def self.word(word)
|
|
130
|
+
runs = word.segments.chunk_while { |left, right| left.quoted == right.quoted }
|
|
131
|
+
runs.map { |run| run_text(run) }.join
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
sig { params(run: T::Array[AST::WordSegment[T.untyped]]).returns(String) }
|
|
135
|
+
def self.run_text(run)
|
|
136
|
+
body = run.map(&:canon).join
|
|
137
|
+
T.must(run.fetch(0)).quoted ? "\"#{body}\"" : body
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|