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,112 @@
|
|
|
1
|
+
# typed: false
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
# The body of the racc-generated Rush::Parser (its `---- inner` block only
|
|
6
|
+
# `include`s this), kept in a normal linted/covered file. Rule actions call
|
|
7
|
+
# these factories; none contain node logic.
|
|
8
|
+
module ParserSupport
|
|
9
|
+
def initialize(lexer)
|
|
10
|
+
@lexer = lexer
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def parse
|
|
14
|
+
do_parse
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def next_token
|
|
18
|
+
@lexer.next_token
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def on_error(token_id, value, _stack)
|
|
22
|
+
raise IncompleteInput, 'unexpected end of input' if value == false
|
|
23
|
+
|
|
24
|
+
near = value.is_a?(AST::Word) ? value.literal_text : value
|
|
25
|
+
raise ParseError, "syntax error at #{@lexer.location}: unexpected #{token_to_str(token_id)} `#{near}`"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def make_list(entries)
|
|
31
|
+
AST::List.new(entries.map { |and_or, sep| AST::ListEntry.new(and_or: and_or, async: sep == '&') })
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def pending_entry(and_or)
|
|
35
|
+
[and_or, ';']
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def terminate_list(entries, sep)
|
|
39
|
+
entries.last[1] = sep
|
|
40
|
+
entries
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def append_and_or(entries, sep, and_or)
|
|
44
|
+
entries.last[1] = sep
|
|
45
|
+
entries << [and_or, ';']
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def make_and_or(left, op, right)
|
|
49
|
+
AST::AndOr.new(left, op, right)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def make_pipeline(commands, negate)
|
|
53
|
+
AST::Pipeline.new(commands, negate)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def make_if(condition, consequent, alternative)
|
|
57
|
+
AST::If.new(condition, consequent, alternative)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def make_brace_group(body)
|
|
61
|
+
AST::BraceGroup.new(body)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def make_subshell(body)
|
|
65
|
+
AST::Subshell.new(body)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def make_redirected(command, redirects)
|
|
69
|
+
AST::Redirected.new(command, redirects)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def make_while(condition, body)
|
|
73
|
+
AST::While.new(condition, body)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def make_until(condition, body)
|
|
77
|
+
AST::Until.new(condition, body)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def make_for(name, words, body)
|
|
81
|
+
AST::For.new(name, words, body)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def make_case(word, items)
|
|
85
|
+
AST::Case.new(word, items)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def make_case_item(patterns, body)
|
|
89
|
+
AST::CaseItem.new(patterns: patterns, body: body)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def make_function(word, body)
|
|
93
|
+
AST::FunctionDef.new(word.literal_text, body)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def make_simple_command(prefix, word, suffix)
|
|
97
|
+
AST::SimpleCommand.new(prefix + [word].compact + suffix)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def make_redirect(kind, target)
|
|
101
|
+
AST::Redirect.new(kind: kind, target: target, io_number: nil)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def make_heredoc(holder)
|
|
105
|
+
AST::Redirect.new(kind: :heredoc, target: holder, io_number: nil)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def with_io_number(redirect, number)
|
|
109
|
+
redirect.with(io_number: number)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'strscan'
|
|
5
|
+
|
|
6
|
+
module Rush
|
|
7
|
+
# Shared left-to-right shell-pattern scanner. Concrete compilers implement
|
|
8
|
+
# the four append_* handlers; this class owns token dispatch and cursor
|
|
9
|
+
# progress so the Ruby fallback and native ERE translator cannot drift.
|
|
10
|
+
class PatternScanner
|
|
11
|
+
extend T::Sig
|
|
12
|
+
|
|
13
|
+
HANDLERS = { '\\' => :append_escape, '*' => :append_wildcard,
|
|
14
|
+
'?' => :append_wildcard, '[' => :append_bracket }.freeze
|
|
15
|
+
|
|
16
|
+
sig { params(pattern: String).void }
|
|
17
|
+
def initialize(pattern)
|
|
18
|
+
@scanner = StringScanner.new(pattern)
|
|
19
|
+
scan
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
sig { void }
|
|
25
|
+
def scan
|
|
26
|
+
step until @scanner.eos?
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
sig { void }
|
|
30
|
+
def step
|
|
31
|
+
handler = HANDLERS.fetch(@scanner.peek(1), :append_literal)
|
|
32
|
+
send(handler)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
# Coalesces caught signals until the evaluator reaches a safe command
|
|
6
|
+
# boundary. POSIX does not require multiple instances of one signal to queue;
|
|
7
|
+
# dash drains distinct pending signals in signal-number order.
|
|
8
|
+
class PendingSignals
|
|
9
|
+
extend T::Sig
|
|
10
|
+
|
|
11
|
+
sig { void }
|
|
12
|
+
def initialize
|
|
13
|
+
@names = T.let(empty, T::Hash[String, T::Boolean])
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Called from Ruby's Signal.trap block: deliberately only one Hash write,
|
|
17
|
+
# with no parser, evaluator, mutex or user shell code in handler context.
|
|
18
|
+
sig { params(name: String).void }
|
|
19
|
+
def record(name)
|
|
20
|
+
@names[name] = true
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
sig { params(blk: T.proc.params(name: String).void).void }
|
|
24
|
+
def drain(&blk)
|
|
25
|
+
take.each(&blk) until @names.empty?
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
sig { returns(T.nilable(String)) }
|
|
29
|
+
def first
|
|
30
|
+
@names.keys.min_by { |name| Signals.number(name) }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
sig { returns(T::Boolean) }
|
|
34
|
+
def any?
|
|
35
|
+
@names.any?
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
sig { void }
|
|
39
|
+
def clear
|
|
40
|
+
@names.clear
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
sig { returns(T::Array[String]) }
|
|
46
|
+
def take
|
|
47
|
+
names = @names
|
|
48
|
+
@names = empty
|
|
49
|
+
names.keys.sort_by { |name| Signals.number(name) }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
sig { returns(T::Hash[String, T::Boolean]) }
|
|
53
|
+
def empty
|
|
54
|
+
{} #: Hash[String, bool]
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
# Runs a multi-stage pipeline: a pipe between each pair of stages, every stage
|
|
6
|
+
# forked (so they run concurrently and never deadlock on a full pipe buffer),
|
|
7
|
+
# the parent's pipe ends closed, then waitpid for all. The pipeline's status is
|
|
8
|
+
# normally the last stage's, or the rightmost non-zero stage's under pipefail.
|
|
9
|
+
# A stage is an arbitrary command — a simple command, but
|
|
10
|
+
# also a group/subshell/if/while/for/case or a function call — so it is run via
|
|
11
|
+
# the executor with the stage's pipe ends bound as the base IoTable. Each stage
|
|
12
|
+
# is a subshell environment (POSIX 2.12): exit/return end only the stage, loop
|
|
13
|
+
# control is inert, traps reset, and a fatal error aborts just that stage —
|
|
14
|
+
# SubshellRunner#run_body is exactly that resolution, so stages run through it.
|
|
15
|
+
# `start_stage` is the one irreducible fork/exit wrapper; the child-side
|
|
16
|
+
# `run_stage` (and its fd setup) is tested directly.
|
|
17
|
+
class PipelineRunner
|
|
18
|
+
extend T::Sig
|
|
19
|
+
|
|
20
|
+
# One stage of the pipeline: its position plus the shared pipe array and the
|
|
21
|
+
# stage count. The fd topology — which pipe end feeds this stage (input) and
|
|
22
|
+
# which it feeds (output), and so which ends to keep open (ends) — derives
|
|
23
|
+
# from all three, so the runner threads a single Stage rather than the
|
|
24
|
+
# (index, pipes) pair through every per-stage step.
|
|
25
|
+
class Stage
|
|
26
|
+
extend T::Sig
|
|
27
|
+
|
|
28
|
+
sig { returns(Integer) }
|
|
29
|
+
attr_reader :index, :count
|
|
30
|
+
|
|
31
|
+
sig { returns(T::Array[[IO, IO]]) }
|
|
32
|
+
attr_reader :pipes
|
|
33
|
+
|
|
34
|
+
sig { params(index: Integer, pipes: T::Array[[IO, IO]], count: Integer).void }
|
|
35
|
+
def initialize(index, pipes, count)
|
|
36
|
+
@index = index
|
|
37
|
+
@pipes = pipes
|
|
38
|
+
@count = count
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
sig { returns(T::Boolean) }
|
|
42
|
+
def last?
|
|
43
|
+
index == count - 1
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
sig { returns(T.nilable(IO)) }
|
|
47
|
+
def input
|
|
48
|
+
index.positive? ? pipes.fetch(index - 1).first : nil
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
sig { returns(T.nilable(IO)) }
|
|
52
|
+
def output
|
|
53
|
+
last? ? nil : pipes.fetch(index).last
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
sig { returns(T::Array[IO]) }
|
|
57
|
+
def ends
|
|
58
|
+
[input, output].compact
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Layer this stage's pipe ends over the base IoTable: stdin from the
|
|
62
|
+
# previous pipe (unless first), stdout to the next pipe (unless last).
|
|
63
|
+
sig { params(base: IoTable).returns(IoTable) }
|
|
64
|
+
def io(base)
|
|
65
|
+
base = base.with(0, input) if input
|
|
66
|
+
output ? base.with(1, output) : base
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
sig { void }
|
|
70
|
+
def close_unused
|
|
71
|
+
keep = ends
|
|
72
|
+
pipes.each { |pipe| close_unkept(pipe, keep) }
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
sig { params(pipe: [IO, IO], keep: T::Array[IO]).void }
|
|
78
|
+
def close_unkept(pipe, keep)
|
|
79
|
+
pipe.each { |io| io.close unless keep.include?(io) }
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
sig { params(executor: Executor, commands: T::Array[AST::Node]).void }
|
|
84
|
+
def initialize(executor, commands)
|
|
85
|
+
@executor = executor
|
|
86
|
+
@commands = commands
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# The whole pipeline is one job: under job control its group (led by the
|
|
90
|
+
# first stage) owns the terminal until every stage has settled, so the
|
|
91
|
+
# reclaim wraps the full wait, not each stage's — and a ^Z parks all of
|
|
92
|
+
# it as one Stopped entry.
|
|
93
|
+
sig { returns(Status) }
|
|
94
|
+
def call
|
|
95
|
+
pipes = build_pipes
|
|
96
|
+
pids = start_stages(pipes)
|
|
97
|
+
close_all(pipes)
|
|
98
|
+
@executor.job_control.foreground(pids, text: text) { wait(pids) }
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
private
|
|
102
|
+
|
|
103
|
+
# The adopted-job text of a ^Z-parked pipeline (nil without job control).
|
|
104
|
+
sig { returns(T.nilable(String)) }
|
|
105
|
+
def text
|
|
106
|
+
CommandText.stages(@commands) if @executor.job_control.monitored?
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
sig { returns(T::Array[[IO, IO]]) }
|
|
110
|
+
def build_pipes
|
|
111
|
+
Array.new(@commands.size - 1) { @executor.system.pipe }
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Stages fork sequentially, each joining the first stage's process group
|
|
115
|
+
# under job control (dash: one group per pipeline, first process is the
|
|
116
|
+
# leader). Because stage 0's group is settled — parent-side setpgid —
|
|
117
|
+
# before stage 1 is even forked, later members can never race the group
|
|
118
|
+
# into existence.
|
|
119
|
+
sig { params(pipes: T::Array[[IO, IO]]).returns(T::Array[Integer]) }
|
|
120
|
+
def start_stages(pipes)
|
|
121
|
+
@commands.each_index.with_object([]) do |index, pids|
|
|
122
|
+
pids << start_stage(Stage.new(index, pipes, @commands.size), pids.first)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
sig { params(stage: Stage, leader: T.nilable(Integer)).returns(Integer) }
|
|
127
|
+
def start_stage(stage, leader)
|
|
128
|
+
@executor.job_control.launch(leader: leader) { @executor.system.exit!(run_stage(stage).exitstatus) }
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Child-side. The relay is armed before the body (and its subshell
|
|
132
|
+
# reset) runs: a stage of a monitor-mode job propagates a member's
|
|
133
|
+
# stop upward instead of blocking on it (rush-l4o).
|
|
134
|
+
sig { params(stage: Stage).returns(Status) }
|
|
135
|
+
def run_stage(stage)
|
|
136
|
+
close_unused(stage)
|
|
137
|
+
@executor.jobs.control.arm_stage_relay
|
|
138
|
+
body = @commands.fetch(stage.index)
|
|
139
|
+
@executor.with_io(stage.io(@executor.io)) { SubshellRunner.new(@executor, body).run_body }
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
sig { params(stage: Stage).void }
|
|
143
|
+
def close_unused(stage)
|
|
144
|
+
stage.close_unused
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
sig { params(pipes: T::Array[[IO, IO]]).void }
|
|
148
|
+
def close_all(pipes)
|
|
149
|
+
pipes.each { |pipe| pipe.each(&:close) }
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
sig { params(pids: T::Array[Integer]).returns(Status) }
|
|
153
|
+
def wait(pids)
|
|
154
|
+
statuses = PipelineStatuses.new(pids.map { |pid| wait_status(pid) })
|
|
155
|
+
pipefail? ? statuses.pipefail_verdict : statuses.verdict
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# A directly-killed stage reports on the shell's stderr, like dash's
|
|
159
|
+
# per-stage printsignal (probed: two killed stages print two lines, in
|
|
160
|
+
# stage order, unmoved by the stages' own redirects). The common case —
|
|
161
|
+
# an external dying inside the stage — reports from the stage
|
|
162
|
+
# supervisor's own External instead.
|
|
163
|
+
sig { params(pid: Integer).returns(Status) }
|
|
164
|
+
def wait_status(pid)
|
|
165
|
+
SignalReport.report(@executor.jobs.await(pid), @executor.io.get(2))
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
sig { returns(T::Boolean) }
|
|
169
|
+
def pipefail?
|
|
170
|
+
@executor.state.options.on?(:pipefail)
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
# Chooses the observable status for a settled multi-stage pipeline: the
|
|
6
|
+
# last stage's (or, under pipefail, the rightmost failure), with any
|
|
7
|
+
# stage's stop signal riding along — a ^Z parks the whole job even when
|
|
8
|
+
# the final stage had already exited (Status#with_stop, dash-probed).
|
|
9
|
+
class PipelineStatuses
|
|
10
|
+
extend T::Sig
|
|
11
|
+
|
|
12
|
+
sig { params(entries: T::Array[Status]).void }
|
|
13
|
+
def initialize(entries)
|
|
14
|
+
@entries = entries
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
sig { returns(Status) }
|
|
18
|
+
def verdict
|
|
19
|
+
last_stage.with_stop(job_stopsig)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
sig { returns(Status) }
|
|
23
|
+
def pipefail_verdict
|
|
24
|
+
pipefail.with_stop(job_stopsig)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
sig { returns(Status) }
|
|
28
|
+
def last_stage
|
|
29
|
+
@entries.fetch(-1)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
sig { returns(Status) }
|
|
33
|
+
def pipefail
|
|
34
|
+
@entries.reverse_each { |status| return status unless status.success? }
|
|
35
|
+
Status.success
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
sig { returns(T.nilable(Integer)) }
|
|
41
|
+
def job_stopsig
|
|
42
|
+
@entries.filter_map(&:stopsig).first
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'forwardable'
|
|
5
|
+
|
|
6
|
+
module Rush
|
|
7
|
+
# The positional parameters $1..$n: a list of strings that `set` replaces,
|
|
8
|
+
# `shift` drops the front of, and a function call brackets with #with so the
|
|
9
|
+
# caller's are restored on return. Reads ($1, $#, $@, the for-loop word list)
|
|
10
|
+
# delegate to the underlying array.
|
|
11
|
+
class Positional
|
|
12
|
+
extend T::Sig
|
|
13
|
+
extend Forwardable
|
|
14
|
+
|
|
15
|
+
def_delegators :@values, :==, :[], :size, :empty?, :join, :map, :each, :to_a
|
|
16
|
+
|
|
17
|
+
sig { params(values: T::Array[String]).void }
|
|
18
|
+
def initialize(values = [])
|
|
19
|
+
@values = values
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
sig { params(values: T::Array[String]).void }
|
|
23
|
+
def replace(values)
|
|
24
|
+
@values = values
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
sig { params(count: Integer).void }
|
|
28
|
+
def shift(count)
|
|
29
|
+
@values = @values.drop(count)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
sig do
|
|
33
|
+
type_parameters(:U)
|
|
34
|
+
.params(values: T::Array[String], blk: T.proc.returns(T.type_parameter(:U)))
|
|
35
|
+
.returns(T.type_parameter(:U))
|
|
36
|
+
end
|
|
37
|
+
def with(values, &blk)
|
|
38
|
+
saved = T.let(@values, T::Array[String])
|
|
39
|
+
@values = values
|
|
40
|
+
yield
|
|
41
|
+
ensure
|
|
42
|
+
@values = T.must(saved)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
# Translates one shell pattern into a POSIX extended regular expression. The
|
|
6
|
+
# libc regex engine then supplies LC_COLLATE equivalence classes, collating
|
|
7
|
+
# symbols and locale ranges that Ruby's regexp/fnmatch APIs do not expose.
|
|
8
|
+
# :reek:InstanceVariableAssumption -- @scanner is initialized by PatternScanner#initialize
|
|
9
|
+
class PosixPattern < PatternScanner
|
|
10
|
+
extend T::Sig
|
|
11
|
+
|
|
12
|
+
ERE_SPECIAL = /[.\^$+(){}|\[\]*?\\]/
|
|
13
|
+
RUBY_GLOB_SPECIAL = %w[{ }].freeze
|
|
14
|
+
|
|
15
|
+
sig { returns(String) }
|
|
16
|
+
attr_reader :source, :glob_source
|
|
17
|
+
|
|
18
|
+
sig { params(pattern: String).void }
|
|
19
|
+
def initialize(pattern)
|
|
20
|
+
@source = +'^'
|
|
21
|
+
@glob_source = +''
|
|
22
|
+
super
|
|
23
|
+
@source << '$'
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
sig { void }
|
|
29
|
+
def append_escape
|
|
30
|
+
@scanner.getch
|
|
31
|
+
escaped = @scanner.getch || '\\'
|
|
32
|
+
@glob_source << '\\' << escaped
|
|
33
|
+
append_ere_literal(escaped)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
sig { void }
|
|
37
|
+
def append_wildcard
|
|
38
|
+
char = @scanner.getch.to_s
|
|
39
|
+
append_discovery(char)
|
|
40
|
+
@source << (char == '*' ? '.*' : '.')
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
sig { void }
|
|
44
|
+
def append_bracket
|
|
45
|
+
bracket = BracketExpression.parse(@scanner.string, @scanner.pos)
|
|
46
|
+
return append_literal unless bracket
|
|
47
|
+
|
|
48
|
+
@source << bracket_source(bracket)
|
|
49
|
+
append_discovery('*')
|
|
50
|
+
@scanner.pos = bracket.finish
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
sig { params(bracket: BracketExpression).returns(String) }
|
|
54
|
+
def bracket_source(bracket)
|
|
55
|
+
source = bracket.source.sub(/\A\[!/, '[^')
|
|
56
|
+
source.include?('\\') ? EscapedBracket.new(source).source : source
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
sig { void }
|
|
60
|
+
def append_literal
|
|
61
|
+
char = @scanner.getch.to_s
|
|
62
|
+
append_discovery(char)
|
|
63
|
+
append_ere_literal(char)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
sig { params(char: String).void }
|
|
67
|
+
def append_discovery(char)
|
|
68
|
+
return if char == '*' && @glob_source.end_with?('*')
|
|
69
|
+
|
|
70
|
+
@glob_source << (RUBY_GLOB_SPECIAL.include?(char) ? "\\#{char}" : char)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
sig { params(char: String).void }
|
|
74
|
+
def append_ere_literal(char)
|
|
75
|
+
@source << (char.match?(ERE_SPECIAL) ? "\\#{char}" : char)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
# The program text and shell parameters implied by
|
|
6
|
+
# `-c command_string [command_name [argument...]]`.
|
|
7
|
+
class CommandInput
|
|
8
|
+
extend T::Sig
|
|
9
|
+
|
|
10
|
+
sig { params(operands: T::Array[String]).void }
|
|
11
|
+
def initialize(operands)
|
|
12
|
+
@operands = operands
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
sig { returns(String) }
|
|
16
|
+
def name
|
|
17
|
+
@operands.fetch(1, 'rush')
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
sig { returns(T::Array[String]) }
|
|
21
|
+
def positionals
|
|
22
|
+
@operands.drop(2)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
sig { returns(String) }
|
|
26
|
+
def source
|
|
27
|
+
@operands.fetch(0) { raise InvocationError, '-c requires an argument' }
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# The program text and shell parameters implied by a script-file operand:
|
|
32
|
+
# the path becomes $0, later operands the positional parameters, and the
|
|
33
|
+
# file is read lazily — unreadable files are an invocation error (status 2,
|
|
34
|
+
# like dash).
|
|
35
|
+
class ScriptInput
|
|
36
|
+
extend T::Sig
|
|
37
|
+
|
|
38
|
+
sig { params(operands: T::Array[String], system: SystemCalls).void }
|
|
39
|
+
def initialize(operands, system)
|
|
40
|
+
@operands = operands
|
|
41
|
+
@system = system
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
sig { returns(String) }
|
|
45
|
+
def name
|
|
46
|
+
@operands.fetch(0)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
sig { returns(T::Array[String]) }
|
|
50
|
+
def positionals
|
|
51
|
+
@operands.drop(1)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
sig { returns(String) }
|
|
55
|
+
def source
|
|
56
|
+
@system.read_file(name)
|
|
57
|
+
rescue SystemCallError => e
|
|
58
|
+
raise InvocationError, "cannot open #{name}: #{e.message}"
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# The program text and shell parameters implied by reading stdin (implicitly
|
|
63
|
+
# or via -s): the operands become the positional parameters and $0 stays the
|
|
64
|
+
# shell name.
|
|
65
|
+
class StdinInput
|
|
66
|
+
extend T::Sig
|
|
67
|
+
|
|
68
|
+
sig { params(operands: T::Array[String], system: SystemCalls).void }
|
|
69
|
+
def initialize(operands, system)
|
|
70
|
+
@operands = operands
|
|
71
|
+
@system = system
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
sig { returns(String) }
|
|
75
|
+
def name
|
|
76
|
+
'rush'
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
sig { returns(T::Array[String]) }
|
|
80
|
+
def positionals
|
|
81
|
+
@operands
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
sig { returns(String) }
|
|
85
|
+
def source
|
|
86
|
+
@system.stdin.read
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
# Reads a shell source one line at a time, accumulating until the buffer parses
|
|
6
|
+
# to a complete program. POSIX reads and executes command by command, so an
|
|
7
|
+
# earlier command (a function definition, a future `alias`) can shape later
|
|
8
|
+
# input and a syntax error only stops the commands that follow it — the ones
|
|
9
|
+
# before it have already run. IncompleteInput means "the construct is
|
|
10
|
+
# unfinished": pull another line (multi-line here-documents included). At end of
|
|
11
|
+
# input a pending buffer gets one final, non-interactive parse so an
|
|
12
|
+
# unterminated here-document still runs with the body so far, exactly like dash.
|
|
13
|
+
class ProgramReader
|
|
14
|
+
extend T::Sig
|
|
15
|
+
|
|
16
|
+
sig do
|
|
17
|
+
params(aliases: AliasTable,
|
|
18
|
+
next_line: T.proc.params(buffered: T::Boolean).returns(T.nilable(String))).void
|
|
19
|
+
end
|
|
20
|
+
def initialize(aliases: AliasTable.new, &next_line)
|
|
21
|
+
@next_line = next_line
|
|
22
|
+
@aliases = aliases
|
|
23
|
+
@buffer = +''
|
|
24
|
+
@lines = SourceLineCounter.new
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# The next complete program, or :eof. Raises ParseError on a real syntax
|
|
28
|
+
# error, so the caller decides policy: batch aborts, the REPL reports and
|
|
29
|
+
# resumes.
|
|
30
|
+
sig { returns(T.any(AST::List, Symbol)) }
|
|
31
|
+
def next_program
|
|
32
|
+
@buffer = +''
|
|
33
|
+
loop do
|
|
34
|
+
outcome = read_more
|
|
35
|
+
return outcome unless outcome == :more
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
sig { returns(T.any(AST::List, Symbol)) }
|
|
42
|
+
def read_more
|
|
43
|
+
line = @next_line.call(!@buffer.empty?)
|
|
44
|
+
return finish unless line
|
|
45
|
+
|
|
46
|
+
record_line(line)
|
|
47
|
+
attempt(@buffer << line)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
sig { params(line: String).void }
|
|
51
|
+
def record_line(line)
|
|
52
|
+
@buffer.empty? ? @lines.start(line) : @lines.continue(line)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
sig { params(source: String).returns(T.any(AST::List, Symbol)) }
|
|
56
|
+
def attempt(source)
|
|
57
|
+
Parser.new(Lexer.new(source, interactive: true, aliases: @aliases, line_offset: @lines.offset)).parse
|
|
58
|
+
rescue IncompleteInput
|
|
59
|
+
:more
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
sig { returns(T.any(AST::List, Symbol)) }
|
|
63
|
+
def finish
|
|
64
|
+
return :eof if @buffer.empty?
|
|
65
|
+
|
|
66
|
+
Parser.new(Lexer.new(@buffer, interactive: false, aliases: @aliases, line_offset: @lines.offset)).parse
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|