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,134 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'strscan'
|
|
5
|
+
|
|
6
|
+
module Rush
|
|
7
|
+
class Lexer
|
|
8
|
+
# Scans one word from the shared StringScanner into an AST::Word of typed
|
|
9
|
+
# segments: literal runs (quote-removed, with a `quoted` flag) and :param
|
|
10
|
+
# segments for $name / ${...}. Handles the three quoting forms; command
|
|
11
|
+
# substitution and arithmetic arrive in later slices.
|
|
12
|
+
class WordScanner
|
|
13
|
+
extend T::Sig
|
|
14
|
+
include ScannerPredicates
|
|
15
|
+
|
|
16
|
+
TERMINATOR = /[ \t\n;&|<>()]/
|
|
17
|
+
DISPATCH = {
|
|
18
|
+
"'" => :single_quote, '"' => :double_quote, '\\' => :escape,
|
|
19
|
+
'$' => :dollar, '`' => :backtick
|
|
20
|
+
}.freeze
|
|
21
|
+
|
|
22
|
+
# Scan the next shell word from a live lexer scanner, stopping at the first
|
|
23
|
+
# unquoted terminator (blank / operator). `interactive` mirrors the lexer:
|
|
24
|
+
# an accumulating parse may ask for more input, the final one may not.
|
|
25
|
+
sig { params(scanner: StringScanner, interactive: T::Boolean).returns(AST::Word) }
|
|
26
|
+
def self.next_word(scanner, interactive:)
|
|
27
|
+
new(scanner, terminator: TERMINATOR, interactive: interactive).scan
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Scan a complete, already-delimited string (a ${...} operator word or
|
|
31
|
+
# arithmetic source) as one word: no terminators apply (blanks/operators
|
|
32
|
+
# are literal), only quote / $ / ` stay special.
|
|
33
|
+
sig { params(text: String).returns(AST::Word) }
|
|
34
|
+
def self.entire(text)
|
|
35
|
+
new(StringScanner.new(text), terminator: nil, interactive: false).scan
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# terminator: the character class that ends a word, or nil in whole mode.
|
|
39
|
+
sig { params(scanner: StringScanner, terminator: T.nilable(Regexp), interactive: T::Boolean).void }
|
|
40
|
+
def initialize(scanner, terminator:, interactive:)
|
|
41
|
+
@scanner = scanner
|
|
42
|
+
@terminator = terminator
|
|
43
|
+
@interactive = interactive
|
|
44
|
+
@buffer = SegmentBuffer.new
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
sig { returns(AST::Word) }
|
|
48
|
+
def scan
|
|
49
|
+
loop do
|
|
50
|
+
char = @scanner.getch or return @buffer.word
|
|
51
|
+
return rewind_then_build if terminates?(char)
|
|
52
|
+
|
|
53
|
+
step(char)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
sig { params(char: String).returns(T::Boolean) }
|
|
60
|
+
def terminates?(char)
|
|
61
|
+
terminator = @terminator
|
|
62
|
+
terminator ? char.match?(terminator) : false
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
sig { returns(AST::Word) }
|
|
66
|
+
def rewind_then_build
|
|
67
|
+
@scanner.unscan
|
|
68
|
+
@buffer.word
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
sig { params(char: String).void }
|
|
72
|
+
def step(char)
|
|
73
|
+
handler = DISPATCH.fetch(char, nil)
|
|
74
|
+
handler ? send(handler) : (@buffer << char)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
sig { void }
|
|
78
|
+
def single_quote
|
|
79
|
+
content = @scanner.scan(/[^']*/)
|
|
80
|
+
raise IncompleteInput, 'unterminated single quote' unless @scanner.scan("'")
|
|
81
|
+
|
|
82
|
+
push(content, quoted: true)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
sig { void }
|
|
86
|
+
def double_quote
|
|
87
|
+
DoubleQuoteScanner.new(@scanner, @buffer, -> { continuation }).scan
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Line continuation (POSIX 2.2.1): the backslash-newline pair vanishes.
|
|
91
|
+
# When the newline ends an accumulating (interactive) buffer, the logical
|
|
92
|
+
# line continues on input the lexer does not have yet, so ask the reader
|
|
93
|
+
# for the next line. The final parse at end of input — and `entire` mode,
|
|
94
|
+
# whose text is already complete — just drops the pair, like dash.
|
|
95
|
+
sig { void }
|
|
96
|
+
def continuation
|
|
97
|
+
@scanner.skip(/\n/)
|
|
98
|
+
raise IncompleteInput, 'line continuation' if @interactive && @terminator && @scanner.eos?
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# A lone `$` that begins no valid reference stays a literal dollar: merged
|
|
102
|
+
# into the current literal run when bare, a quoted literal segment in "...".
|
|
103
|
+
sig { void }
|
|
104
|
+
def dollar
|
|
105
|
+
@scanner.unscan
|
|
106
|
+
segment = DollarScanner.new(@scanner).read(quoted: false)
|
|
107
|
+
segment ? @buffer.push(segment) : (@buffer << '$')
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
sig { void }
|
|
111
|
+
def backtick
|
|
112
|
+
@scanner.unscan
|
|
113
|
+
@buffer.push(DollarScanner.new(@scanner).read_backtick(quoted: false))
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
sig { void }
|
|
117
|
+
def escape
|
|
118
|
+
char = @scanner.getch or return trailing_backslash
|
|
119
|
+
char == "\n" ? continuation : push(char, quoted: true)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# A backslash that ends the input stays literal, like dash.
|
|
123
|
+
sig { void }
|
|
124
|
+
def trailing_backslash
|
|
125
|
+
@buffer << '\\'
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
sig { params(value: T.untyped, quoted: T::Boolean).void }
|
|
129
|
+
def push(value, quoted:)
|
|
130
|
+
@buffer.push(AST::LiteralSegment.new(value, quoted))
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
data/lib/rush/lexer.rb
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'strscan'
|
|
5
|
+
require_relative 'lexer/operator_table'
|
|
6
|
+
require_relative 'lexer/lex_state'
|
|
7
|
+
require_relative 'lexer/source_lines'
|
|
8
|
+
require_relative 'lexer/scanner_predicates'
|
|
9
|
+
require_relative 'lexer/token_predicates'
|
|
10
|
+
require_relative 'lexer/quote_skips'
|
|
11
|
+
require_relative 'lexer/case_frame'
|
|
12
|
+
require_relative 'lexer/case_tracker'
|
|
13
|
+
require_relative 'lexer/paren_regions'
|
|
14
|
+
require_relative 'lexer/paren_reader'
|
|
15
|
+
require_relative 'lexer/substitution_reader'
|
|
16
|
+
require_relative 'lexer/braced_reader'
|
|
17
|
+
require_relative 'lexer/param_scanner'
|
|
18
|
+
require_relative 'lexer/quoted_word'
|
|
19
|
+
require_relative 'lexer/dollar_scanner'
|
|
20
|
+
require_relative 'lexer/double_quote_scanner'
|
|
21
|
+
require_relative 'lexer/word_scanner'
|
|
22
|
+
require_relative 'lexer/heredoc_body'
|
|
23
|
+
require_relative 'lexer/heredoc_reader'
|
|
24
|
+
require_relative 'lexer/token_classifier'
|
|
25
|
+
require_relative 'lexer/alias_expander'
|
|
26
|
+
|
|
27
|
+
module Rush
|
|
28
|
+
# StringScanner pump that yields [symbol, value] pairs for racc. It skips
|
|
29
|
+
# blanks and comments, then emits NEWLINE, an IO_NUMBER, an operator, or a
|
|
30
|
+
# WORD/ASSIGNMENT_WORD (classified against LexState, which advances after each
|
|
31
|
+
# token to track command position — the seed of POSIX Grammar Rules 1-9).
|
|
32
|
+
class Lexer
|
|
33
|
+
extend T::Sig
|
|
34
|
+
include TokenPredicates
|
|
35
|
+
|
|
36
|
+
# Between tokens, blanks, comments and backslash-newline continuations
|
|
37
|
+
# (POSIX 2.2.1) are all insignificant. A continuation at the buffer's very
|
|
38
|
+
# end is left for the word scanner, which asks for the next line instead
|
|
39
|
+
# of ending the token stream.
|
|
40
|
+
INSIGNIFICANT = /(?:[ \t]+|#[^\n]*|\\\n(?=.))+/m
|
|
41
|
+
# "Immediately before < or >" sees through spliced continuations, like the
|
|
42
|
+
# operator matcher: dash-verified, `2\<newline>>f` redirects fd 2.
|
|
43
|
+
IO_NUMBER = /\d+(?=(?:\\\n)*[<>])/
|
|
44
|
+
HEREDOC_OPS = { DLESS: :plain, DLESSDASH: :strip }.freeze
|
|
45
|
+
|
|
46
|
+
sig { params(source: String, interactive: T::Boolean, aliases: T.nilable(AliasTable), line_offset: Integer).void }
|
|
47
|
+
def initialize(source, interactive: false, aliases: nil, line_offset: 0)
|
|
48
|
+
@scanner = StringScanner.new(source)
|
|
49
|
+
@aliases = AliasExpander.new(aliases)
|
|
50
|
+
@interactive = interactive
|
|
51
|
+
@state = LexState.new
|
|
52
|
+
@lines = SourceLines.new(@scanner, line_offset)
|
|
53
|
+
@awaiting = nil
|
|
54
|
+
@heredocs = []
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
sig { returns(Integer) }
|
|
58
|
+
def location
|
|
59
|
+
@scanner.charpos
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
sig { returns([T.untyped, T.untyped]) }
|
|
63
|
+
def next_token
|
|
64
|
+
drain
|
|
65
|
+
return [false, false] if @scanner.eos?
|
|
66
|
+
|
|
67
|
+
token = scan_token
|
|
68
|
+
token ? emit(token) : next_token
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
sig { params(token: [T.untyped, T.untyped]).returns([T.untyped, T.untyped]) }
|
|
74
|
+
def emit(token)
|
|
75
|
+
@state.advance(token.first)
|
|
76
|
+
@aliases.spend
|
|
77
|
+
token
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Skip blanks and comments; when the current frame is an exhausted alias
|
|
81
|
+
# replacement, restore the input beneath it and keep skipping.
|
|
82
|
+
sig { void }
|
|
83
|
+
def drain
|
|
84
|
+
skip_insignificant
|
|
85
|
+
return unless @scanner.eos? && @aliases.nested?
|
|
86
|
+
|
|
87
|
+
@scanner = @aliases.pop
|
|
88
|
+
drain
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
sig { void }
|
|
92
|
+
def skip_insignificant
|
|
93
|
+
@scanner.skip(INSIGNIFICANT)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
sig { returns(T.nilable([T.untyped, T.untyped])) }
|
|
97
|
+
def scan_token
|
|
98
|
+
return heredoc_newline if @scanner.scan("\n")
|
|
99
|
+
|
|
100
|
+
io_number || operator || word
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
sig { returns(T.nilable([T.untyped, T.untyped])) }
|
|
104
|
+
def io_number
|
|
105
|
+
digits = @scanner.scan(IO_NUMBER)
|
|
106
|
+
digits ? [:IO_NUMBER, Integer(digits, 10)] : nil
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
sig { returns(T.nilable([T.untyped, T.untyped])) }
|
|
110
|
+
def operator
|
|
111
|
+
matched = @scanner.scan(OperatorTable::PATTERN)
|
|
112
|
+
matched ? operator_token(matched) : nil
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# The match may carry line continuations between its characters (see
|
|
116
|
+
# OperatorTable::PATTERN); splicing them out recovers the operator.
|
|
117
|
+
sig { params(matched: String).returns([T.untyped, T.untyped]) }
|
|
118
|
+
def operator_token(matched)
|
|
119
|
+
operator = matched.gsub(OperatorTable::CONTINUATION, '')
|
|
120
|
+
symbol = OperatorTable::OPERATORS.fetch(operator)
|
|
121
|
+
@awaiting = HEREDOC_OPS.fetch(symbol, nil)
|
|
122
|
+
[symbol, operator]
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# A word awaited as a here-document delimiter is taken raw — never
|
|
126
|
+
# classified, aliased or reserved. Otherwise classify it, then (only a
|
|
127
|
+
# plain WORD) splice its alias replacement in its place; a splice returns
|
|
128
|
+
# nil so next_token re-reads from the new frame.
|
|
129
|
+
sig { returns(T.nilable([T.untyped, T.untyped])) }
|
|
130
|
+
def word
|
|
131
|
+
scanned = @lines.word { WordScanner.next_word(@scanner, interactive: @interactive) }
|
|
132
|
+
return delimiter(scanned) if @awaiting
|
|
133
|
+
|
|
134
|
+
token = TokenClassifier.new(scanned, @state).call
|
|
135
|
+
replacement = alias_for(token, scanned)
|
|
136
|
+
replacement ? splice(replacement) : token
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
sig { params(token: [T.untyped, T.untyped], word: AST::Word).returns(T.nilable(AliasExpander::Replacement)) }
|
|
140
|
+
def alias_for(token, word)
|
|
141
|
+
return if !word_token?(token) || !@state.command_mode?
|
|
142
|
+
|
|
143
|
+
@aliases.expand(word, @state.expects_command?)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
sig { params(replacement: AliasExpander::Replacement).returns(NilClass) }
|
|
147
|
+
def splice(replacement)
|
|
148
|
+
@aliases.push(replacement, @scanner)
|
|
149
|
+
@scanner = StringScanner.new(replacement.value)
|
|
150
|
+
nil
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
sig { params(word: AST::Word).returns([T.untyped, T.untyped]) }
|
|
154
|
+
def delimiter(word)
|
|
155
|
+
holder = HereDoc.new(delimiter: word.segments.map(&:value).join,
|
|
156
|
+
quoted: word.segments.any?(&:quoted), strip: @awaiting == :strip,
|
|
157
|
+
source_line: word.source_line)
|
|
158
|
+
@awaiting = nil
|
|
159
|
+
@heredocs << holder
|
|
160
|
+
[:WORD, holder]
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# On the newline that ends the command line, drain the pending here-docs:
|
|
164
|
+
# read each body from the lines that follow, in the order the `<<`s appeared.
|
|
165
|
+
sig { returns([T.untyped, T.untyped]) }
|
|
166
|
+
def heredoc_newline
|
|
167
|
+
start = @scanner.pos
|
|
168
|
+
HeredocReader.new(@scanner, interactive: @interactive).fill(@heredocs)
|
|
169
|
+
@heredocs = []
|
|
170
|
+
@lines.heredoc_newline(start)
|
|
171
|
+
[:NEWLINE, "\n"]
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
# Shared break/continue handling for the loop runners. A multi-level signal is
|
|
6
|
+
# re-raised with one fewer level so the next enclosing loop handles it; a
|
|
7
|
+
# single-level signal stops here and yields the last command status. Hosts must
|
|
8
|
+
# expose @executor.
|
|
9
|
+
module LoopControlHandling
|
|
10
|
+
extend T::Sig
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
sig { params(signal: LoopControl).returns(Status) }
|
|
15
|
+
def unwind(signal)
|
|
16
|
+
relayed = relay(signal)
|
|
17
|
+
# Kernel.raise (not bare raise): in this mixin Sorbet can't resolve a bare
|
|
18
|
+
# raise on the module's self.
|
|
19
|
+
Kernel.raise relayed if relayed
|
|
20
|
+
|
|
21
|
+
@executor.state.last_status
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
sig { params(signal: LoopControl).returns(T.nilable(LoopControl)) }
|
|
25
|
+
def relay(signal)
|
|
26
|
+
signal.class.new(signal.count - 1) if signal.count > 1
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
# Lexical loop nesting for break/continue: the count of for/while/until loops
|
|
6
|
+
# enclosing the current command in the same execution environment. A function
|
|
7
|
+
# call or subshell starts fresh (#without), so a break inside a function cannot
|
|
8
|
+
# reach the caller's loop, while dot/eval/group bodies run inline and keep the
|
|
9
|
+
# count. break/continue read #depth to clamp their level and #any? to no-op
|
|
10
|
+
# when there is no enclosing loop.
|
|
11
|
+
class LoopNesting
|
|
12
|
+
extend T::Sig
|
|
13
|
+
|
|
14
|
+
sig { returns(Integer) }
|
|
15
|
+
attr_reader :depth
|
|
16
|
+
|
|
17
|
+
sig { void }
|
|
18
|
+
def initialize
|
|
19
|
+
@depth = 0
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
sig { void }
|
|
23
|
+
def enter
|
|
24
|
+
@depth += 1
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
sig { void }
|
|
28
|
+
def leave
|
|
29
|
+
@depth -= 1
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
sig { returns(T::Boolean) }
|
|
33
|
+
def any?
|
|
34
|
+
@depth.positive?
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
sig do
|
|
38
|
+
type_parameters(:U)
|
|
39
|
+
.params(blk: T.proc.returns(T.type_parameter(:U)))
|
|
40
|
+
.returns(T.type_parameter(:U))
|
|
41
|
+
end
|
|
42
|
+
def without(&blk)
|
|
43
|
+
saved = T.let(@depth, Integer)
|
|
44
|
+
@depth = 0
|
|
45
|
+
yield
|
|
46
|
+
ensure
|
|
47
|
+
@depth = T.must(saved)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
# Runs a while/until loop, catching break/continue at the loop boundary (see
|
|
6
|
+
# LoopControlHandling). The loop's status is the last body status (0 if the
|
|
7
|
+
# body never runs).
|
|
8
|
+
class LoopRunner
|
|
9
|
+
extend T::Sig
|
|
10
|
+
|
|
11
|
+
include LoopControlHandling
|
|
12
|
+
|
|
13
|
+
sig { params(executor: Executor, condition: AST::Node, body: AST::Node, sense: Symbol).void }
|
|
14
|
+
def initialize(executor, condition, body, sense)
|
|
15
|
+
@executor = executor
|
|
16
|
+
@condition = condition
|
|
17
|
+
@body = body
|
|
18
|
+
@sense = sense
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Bracket the loop so break/continue see the right nesting depth; leave runs
|
|
22
|
+
# even when break unwinds.
|
|
23
|
+
sig { returns(Status) }
|
|
24
|
+
def call
|
|
25
|
+
@executor.state.with_loop { run_loop }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
sig { returns(Status) }
|
|
31
|
+
def run_loop
|
|
32
|
+
# T.let pins the loop variable's type: Status.success is now sig'd (Status),
|
|
33
|
+
# but #iterate is unsig'd (untyped), and Sorbet forbids a variable changing
|
|
34
|
+
# type across a loop (srb.help/7001). Steep ignores T.let (see sorbet_dsl.rbs).
|
|
35
|
+
status = T.let(Status.success, Status)
|
|
36
|
+
status = iterate while proceed?
|
|
37
|
+
status
|
|
38
|
+
rescue BreakSignal => e
|
|
39
|
+
unwind(e)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
sig { returns(T::Boolean) }
|
|
43
|
+
def proceed?
|
|
44
|
+
met = @executor.succeeds?(@condition)
|
|
45
|
+
@sense == :while ? met : !met
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
sig { returns(Status) }
|
|
49
|
+
def iterate
|
|
50
|
+
@executor.run(@body)
|
|
51
|
+
rescue ContinueSignal => e
|
|
52
|
+
unwind(e)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
# A `-abc` / `+abc` cluster of single-letter options, the shape POSIX shares
|
|
6
|
+
# between the set builtin and the sh invocation line (XCU set / sh): the
|
|
7
|
+
# leading sign and the letters after it. parse answers nil for anything
|
|
8
|
+
# else — `--`, a lone dash or plus, or a plain operand.
|
|
9
|
+
class OptionCluster
|
|
10
|
+
extend T::Sig
|
|
11
|
+
|
|
12
|
+
sig { returns(String) }
|
|
13
|
+
attr_reader :sign
|
|
14
|
+
|
|
15
|
+
sig { returns(T::Array[String]) }
|
|
16
|
+
attr_reader :letters
|
|
17
|
+
|
|
18
|
+
sig { params(arg: T.nilable(String)).returns(T.nilable(OptionCluster)) }
|
|
19
|
+
def self.parse(arg)
|
|
20
|
+
return unless arg.is_a?(String) && arg.length > 1 && arg != '--' && arg.start_with?('-', '+')
|
|
21
|
+
|
|
22
|
+
new(arg)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
sig { params(flag: String).void }
|
|
26
|
+
def initialize(flag)
|
|
27
|
+
sign, *letters = flag.chars
|
|
28
|
+
@sign = T.must(sign)
|
|
29
|
+
@letters = T.let(letters, T::Array[String])
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Yield each option letter with the cluster's sign, the shape both
|
|
33
|
+
# consumers toggle by.
|
|
34
|
+
sig { params(blk: T.proc.params(letter: String, sign: String).void).void }
|
|
35
|
+
def each_letter(&blk)
|
|
36
|
+
letters.each { |letter| yield(letter, sign) }
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
data/lib/rush/options.rb
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
# The shell options toggled by `set -[+]o name` / `set -[+]x` and friends
|
|
6
|
+
# (:errexit, :nounset, :noexec, :xtrace, :noglob, :verbose, :pipefail), plus the
|
|
7
|
+
# invocation-only :interactive (-i) and :stdin (-s) flags: the set of those
|
|
8
|
+
# currently on, with #set to flip one, #on? to query it, #letters to
|
|
9
|
+
# render $-, and the bare `set -o`/`set +o` listings. The letter/name
|
|
10
|
+
# tables live here so the set builtin and Invocation share one vocabulary.
|
|
11
|
+
class Options
|
|
12
|
+
extend T::Sig
|
|
13
|
+
|
|
14
|
+
LETTERS = { 'a' => :allexport, 'C' => :noclobber, 'e' => :errexit, 'u' => :nounset,
|
|
15
|
+
'x' => :xtrace, 'f' => :noglob, 'v' => :verbose, 'm' => :monitor,
|
|
16
|
+
'n' => :noexec }.freeze
|
|
17
|
+
# Every long name in dash's listing order — the bare `set -o`/`set +o`
|
|
18
|
+
# output shows the invocation-only flags too, like dash does.
|
|
19
|
+
NAMES = { 'errexit' => :errexit, 'noglob' => :noglob, 'interactive' => :interactive,
|
|
20
|
+
'monitor' => :monitor, 'noexec' => :noexec, 'stdin' => :stdin, 'xtrace' => :xtrace,
|
|
21
|
+
'verbose' => :verbose, 'noclobber' => :noclobber, 'allexport' => :allexport,
|
|
22
|
+
'nounset' => :nounset, 'pipefail' => :pipefail }.freeze
|
|
23
|
+
# The names `set -o name` accepts: everything listable minus the flags
|
|
24
|
+
# only invocation can set.
|
|
25
|
+
LONG = NAMES.except('interactive', 'stdin').freeze
|
|
26
|
+
# $- renders enabled flags in dash's order (its option list, reversed), so
|
|
27
|
+
# differential corpus lines printing $- compare equal against the oracle.
|
|
28
|
+
DASH_ORDER = T.let([[:nounset, 'u'], [:allexport, 'a'], [:noclobber, 'C'], [:verbose, 'v'],
|
|
29
|
+
[:xtrace, 'x'], [:stdin, 's'], [:noexec, 'n'], [:monitor, 'm'], [:interactive, 'i'],
|
|
30
|
+
[:noglob, 'f'], [:errexit, 'e']].freeze, T::Array[[Symbol, String]])
|
|
31
|
+
|
|
32
|
+
sig { void }
|
|
33
|
+
def initialize
|
|
34
|
+
@enabled = Set.new
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
sig { params(name: Symbol, enabled: T::Boolean).void }
|
|
38
|
+
def set(name, enabled)
|
|
39
|
+
enabled ? @enabled.add(name) : @enabled.delete(name)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
sig { params(name: Symbol).returns(T::Boolean) }
|
|
43
|
+
def on?(name)
|
|
44
|
+
@enabled.include?(name)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# The current option flags as the $- special parameter renders them.
|
|
48
|
+
sig { returns(String) }
|
|
49
|
+
def letters
|
|
50
|
+
DASH_ORDER.filter_map { |option, letter| letter if on?(option) }.join
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# The bare `set -o` table rows in dash's format: the name padded to 16
|
|
54
|
+
# columns, then on/off.
|
|
55
|
+
sig { returns(T::Array[String]) }
|
|
56
|
+
def settings_rows
|
|
57
|
+
render_settings('%<name>-16son', '%<name>-16soff')
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# The bare `set +o` output: `set -o name` / `set +o name` lines that
|
|
61
|
+
# reproduce the current settings when re-input (POSIX XCU set).
|
|
62
|
+
sig { returns(T::Array[String]) }
|
|
63
|
+
def reinput_lines
|
|
64
|
+
render_settings('set -o %<name>s', 'set +o %<name>s')
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
# One rendered line per long option name: the on form when the option is
|
|
70
|
+
# enabled, the off form otherwise.
|
|
71
|
+
sig { params(on_form: String, off_form: String).returns(T::Array[String]) }
|
|
72
|
+
def render_settings(on_form, off_form)
|
|
73
|
+
NAMES.map { |name, option| format(on?(option) ? on_form : off_form, name: name) }
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
# A runtime string subjected to parameter expansion ONLY — the treatment
|
|
6
|
+
# POSIX gives PS1/PS2 prompts and the ENV pathname: $name and ${...} become
|
|
7
|
+
# ParamSegments via the shared ParamScanner; any other character — including
|
|
8
|
+
# command substitution, backticks and backslashes — is literal text. #word
|
|
9
|
+
# yields the segments for Expansion::Pipeline#expand_value.
|
|
10
|
+
class ParamText
|
|
11
|
+
extend T::Sig
|
|
12
|
+
|
|
13
|
+
sig { params(text: String).void }
|
|
14
|
+
def initialize(text)
|
|
15
|
+
@scanner = StringScanner.new(text)
|
|
16
|
+
@buffer = SegmentBuffer.new
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
sig { returns(AST::Word) }
|
|
20
|
+
def word
|
|
21
|
+
step until @scanner.eos?
|
|
22
|
+
@buffer.word
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
sig { void }
|
|
28
|
+
def step
|
|
29
|
+
char = T.must(@scanner.getch)
|
|
30
|
+
char == '$' ? dollar : @buffer << char
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
sig { void }
|
|
34
|
+
def dollar
|
|
35
|
+
ref = Lexer::ParamScanner.new(@scanner, quoted: false).read
|
|
36
|
+
ref ? @buffer.push(AST::ParamSegment.new(ref, false)) : @buffer << '$'
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|