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,113 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
class Lexer
|
|
6
|
+
# Reads the body of a $( ... ) after the consumed `$(`, up to the closing
|
|
7
|
+
# `)` (POSIX 2.6.3: any valid shell script may appear inside). Plain
|
|
8
|
+
# parentheses nest; quoted strings, backslash escapes, backticks, ${...}
|
|
9
|
+
# references and `#` comments are skipped whole (ParenRegions); and a
|
|
10
|
+
# `case` pattern's unbalanced `)` (CaseTracker) does not count. The body
|
|
11
|
+
# keeps its raw spelling — it is re-parsed at expansion time.
|
|
12
|
+
class ParenReader
|
|
13
|
+
extend T::Sig
|
|
14
|
+
include ParenRegions
|
|
15
|
+
|
|
16
|
+
# The longest run with no syntactic weight for paren matching.
|
|
17
|
+
PLAIN = /[^\s()'"\\`;&|<>#$]+/
|
|
18
|
+
|
|
19
|
+
# Which CaseTracker event each operator character raises.
|
|
20
|
+
OPERATORS = {
|
|
21
|
+
'&' => :separator, '|' => :separator, "\n" => :newline,
|
|
22
|
+
'<' => :redirect, '>' => :redirect
|
|
23
|
+
}.freeze
|
|
24
|
+
|
|
25
|
+
sig { params(scanner: StringScanner).void }
|
|
26
|
+
def initialize(scanner)
|
|
27
|
+
@scanner = scanner
|
|
28
|
+
@tracker = T.let(CaseTracker.new, CaseTracker)
|
|
29
|
+
@word_start = T.let(true, T::Boolean)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
sig { returns(String) }
|
|
33
|
+
def read
|
|
34
|
+
body = +''
|
|
35
|
+
body << step while @tracker.open?
|
|
36
|
+
body
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
sig { returns(String) }
|
|
42
|
+
def step
|
|
43
|
+
blank || word || region || punct || closer
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
sig { returns(T.nilable(String)) }
|
|
47
|
+
def blank
|
|
48
|
+
text = @scanner.scan(/[^\S\n]+/)
|
|
49
|
+
@word_start = true if text
|
|
50
|
+
text
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
sig { returns(T.nilable(String)) }
|
|
54
|
+
def word
|
|
55
|
+
text = @scanner.scan(PLAIN)
|
|
56
|
+
return unless text
|
|
57
|
+
|
|
58
|
+
@tracker.word(@word_start ? text : '')
|
|
59
|
+
@word_start = false
|
|
60
|
+
text
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
sig { returns(T.nilable(String)) }
|
|
64
|
+
def punct
|
|
65
|
+
semi || operator || open_paren
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
sig { returns(T.nilable(String)) }
|
|
69
|
+
def semi
|
|
70
|
+
return unless @scanner.scan(';')
|
|
71
|
+
|
|
72
|
+
@word_start = true
|
|
73
|
+
return dsemi if @scanner.scan(';')
|
|
74
|
+
|
|
75
|
+
@tracker.separator
|
|
76
|
+
';'
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
sig { returns(String) }
|
|
80
|
+
def dsemi
|
|
81
|
+
@tracker.dsemi
|
|
82
|
+
';;'
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
sig { returns(T.nilable(String)) }
|
|
86
|
+
def operator
|
|
87
|
+
char = @scanner.scan(/[&|<>\n]/)
|
|
88
|
+
return unless char
|
|
89
|
+
|
|
90
|
+
@tracker.public_send(OPERATORS.fetch(char))
|
|
91
|
+
@word_start = true
|
|
92
|
+
char
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
sig { returns(T.nilable(String)) }
|
|
96
|
+
def open_paren
|
|
97
|
+
return unless @scanner.scan('(')
|
|
98
|
+
|
|
99
|
+
@tracker.open_paren
|
|
100
|
+
@word_start = true
|
|
101
|
+
'('
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
sig { returns(String) }
|
|
105
|
+
def closer
|
|
106
|
+
raise IncompleteInput, 'unterminated $(' unless @scanner.scan(')')
|
|
107
|
+
|
|
108
|
+
@word_start = true
|
|
109
|
+
@tracker.close_paren? ? ')' : ''
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
class Lexer
|
|
6
|
+
# The skip-whole regions of a $( ... ) body (ParenReader's mixin): quoted
|
|
7
|
+
# strings, backslash escapes, backticks, ${...} references, and `#`
|
|
8
|
+
# comments. A region reads as one ordinary — never keyword — word to the
|
|
9
|
+
# CaseTracker, and its text returns with the delimiters so the body keeps
|
|
10
|
+
# its raw spelling for the expansion-time re-parse.
|
|
11
|
+
module ParenRegions
|
|
12
|
+
extend T::Sig
|
|
13
|
+
include QuoteSkips
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
sig { returns(T.nilable(String)) }
|
|
18
|
+
def region
|
|
19
|
+
squote || dquote || backtick || escape || dollar || comment
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
sig { returns(T.nilable(String)) }
|
|
23
|
+
def squote
|
|
24
|
+
skipped(single) if @scanner.scan("'")
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
sig { returns(T.nilable(String)) }
|
|
28
|
+
def dquote
|
|
29
|
+
skipped(double) if @scanner.scan('"')
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
sig { returns(T.nilable(String)) }
|
|
33
|
+
def backtick
|
|
34
|
+
skipped("`#{SubstitutionReader.new(@scanner).backticks}`") if @scanner.scan('`')
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
sig { returns(T.nilable(String)) }
|
|
38
|
+
def escape
|
|
39
|
+
skipped("\\#{must_char}") if @scanner.scan('\\')
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
sig { returns(T.nilable(String)) }
|
|
43
|
+
def dollar
|
|
44
|
+
return unless @scanner.scan('$')
|
|
45
|
+
return skipped('$') unless @scanner.scan('{')
|
|
46
|
+
|
|
47
|
+
skipped("${#{braced}}")
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
sig { returns(String) }
|
|
51
|
+
def braced
|
|
52
|
+
BracedReader.new(@scanner, error: IncompleteInput, quoted: false).read
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# A `#` at the start of a word begins a comment running to the line
|
|
56
|
+
# end (kept raw, so a `)` inside it does not count); mid-word it is an
|
|
57
|
+
# ordinary character.
|
|
58
|
+
sig { returns(T.nilable(String)) }
|
|
59
|
+
def comment
|
|
60
|
+
return unless peek?('#')
|
|
61
|
+
return skipped(must_char) unless @word_start
|
|
62
|
+
|
|
63
|
+
@scanner.scan(/#[^\n]*/)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
sig { params(text: String).returns(String) }
|
|
67
|
+
def skipped(text)
|
|
68
|
+
@tracker.word('')
|
|
69
|
+
@word_start = false
|
|
70
|
+
text
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
class Lexer
|
|
6
|
+
# Quote-region skipping shared by the readers that must find a closing
|
|
7
|
+
# delimiter in raw source (BracedReader's `}`, ParenReader's `)`). Each
|
|
8
|
+
# helper consumes one region after its opening character and returns it
|
|
9
|
+
# with its delimiters, so the body keeps its raw spelling; a backslash
|
|
10
|
+
# always protects exactly the next character. Includers provide @scanner
|
|
11
|
+
# and may override #error_class to raise their configured error.
|
|
12
|
+
module QuoteSkips
|
|
13
|
+
extend T::Sig
|
|
14
|
+
include Kernel
|
|
15
|
+
include ScannerPredicates
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
# The error for unterminated input; an unfinished region can continue
|
|
20
|
+
# on the next interactive line.
|
|
21
|
+
sig { overridable.returns(T.class_of(ParseError)) }
|
|
22
|
+
def error_class
|
|
23
|
+
IncompleteInput
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
sig { returns(String) }
|
|
27
|
+
def must_char
|
|
28
|
+
@scanner.getch || raise(error_class, 'unterminated escape')
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
sig { returns(String) }
|
|
32
|
+
def single
|
|
33
|
+
body = @scanner.scan(/[^']*/)
|
|
34
|
+
raise error_class, 'unterminated single quote' unless @scanner.scan("'")
|
|
35
|
+
|
|
36
|
+
"'#{body}'"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
sig { returns(String) }
|
|
40
|
+
def double
|
|
41
|
+
body = +'"'
|
|
42
|
+
body << double_chunk until peek?('"')
|
|
43
|
+
@scanner.getch
|
|
44
|
+
body << '"'
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
sig { returns(String) }
|
|
48
|
+
def double_chunk
|
|
49
|
+
@scanner.scan(/[^"\\$`]+/) || double_special
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# `\`, `$` and backticks stay live inside double quotes: an embedded
|
|
53
|
+
# $( ... ) or ${ ... } is skipped whole, so its `)` and `}` never count.
|
|
54
|
+
sig { returns(String) }
|
|
55
|
+
def double_special
|
|
56
|
+
return "\\#{must_char}" if @scanner.scan('\\')
|
|
57
|
+
return "`#{SubstitutionReader.new(@scanner).backticks}`" if @scanner.scan('`')
|
|
58
|
+
|
|
59
|
+
double_dollar
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
sig { returns(String) }
|
|
63
|
+
def double_dollar
|
|
64
|
+
raise error_class, 'unterminated double quote' unless @scanner.scan('$')
|
|
65
|
+
return "${#{quoted_braced}}" if @scanner.scan('{')
|
|
66
|
+
return '$' unless @scanner.scan('(')
|
|
67
|
+
|
|
68
|
+
"$(#{SubstitutionReader.new(@scanner).parens})"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
sig { returns(String) }
|
|
72
|
+
def quoted_braced
|
|
73
|
+
BracedReader.new(@scanner, error: error_class, quoted: true).read
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'strscan'
|
|
5
|
+
|
|
6
|
+
module Rush
|
|
7
|
+
class Lexer
|
|
8
|
+
# Scans the operator word of a ${...} that appeared in a quoted context
|
|
9
|
+
# (double quotes or a here-doc body) into an AST::Word, following dash:
|
|
10
|
+
# $ and ` stay live, a backslash escapes " \ $ ` and the } it protected
|
|
11
|
+
# from closing the reference, embedded double quotes are removed (their
|
|
12
|
+
# content was already quoted), and single quotes are ordinary characters.
|
|
13
|
+
class QuotedWord
|
|
14
|
+
extend T::Sig
|
|
15
|
+
include ScannerPredicates
|
|
16
|
+
|
|
17
|
+
ESCAPES = { '"' => '"', '\\' => '\\', '$' => '$', '`' => '`', '}' => '}' }.freeze
|
|
18
|
+
ESCAPE_TABLE = EscapeTable.new(ESCAPES).freeze
|
|
19
|
+
DISPATCH = { '$' => :dollar, '`' => :backtick, '\\' => :escape }.freeze
|
|
20
|
+
|
|
21
|
+
sig { params(text: String).void }
|
|
22
|
+
def initialize(text)
|
|
23
|
+
@scanner = StringScanner.new(text)
|
|
24
|
+
@buffer = SegmentBuffer.new
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
sig { returns(AST::Word) }
|
|
28
|
+
def word
|
|
29
|
+
step until @scanner.eos?
|
|
30
|
+
@buffer.word
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
sig { void }
|
|
36
|
+
def step
|
|
37
|
+
char = T.must(@scanner.getch)
|
|
38
|
+
return if char == '"'
|
|
39
|
+
|
|
40
|
+
handler = DISPATCH.fetch(char, nil)
|
|
41
|
+
handler ? send(handler) : (@buffer << char)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# A lone `$` that begins no valid reference stays a literal dollar.
|
|
45
|
+
sig { void }
|
|
46
|
+
def dollar
|
|
47
|
+
@scanner.unscan
|
|
48
|
+
segment = DollarScanner.new(@scanner).read(quoted: true)
|
|
49
|
+
segment ? @buffer.push(segment) : (@buffer << '$')
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
sig { void }
|
|
53
|
+
def backtick
|
|
54
|
+
@scanner.unscan
|
|
55
|
+
@buffer.push(DollarScanner.new(@scanner).read_backtick(quoted: true))
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
sig { void }
|
|
59
|
+
def escape
|
|
60
|
+
char = @scanner.getch
|
|
61
|
+
return if char == "\n" # line continuation, as inside double quotes
|
|
62
|
+
|
|
63
|
+
@buffer << ESCAPE_TABLE.escape(char)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
class Lexer
|
|
6
|
+
# Small typed adapter for the StringScanner one-character lookahead pattern
|
|
7
|
+
# shared by the shell word scanners. StringScanner#peek is a library boundary;
|
|
8
|
+
# callers use this predicate instead of repeating the comparison everywhere.
|
|
9
|
+
module ScannerPredicates
|
|
10
|
+
extend T::Sig
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
sig { params(char: String).returns(T::Boolean) }
|
|
15
|
+
def peek?(char)
|
|
16
|
+
@scanner.peek(1) == char
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
class Lexer
|
|
6
|
+
# Tracks source line numbers for tokens produced from a StringScanner. The
|
|
7
|
+
# lexer itself owns tokenization; this helper only maps consumed scanner text
|
|
8
|
+
# to absolute source lines.
|
|
9
|
+
class SourceLines
|
|
10
|
+
extend T::Sig
|
|
11
|
+
|
|
12
|
+
sig { params(scanner: StringScanner, offset: Integer).void }
|
|
13
|
+
def initialize(scanner, offset)
|
|
14
|
+
@scanner = scanner
|
|
15
|
+
@line = offset + 1
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
sig { params(blk: T.proc.returns(AST::Word)).returns(AST::Word) }
|
|
19
|
+
def word(&blk)
|
|
20
|
+
line = @line
|
|
21
|
+
start = @scanner.pos
|
|
22
|
+
yield.tap { count_newlines_since(start) }.then { |word| AST::Word.new(word.segments, source_line: line) }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
sig { params(start: Integer).void }
|
|
26
|
+
def heredoc_newline(start)
|
|
27
|
+
@line += consumed_since(start).count("\n") + 1
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
sig { params(start: Integer).void }
|
|
33
|
+
def count_newlines_since(start)
|
|
34
|
+
@line += consumed_since(start).count("\n")
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
sig { params(start: Integer).returns(String) }
|
|
38
|
+
def consumed_since(start)
|
|
39
|
+
@scanner.string[start...@scanner.pos].to_s
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
class Lexer
|
|
6
|
+
# Reads the raw body of a command substitution from the shared scanner:
|
|
7
|
+
# $( ... ) up to the matching quote-aware `)` (via ParenReader), or
|
|
8
|
+
# ` ... ` up to the next backtick. The body is re-parsed and executed at
|
|
9
|
+
# expansion time.
|
|
10
|
+
class SubstitutionReader
|
|
11
|
+
extend T::Sig
|
|
12
|
+
|
|
13
|
+
DEPTH_DELTA = { '(' => 1, ')' => -1 }.freeze
|
|
14
|
+
|
|
15
|
+
sig { params(scanner: StringScanner).void }
|
|
16
|
+
def initialize(scanner)
|
|
17
|
+
@scanner = scanner
|
|
18
|
+
@depth = 0
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
sig { returns(String) }
|
|
22
|
+
def parens
|
|
23
|
+
ParenReader.new(@scanner).read
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
sig { returns(String) }
|
|
27
|
+
def backticks
|
|
28
|
+
body = @scanner.scan(/[^`]*/) || ''
|
|
29
|
+
raise IncompleteInput, 'unterminated `' unless @scanner.scan('`')
|
|
30
|
+
|
|
31
|
+
body
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Read the body of $(( ... )) after the leading `((`, up to the matching
|
|
35
|
+
# `))`, allowing balanced inner parentheses.
|
|
36
|
+
sig { returns(String) }
|
|
37
|
+
def arithmetic
|
|
38
|
+
@depth = 0
|
|
39
|
+
collect(+'')
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
sig { params(body: String).returns(String) }
|
|
45
|
+
def collect(body)
|
|
46
|
+
piece = arith_char
|
|
47
|
+
return body unless piece
|
|
48
|
+
|
|
49
|
+
collect(body << piece)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
sig { returns(T.nilable(String)) }
|
|
53
|
+
def arith_char
|
|
54
|
+
raise IncompleteInput, 'unterminated $((' if @scanner.eos?
|
|
55
|
+
|
|
56
|
+
adjust_arithmetic(@scanner.getch)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
sig { params(token: T.nilable(String)).returns(T.nilable(String)) }
|
|
60
|
+
def adjust_arithmetic(token)
|
|
61
|
+
delta = DEPTH_DELTA.fetch(token, 0)
|
|
62
|
+
return arith_close if delta.negative?
|
|
63
|
+
|
|
64
|
+
@depth += delta
|
|
65
|
+
token
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
sig { returns(T.nilable(String)) }
|
|
69
|
+
def arith_close
|
|
70
|
+
return ')'.tap { @depth -= 1 } if @depth.nonzero?
|
|
71
|
+
raise(ParseError, 'arithmetic: malformed') unless @scanner.scan(')')
|
|
72
|
+
|
|
73
|
+
nil
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
class Lexer
|
|
6
|
+
# Classifies a scanned word given the lexer state: a reserved word (only an
|
|
7
|
+
# unquoted single-literal word in command position), an ASSIGNMENT_WORD (an
|
|
8
|
+
# unquoted NAME= prefix), or a plain WORD. The assignment value keeps its
|
|
9
|
+
# quoted segments (x="a b").
|
|
10
|
+
class TokenClassifier
|
|
11
|
+
extend T::Sig
|
|
12
|
+
|
|
13
|
+
# The assignment-name capture plus the literal segment it came from.
|
|
14
|
+
class AssignmentHead
|
|
15
|
+
extend T::Sig
|
|
16
|
+
|
|
17
|
+
sig { returns(String) }
|
|
18
|
+
attr_reader :name, :literal
|
|
19
|
+
|
|
20
|
+
sig { params(name: String, literal: String, source_line: Integer).void }
|
|
21
|
+
def initialize(name, literal, source_line)
|
|
22
|
+
@name = name
|
|
23
|
+
@literal = literal
|
|
24
|
+
@source_line = source_line
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
sig { params(tail: T::Array[AST::WordSegment[T.untyped]]).returns(AST::Assignment) }
|
|
28
|
+
def assignment(tail)
|
|
29
|
+
AST::Assignment.new(name: name, value: word(tail))
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
sig { params(tail: T::Array[AST::WordSegment[T.untyped]]).returns(AST::Word) }
|
|
35
|
+
def word(tail)
|
|
36
|
+
remainder = AST::LiteralSegment.new(literal.delete_prefix("#{name}="), false)
|
|
37
|
+
AST::Word.new([remainder] + tail, source_line: @source_line)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
NAME = /\A([a-zA-Z_]\w*)=/
|
|
42
|
+
# `esac` is a reserved word in command position (POSIX 2.4): it closes a
|
|
43
|
+
# case item whose last list omits `;;`, and is a syntax error elsewhere.
|
|
44
|
+
RESERVED = {
|
|
45
|
+
'if' => :If, 'then' => :Then, 'else' => :Else, 'elif' => :Elif, 'fi' => :Fi,
|
|
46
|
+
'while' => :While, 'until' => :Until, 'do' => :Do, 'done' => :Done,
|
|
47
|
+
'for' => :For, 'case' => :Case, 'esac' => :Esac,
|
|
48
|
+
'{' => :Lbrace, '}' => :Rbrace, '!' => :Bang
|
|
49
|
+
}.freeze
|
|
50
|
+
FOR_IN = { 'in' => :In, 'do' => :Do }.freeze
|
|
51
|
+
|
|
52
|
+
sig { params(word: AST::Word, state: LexState).void }
|
|
53
|
+
def initialize(word, state)
|
|
54
|
+
@word = word
|
|
55
|
+
@state = state
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
sig { returns([Symbol, T.untyped]) }
|
|
59
|
+
def call
|
|
60
|
+
state_token || classify
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
sig { returns(T.nilable([Symbol, T.untyped])) }
|
|
66
|
+
def state_token
|
|
67
|
+
forced || header_token
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
sig { returns(T.nilable([Symbol, T.untyped])) }
|
|
71
|
+
def forced
|
|
72
|
+
return [:NAME, @word] if @state.for_name?
|
|
73
|
+
return [arm_token, @word] if @state.case_arm?
|
|
74
|
+
|
|
75
|
+
[:WORD, @word] if @state.case_subject? || @state.case_pat?
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
sig { returns(T.nilable([Symbol, T.untyped])) }
|
|
79
|
+
def header_token
|
|
80
|
+
keyword = for_header
|
|
81
|
+
return [keyword, @word] if @state.for_in? && keyword
|
|
82
|
+
|
|
83
|
+
[:In, @word] if @state.case_in? && @word.literal_name == 'in'
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
sig { returns(Symbol) }
|
|
87
|
+
def arm_token
|
|
88
|
+
@word.literal_name == 'esac' ? :Esac : :WORD
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
sig { returns([Symbol, T.untyped]) }
|
|
92
|
+
def classify
|
|
93
|
+
keyword = reserved
|
|
94
|
+
return [keyword, @word] if keyword
|
|
95
|
+
|
|
96
|
+
head = assignment_head
|
|
97
|
+
head ? assignment_token(head) : [:WORD, @word]
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
sig { returns(T.nilable(Symbol)) }
|
|
101
|
+
def for_header
|
|
102
|
+
lookup(FOR_IN)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
sig { returns(T.nilable(Symbol)) }
|
|
106
|
+
def reserved
|
|
107
|
+
lookup(RESERVED) if @state.expects_command?
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
sig { params(table: T::Hash[String, Symbol]).returns(T.nilable(Symbol)) }
|
|
111
|
+
def lookup(table)
|
|
112
|
+
name = @word.literal_name
|
|
113
|
+
name ? table.fetch(name, nil) : nil
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
sig { returns(T.nilable(AssignmentHead)) }
|
|
117
|
+
def assignment_head
|
|
118
|
+
return unless @state.expects_command?
|
|
119
|
+
|
|
120
|
+
literal = assignment_literal
|
|
121
|
+
return unless literal
|
|
122
|
+
|
|
123
|
+
name = capture(literal)
|
|
124
|
+
name ? AssignmentHead.new(name, literal, @word.source_line) : nil
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
sig { returns(T.nilable(String)) }
|
|
128
|
+
def assignment_literal
|
|
129
|
+
@word.first_literal_value
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
sig { params(value: String).returns(T.nilable(String)) }
|
|
133
|
+
def capture(value)
|
|
134
|
+
match = NAME.match(value)
|
|
135
|
+
match && match[1]
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
sig { params(head: AssignmentHead).returns([Symbol, T.untyped]) }
|
|
139
|
+
def assignment_token(head)
|
|
140
|
+
[:ASSIGNMENT_WORD, head.assignment(@word.segments.drop(1))]
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
class Lexer
|
|
6
|
+
# Typed token-shape predicates for the raw [kind, value] tuples returned to
|
|
7
|
+
# Racc. Keeping them here avoids repeating untyped tuple discriminator checks
|
|
8
|
+
# in the lexer hot path.
|
|
9
|
+
module TokenPredicates
|
|
10
|
+
extend T::Sig
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
sig { params(token: [T.untyped, T.untyped]).returns(T::Boolean) }
|
|
15
|
+
def word_token?(token)
|
|
16
|
+
case token.first
|
|
17
|
+
when :WORD then true
|
|
18
|
+
else false
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|