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,48 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
# The shell's alias definitions: a name maps to the replacement text the lexer
|
|
6
|
+
# splices in when the name appears in command position (POSIX 2.3.1). The
|
|
7
|
+
# `alias` builtin validates names; the table just stores what it is given.
|
|
8
|
+
class AliasTable
|
|
9
|
+
extend T::Sig
|
|
10
|
+
|
|
11
|
+
sig { void }
|
|
12
|
+
def initialize
|
|
13
|
+
@aliases = {}
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
sig { params(name: String, value: String).returns(String) }
|
|
17
|
+
def define(name, value)
|
|
18
|
+
@aliases[name] = value
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
sig { params(name: String).returns(T.nilable(String)) }
|
|
22
|
+
def remove(name)
|
|
23
|
+
@aliases.delete(name)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
sig { params(name: String).returns(T.nilable(String)) }
|
|
27
|
+
def value(name)
|
|
28
|
+
@aliases.fetch(name, nil)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
sig { params(name: String).returns(T::Boolean) }
|
|
32
|
+
def key?(name)
|
|
33
|
+
@aliases.key?(name)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
sig { void }
|
|
37
|
+
def clear
|
|
38
|
+
@aliases.clear
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# name => value pairs sorted by name, for `alias` with no operands. dash lists
|
|
42
|
+
# in hash order; rush sorts for a deterministic, testable listing.
|
|
43
|
+
sig { returns(T::Array[[String, String]]) }
|
|
44
|
+
def listing
|
|
45
|
+
@aliases.sort
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
# Applies `name[=value]` declaration operands to shell variables. A missing `=`
|
|
6
|
+
# leaves the variable unchanged; `name=` assigns the empty string. The local
|
|
7
|
+
# path snapshots the parsed name before assignment.
|
|
8
|
+
class Assignments
|
|
9
|
+
extend T::Sig
|
|
10
|
+
|
|
11
|
+
sig { params(variables: ShellVariables).void }
|
|
12
|
+
def initialize(variables)
|
|
13
|
+
@variables = variables
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
sig { params(text: String).returns(String) }
|
|
17
|
+
def apply(text)
|
|
18
|
+
name, value = parse(text)
|
|
19
|
+
assign(name, value)
|
|
20
|
+
name
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
sig { params(text: String).returns(String) }
|
|
24
|
+
def apply_local(text)
|
|
25
|
+
name, value = parse(text)
|
|
26
|
+
@variables.declare_local(name)
|
|
27
|
+
assign(name, value)
|
|
28
|
+
name
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
sig { params(text: String).returns([String, T.nilable(String)]) }
|
|
34
|
+
def parse(text)
|
|
35
|
+
return [text, nil] unless text.include?('=')
|
|
36
|
+
|
|
37
|
+
parts = text.split('=', 2)
|
|
38
|
+
name = parts.fetch(0)
|
|
39
|
+
value = parts.fetch(1)
|
|
40
|
+
[name, value]
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
sig { params(name: String, value: T.nilable(String)).void }
|
|
44
|
+
def assign(name, value)
|
|
45
|
+
@variables.assign(name, value) if value
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
module AST
|
|
6
|
+
# `pipeline && pipeline` / `pipeline || pipeline`, left-associative. Runs the
|
|
7
|
+
# left side, then the right side only when the operator's short-circuit allows.
|
|
8
|
+
class AndOr < Node
|
|
9
|
+
extend T::Sig
|
|
10
|
+
|
|
11
|
+
attr_reader :left, :op, :right
|
|
12
|
+
|
|
13
|
+
sig { params(left: Node, op: Symbol, right: Node).void }
|
|
14
|
+
def initialize(left, op, right)
|
|
15
|
+
@left = left
|
|
16
|
+
@op = op
|
|
17
|
+
@right = right
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# The left side is non-final, so it runs in a tested context (errexit
|
|
21
|
+
# suppressed); only the final command's status reaches the errexit check.
|
|
22
|
+
sig { params(executor: Executor).returns(Status) }
|
|
23
|
+
def execute(executor)
|
|
24
|
+
status = executor.errexit.tested { executor.run(left) }
|
|
25
|
+
run_right?(status) ? executor.run(right) : status
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
sig { params(status: Status).returns(T::Boolean) }
|
|
31
|
+
def run_right?(status)
|
|
32
|
+
succeeded = status.success?
|
|
33
|
+
op == :and ? succeeded : !succeeded
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
module AST
|
|
6
|
+
# `name=value` — value is an unexpanded Word (it receives tilde and the
|
|
7
|
+
# step-1 expansions, but no field splitting or pathname expansion).
|
|
8
|
+
Assignment = Data.define(:name, :value)
|
|
9
|
+
|
|
10
|
+
# Behaviour for the Data-defined assignment payload.
|
|
11
|
+
class Assignment
|
|
12
|
+
extend T::Sig
|
|
13
|
+
|
|
14
|
+
sig { returns(Integer) }
|
|
15
|
+
def source_line
|
|
16
|
+
value.source_line
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
module AST
|
|
6
|
+
# `{ list; }` — groups commands without a subshell, so they run in the
|
|
7
|
+
# current shell environment. (Redirections on the group arrive with the
|
|
8
|
+
# fd-management slice.)
|
|
9
|
+
class BraceGroup < Node
|
|
10
|
+
extend T::Sig
|
|
11
|
+
|
|
12
|
+
attr_reader :body
|
|
13
|
+
|
|
14
|
+
sig { params(body: Node).void }
|
|
15
|
+
def initialize(body)
|
|
16
|
+
@body = body
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
sig { params(executor: Executor).returns(Status) }
|
|
20
|
+
def execute(executor)
|
|
21
|
+
executor.run(body)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
module AST
|
|
6
|
+
# One `pattern|pattern) body ;;` arm of a case statement.
|
|
7
|
+
CaseItem = Data.define(:patterns, :body)
|
|
8
|
+
|
|
9
|
+
# `case word in pattern) body ;; ... esac`. Expands the subject, then runs the
|
|
10
|
+
# body of the first arm with a pattern (fnmatch glob) that matches it; no
|
|
11
|
+
# match yields status 0. There is no fall-through.
|
|
12
|
+
class Case < Node
|
|
13
|
+
extend T::Sig
|
|
14
|
+
|
|
15
|
+
attr_reader :word, :items
|
|
16
|
+
|
|
17
|
+
sig { params(word: Word, items: T::Array[CaseItem]).void }
|
|
18
|
+
def initialize(word, items)
|
|
19
|
+
@word = word
|
|
20
|
+
@items = items
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
sig { params(executor: Executor).returns(Status) }
|
|
24
|
+
def execute(executor)
|
|
25
|
+
subject = executor.expander.expand_value(word)
|
|
26
|
+
item = items.find { |candidate| matches?(executor, candidate, subject) }
|
|
27
|
+
item ? executor.run(item.body) : Status.success
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
sig { params(executor: Executor, item: CaseItem, subject: String).returns(T::Boolean) }
|
|
33
|
+
def matches?(executor, item, subject)
|
|
34
|
+
item.patterns.any? do |pattern|
|
|
35
|
+
expanded = executor.expander.expand_pattern(pattern)
|
|
36
|
+
executor.system.fnmatch?(expanded, subject, locale: executor.state.variables.locale_settings)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
module AST
|
|
6
|
+
# Shared shape of the two condition-driven loops: hold the condition and
|
|
7
|
+
# body, delegate to LoopRunner with the subclass's kind — :while runs the
|
|
8
|
+
# body while the condition succeeds, :until until it does.
|
|
9
|
+
class ConditionLoop < Node
|
|
10
|
+
extend T::Sig
|
|
11
|
+
|
|
12
|
+
attr_reader :condition, :body
|
|
13
|
+
|
|
14
|
+
sig { params(condition: Node, body: Node).void }
|
|
15
|
+
def initialize(condition, body)
|
|
16
|
+
@condition = condition
|
|
17
|
+
@body = body
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
sig { params(executor: Executor).returns(Status) }
|
|
21
|
+
def execute(executor)
|
|
22
|
+
LoopRunner.new(executor, condition, body, kind).call
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
# The LoopRunner mode this node runs in.
|
|
28
|
+
sig { returns(Symbol) }
|
|
29
|
+
def kind
|
|
30
|
+
raise NotImplementedError
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
module AST
|
|
6
|
+
# `for name [in words]; do body; done`. With an explicit word list the words
|
|
7
|
+
# are expanded (and field-split); with no `in` clause the loop iterates over
|
|
8
|
+
# the positional parameters ($@).
|
|
9
|
+
class For < Node
|
|
10
|
+
extend T::Sig
|
|
11
|
+
|
|
12
|
+
attr_reader :name, :words, :body
|
|
13
|
+
|
|
14
|
+
sig { params(name: String, words: T.nilable(T::Array[Word]), body: Node).void }
|
|
15
|
+
def initialize(name, words, body)
|
|
16
|
+
@name = name
|
|
17
|
+
@words = words
|
|
18
|
+
@body = body
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
sig { params(executor: Executor).returns(Status) }
|
|
22
|
+
def execute(executor)
|
|
23
|
+
ForRunner.new(executor, name, values(executor), body).call
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
sig { params(executor: Executor).returns(T::Array[String]) }
|
|
29
|
+
def values(executor)
|
|
30
|
+
words ? executor.expander.expand(words) : executor.state.positional.to_a
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
module AST
|
|
6
|
+
# `name() compound-command` — registers the body in the function table and
|
|
7
|
+
# succeeds. The body runs (in the current shell) when the name is invoked.
|
|
8
|
+
class FunctionDef < Node
|
|
9
|
+
extend T::Sig
|
|
10
|
+
|
|
11
|
+
attr_reader :name, :body
|
|
12
|
+
|
|
13
|
+
sig { params(name: String, body: Node).void }
|
|
14
|
+
def initialize(name, body)
|
|
15
|
+
@name = name
|
|
16
|
+
@body = body
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
sig { params(executor: Executor).returns(Status) }
|
|
20
|
+
def execute(executor)
|
|
21
|
+
executor.state.functions.define(name, body)
|
|
22
|
+
Status.success
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
module AST
|
|
6
|
+
# `if cond; then body; [elif/else ...] fi`. Runs the condition list; on
|
|
7
|
+
# success runs the consequent, otherwise the alternative (an else List, a
|
|
8
|
+
# nested If for elif, or nil). With no matching branch the status is 0.
|
|
9
|
+
class If < Node
|
|
10
|
+
extend T::Sig
|
|
11
|
+
|
|
12
|
+
attr_reader :condition, :consequent, :alternative
|
|
13
|
+
|
|
14
|
+
sig { params(condition: Node, consequent: Node, alternative: T.nilable(Node)).void }
|
|
15
|
+
def initialize(condition, consequent, alternative)
|
|
16
|
+
@condition = condition
|
|
17
|
+
@consequent = consequent
|
|
18
|
+
@alternative = alternative
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
sig { params(executor: Executor).returns(Status) }
|
|
22
|
+
def execute(executor)
|
|
23
|
+
return executor.run(consequent) if executor.succeeds?(condition)
|
|
24
|
+
|
|
25
|
+
alternative ? executor.run(alternative) : Status.success
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
module AST
|
|
6
|
+
# One element of a list: an and-or list and whether it was terminated by `&`.
|
|
7
|
+
ListEntry = Data.define(:and_or, :async)
|
|
8
|
+
|
|
9
|
+
# A `;` / `&` / newline-separated list of and-or lists and the program root.
|
|
10
|
+
# Runs each entry in order; the list's status is the last entry's. An empty
|
|
11
|
+
# list (a blank or comment-only line, which is its own program under
|
|
12
|
+
# command-by-command reading) preserves $?, like dash. Async (`&`) entries
|
|
13
|
+
# fork a background subshell and return launch status in the parent.
|
|
14
|
+
class List < Node
|
|
15
|
+
extend T::Sig
|
|
16
|
+
|
|
17
|
+
attr_reader :entries
|
|
18
|
+
|
|
19
|
+
sig { params(entries: T::Array[ListEntry]).void }
|
|
20
|
+
def initialize(entries)
|
|
21
|
+
@entries = entries
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
sig { params(executor: Executor).returns(Status) }
|
|
25
|
+
def execute(executor)
|
|
26
|
+
entries.reduce(executor.state.last_status) { |_status, entry| run_entry(executor, entry) }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# A blank or comment-only program runs no command; SourceRunner skips it
|
|
30
|
+
# when tracking eval/dot's result status.
|
|
31
|
+
sig { returns(T::Boolean) }
|
|
32
|
+
def empty?
|
|
33
|
+
entries.empty?
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
# An async (&) command is exempt from errexit, so it runs in a tested context.
|
|
39
|
+
sig { params(executor: Executor, entry: ListEntry).returns(Status) }
|
|
40
|
+
def run_entry(executor, entry)
|
|
41
|
+
return executor.errexit.tested { executor.run_async(entry.and_or) } if entry.async
|
|
42
|
+
|
|
43
|
+
executor.run(entry.and_or)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
module AST
|
|
6
|
+
# Base for every AST node. Subclasses implement #execute(executor) so the
|
|
7
|
+
# executor dispatches polymorphically and never switches on node type.
|
|
8
|
+
class Node
|
|
9
|
+
extend T::Sig
|
|
10
|
+
|
|
11
|
+
sig { params(_executor: Executor).returns(Status) }
|
|
12
|
+
def execute(_executor)
|
|
13
|
+
raise NotImplementedError
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
module AST
|
|
6
|
+
# Parses the body of ${...}: a parameter name plus an optional operator — the
|
|
7
|
+
# default/assign/error/alternative forms (- = ? +, optionally ':'-prefixed),
|
|
8
|
+
# the prefix/suffix removal forms (# ## % %%), or the ${#name} length form
|
|
9
|
+
# (recognised before the others, since a leading # also names $#).
|
|
10
|
+
PARAM_BRACED = /\A(\w+|[@*#?$!0-])(:?[-=?+]|\#{1,2}|%{1,2})?(.*)\z/m
|
|
11
|
+
|
|
12
|
+
ParamRef = Data.define(:name, :op, :arg)
|
|
13
|
+
|
|
14
|
+
# A parameter reference parsed from $name or ${...}. The operator word is
|
|
15
|
+
# expanded lazily at expansion time.
|
|
16
|
+
class ParamRef
|
|
17
|
+
extend T::Sig
|
|
18
|
+
|
|
19
|
+
sig { params(name: String).returns(ParamRef) }
|
|
20
|
+
def self.simple(name)
|
|
21
|
+
new(name: name, op: nil, arg: nil)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
sig { params(body: String).returns(ParamRef) }
|
|
25
|
+
def self.parse(body)
|
|
26
|
+
return new(name: body.delete_prefix('#'), op: '#len', arg: nil) if length?(body)
|
|
27
|
+
|
|
28
|
+
match = braced_match(body)
|
|
29
|
+
new(name: braced_name(match), op: match[2], arg: match[3])
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
sig { params(body: String).returns(T::Boolean) }
|
|
33
|
+
def self.length?(body)
|
|
34
|
+
body.start_with?('#') && body.length > 1
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# dash's job text always braces a parameter — ${T}, "${@}", ${T:-9}
|
|
38
|
+
# with the op's raw source argument (CommandText).
|
|
39
|
+
sig { returns(String) }
|
|
40
|
+
def canon
|
|
41
|
+
return "${##{name}}" if op == '#len'
|
|
42
|
+
|
|
43
|
+
"${#{name}#{op}#{arg}}"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
sig { params(body: String).returns(MatchData) }
|
|
47
|
+
def self.braced_match(body)
|
|
48
|
+
PARAM_BRACED.match(body) || raise(ExpansionError, 'bad substitution')
|
|
49
|
+
end
|
|
50
|
+
private_class_method :braced_match
|
|
51
|
+
|
|
52
|
+
sig { params(match: MatchData).returns(String) }
|
|
53
|
+
def self.braced_name(match)
|
|
54
|
+
match[1] || raise(ExpansionError, 'bad substitution')
|
|
55
|
+
end
|
|
56
|
+
private_class_method :braced_name
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
module AST
|
|
6
|
+
# A pipeline of one or more commands, optionally negated with `!`. A single
|
|
7
|
+
# command runs in-process (so builtins affect the shell); a multi-stage
|
|
8
|
+
# pipeline forks each stage via PipelineRunner. Negation inverts the status.
|
|
9
|
+
class Pipeline < Node
|
|
10
|
+
extend T::Sig
|
|
11
|
+
|
|
12
|
+
attr_reader :commands, :negate
|
|
13
|
+
|
|
14
|
+
sig { params(commands: T::Array[Node], negate: T::Boolean).void }
|
|
15
|
+
def initialize(commands, negate)
|
|
16
|
+
@commands = commands
|
|
17
|
+
@negate = negate
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# A negated pipeline (`! cmd`) is exempt from errexit and runs its stages in
|
|
21
|
+
# a tested context; otherwise the leaf status is the errexit check point.
|
|
22
|
+
sig { params(executor: Executor).returns(Status) }
|
|
23
|
+
def execute(executor)
|
|
24
|
+
return invert(executor.errexit.tested { run_stages(executor) }) if negate
|
|
25
|
+
|
|
26
|
+
executor.errexit.exit_on_error(run_stages(executor))
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
sig { params(executor: Executor).returns(Status) }
|
|
32
|
+
def run_stages(executor)
|
|
33
|
+
case commands
|
|
34
|
+
in [command] then executor.run(command)
|
|
35
|
+
else PipelineRunner.new(executor, commands).call
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
sig { params(status: Status).returns(Status) }
|
|
40
|
+
def invert(status)
|
|
41
|
+
status.success? ? Status.failure : Status.success
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
module AST
|
|
6
|
+
# A redirection: a kind (:in/:out/:append/:dup_in/:dup_out/:readwrite/
|
|
7
|
+
# :clobber), an unexpanded target Word, and an optional explicit fd parsed
|
|
8
|
+
# from a leading IO_NUMBER (nil means the kind's default fd).
|
|
9
|
+
Redirect = Data.define(:kind, :target, :io_number)
|
|
10
|
+
|
|
11
|
+
# Behaviour for the Data-defined redirection payload.
|
|
12
|
+
class Redirect
|
|
13
|
+
extend T::Sig
|
|
14
|
+
|
|
15
|
+
sig { returns(Integer) }
|
|
16
|
+
def source_line
|
|
17
|
+
target.source_line
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
module AST
|
|
6
|
+
# A compound command (group, subshell, loop, if, case) carrying redirections
|
|
7
|
+
# on the whole command: they build a base IoTable that every command in the
|
|
8
|
+
# body inherits, restored once the command finishes. Its status is the body's.
|
|
9
|
+
class Redirected < Node
|
|
10
|
+
extend T::Sig
|
|
11
|
+
|
|
12
|
+
attr_reader :command, :redirects
|
|
13
|
+
|
|
14
|
+
sig { params(command: Node, redirects: T::Array[Redirect]).void }
|
|
15
|
+
def initialize(command, redirects)
|
|
16
|
+
@command = command
|
|
17
|
+
@redirects = redirects
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
sig { params(executor: Executor).returns(Status) }
|
|
21
|
+
def execute(executor)
|
|
22
|
+
executor.run_redirected(command, redirects)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
module AST
|
|
6
|
+
# `name=val ... cmd arg ... >file` — variable assignments, argv words and
|
|
7
|
+
# redirections, preserving both grouped accessors and original source parts.
|
|
8
|
+
class SimpleCommand < Node
|
|
9
|
+
extend T::Sig
|
|
10
|
+
|
|
11
|
+
Part = T.type_alias { T.any(Assignment, Word, Redirect) }
|
|
12
|
+
|
|
13
|
+
attr_reader :parts, :source_line
|
|
14
|
+
|
|
15
|
+
sig do
|
|
16
|
+
params(assignments: T::Array[Assignment], words: T::Array[Word], redirects: T::Array[Redirect],
|
|
17
|
+
source_line: Integer).returns(SimpleCommand)
|
|
18
|
+
end
|
|
19
|
+
def self.from_groups(assignments, words, redirects, source_line: 1)
|
|
20
|
+
new(assignments + words + redirects, source_line: source_line)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
sig { params(parts: T::Array[Part]).returns(Integer) }
|
|
24
|
+
def self.source_line(parts)
|
|
25
|
+
parts.map(&:source_line).min || 1
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
sig { params(parts: T::Array[Part], source_line: Integer).void }
|
|
29
|
+
def initialize(parts, source_line: self.class.source_line(parts))
|
|
30
|
+
@parts = parts
|
|
31
|
+
@source_line = source_line
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
sig { returns(T::Array[Assignment]) }
|
|
35
|
+
def assignments
|
|
36
|
+
result = T.let([], T::Array[Assignment])
|
|
37
|
+
parts.each { |part| result << part if part.is_a?(Assignment) }
|
|
38
|
+
result
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
sig { returns(T::Array[Word]) }
|
|
42
|
+
def words
|
|
43
|
+
result = T.let([], T::Array[Word])
|
|
44
|
+
parts.each { |part| result << part if part.is_a?(Word) }
|
|
45
|
+
result
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
sig { returns(T::Array[Redirect]) }
|
|
49
|
+
def redirects
|
|
50
|
+
result = T.let([], T::Array[Redirect])
|
|
51
|
+
parts.each { |part| result << part if part.is_a?(Redirect) }
|
|
52
|
+
result
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
sig { params(executor: Executor).returns(Status) }
|
|
56
|
+
def execute(executor)
|
|
57
|
+
executor.state.record_lineno(source_line)
|
|
58
|
+
executor.run_simple(self)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
module AST
|
|
6
|
+
# `( list )` — runs the list in a subshell (a forked child) so cd and
|
|
7
|
+
# variable changes do not affect the current shell. Its status is the list's.
|
|
8
|
+
class Subshell < Node
|
|
9
|
+
extend T::Sig
|
|
10
|
+
|
|
11
|
+
attr_reader :body
|
|
12
|
+
|
|
13
|
+
sig { params(body: Node).void }
|
|
14
|
+
def initialize(body)
|
|
15
|
+
@body = body
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
sig { params(executor: Executor).returns(Status) }
|
|
19
|
+
def execute(executor)
|
|
20
|
+
SubshellRunner.new(executor, body).call
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
module AST
|
|
6
|
+
# `until cond; do body; done` — runs the body until the condition succeeds.
|
|
7
|
+
class Until < ConditionLoop
|
|
8
|
+
extend T::Sig
|
|
9
|
+
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
sig { returns(Symbol) }
|
|
13
|
+
def kind
|
|
14
|
+
:until
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|