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,18 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
module AST
|
|
6
|
+
# `while cond; do body; done` — runs the body while the condition succeeds.
|
|
7
|
+
class While < ConditionLoop
|
|
8
|
+
extend T::Sig
|
|
9
|
+
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
sig { returns(Symbol) }
|
|
13
|
+
def kind
|
|
14
|
+
:while
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
module AST
|
|
6
|
+
# An unexpanded word: an ordered list of typed segments. The expander turns
|
|
7
|
+
# the segments into fields; `literal_text` is the simple concatenation used
|
|
8
|
+
# for error messages and (unquoted) assignment-name detection.
|
|
9
|
+
class Word < Node
|
|
10
|
+
extend T::Sig
|
|
11
|
+
|
|
12
|
+
attr_reader :segments, :source_line
|
|
13
|
+
|
|
14
|
+
sig { params(segments: T::Array[WordSegment[T.untyped]], source_line: Integer).void }
|
|
15
|
+
def initialize(segments, source_line: 1)
|
|
16
|
+
@segments = segments
|
|
17
|
+
@source_line = source_line
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
sig { params(text: String, source_line: Integer).returns(Word) }
|
|
21
|
+
def self.literal(text, source_line: 1)
|
|
22
|
+
new([LiteralSegment.new(text, false)], source_line: source_line)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
sig { returns(String) }
|
|
26
|
+
def literal_text
|
|
27
|
+
segments.map(&:value).join
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# The text when this word is a bare name — one unquoted literal segment, no
|
|
31
|
+
# quoting or substitution — else nil. Used for reserved words, NAME=, and
|
|
32
|
+
# alias substitution, all of which only apply to a plain literal word.
|
|
33
|
+
sig { returns(T.nilable(String)) }
|
|
34
|
+
def literal_name
|
|
35
|
+
(segments.first.literal_value if segments.one?)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
sig { returns(T.nilable(String)) }
|
|
39
|
+
def first_literal_value
|
|
40
|
+
segments.fetch(0).literal_value
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
module AST
|
|
6
|
+
# A typed piece of a word. Subclasses know how to expand themselves (the
|
|
7
|
+
# segment-level counterpart of a command node's #execute), so there is no
|
|
8
|
+
# `kind` to dispatch on: `quoted` records whether it came from a quoted
|
|
9
|
+
# context (which governs field splitting and pathname expansion), `value` is
|
|
10
|
+
# the kind-specific payload (literal text, a ParamRef, or substitution
|
|
11
|
+
# source). literal_value is the text when the segment can stand in as a bare
|
|
12
|
+
# name; splittable?/splat? are its field-splitting roles.
|
|
13
|
+
class WordSegment
|
|
14
|
+
extend T::Sig
|
|
15
|
+
extend T::Generic
|
|
16
|
+
|
|
17
|
+
Value = type_member
|
|
18
|
+
|
|
19
|
+
sig { returns(Value) }
|
|
20
|
+
attr_reader :value
|
|
21
|
+
|
|
22
|
+
sig { returns(T::Boolean) }
|
|
23
|
+
attr_reader :quoted
|
|
24
|
+
|
|
25
|
+
sig { params(value: Value, quoted: T::Boolean).void }
|
|
26
|
+
def initialize(value, quoted)
|
|
27
|
+
@value = value
|
|
28
|
+
@quoted = quoted
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Each concrete segment expands itself to a string (the segment-level
|
|
32
|
+
# counterpart of a command node's #execute); the base is abstract.
|
|
33
|
+
sig { params(_executor: Executor).returns(String) }
|
|
34
|
+
def expand(_executor)
|
|
35
|
+
raise NotImplementedError
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
sig { returns(T.nilable(String)) }
|
|
39
|
+
# mutant:disable -- `nil` is the default "not a literal" value; an empty
|
|
40
|
+
# method body has the same public semantics for this abstract fallback.
|
|
41
|
+
def literal_value
|
|
42
|
+
nil
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# This segment as dash's canonical job text prints it (CommandText):
|
|
46
|
+
# each kind re-spells itself; the base is abstract.
|
|
47
|
+
sig { returns(String) }
|
|
48
|
+
def canon
|
|
49
|
+
raise NotImplementedError
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
sig { returns(T::Boolean) }
|
|
53
|
+
def splittable?
|
|
54
|
+
false
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
sig { returns(T::Boolean) }
|
|
58
|
+
def splat?
|
|
59
|
+
false
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
sig { params(executor: Executor, parts: T::Array[Expansion::FieldPart]).void }
|
|
63
|
+
def append_field_parts(executor, parts)
|
|
64
|
+
parts << [expand(executor), splittable?, false, quoted]
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# A copy with a rewritten value (e.g. after tilde expansion).
|
|
68
|
+
sig { params(new_value: Value).returns(WordSegment[Value]) }
|
|
69
|
+
def with_value(new_value)
|
|
70
|
+
self.class.new(new_value, quoted)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
sig { params(other: T.untyped).returns(T::Boolean) }
|
|
74
|
+
def ==(other)
|
|
75
|
+
return false unless other.instance_of?(self.class)
|
|
76
|
+
return false unless equality_value.eql?(other.equality_value)
|
|
77
|
+
return false unless quoted.eql?(other.quoted)
|
|
78
|
+
|
|
79
|
+
true
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
alias eql? ==
|
|
83
|
+
|
|
84
|
+
sig { returns(T.untyped) }
|
|
85
|
+
def equality_value
|
|
86
|
+
value
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
sig { returns(Integer) }
|
|
90
|
+
def hash
|
|
91
|
+
[self.class, value, quoted].hash
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Literal text. It expands to itself, can act as a bare name when unquoted,
|
|
96
|
+
# and is never field-split.
|
|
97
|
+
class LiteralSegment < WordSegment
|
|
98
|
+
extend T::Sig
|
|
99
|
+
extend T::Generic
|
|
100
|
+
|
|
101
|
+
Value = type_member { { fixed: String } }
|
|
102
|
+
|
|
103
|
+
# dash escapes \ " $ ` inside the double quotes a quoted run gets.
|
|
104
|
+
CANON_ESCAPES = T.let(/[\\"$`]/, Regexp)
|
|
105
|
+
|
|
106
|
+
sig { params(_executor: Executor).returns(String) }
|
|
107
|
+
def expand(_executor)
|
|
108
|
+
value
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
sig { returns(T.nilable(String)) }
|
|
112
|
+
def literal_value
|
|
113
|
+
(value unless quoted)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
sig { returns(String) }
|
|
117
|
+
def canon
|
|
118
|
+
quoted ? value.gsub(CANON_ESCAPES) { |char| "\\#{char}" } : value
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# A segment substituted at expansion time (param / command / arithmetic): its
|
|
123
|
+
# unquoted result undergoes field splitting.
|
|
124
|
+
class DynamicSegment < WordSegment
|
|
125
|
+
extend T::Sig
|
|
126
|
+
extend T::Generic
|
|
127
|
+
|
|
128
|
+
Value = type_member
|
|
129
|
+
|
|
130
|
+
sig { returns(T::Boolean) }
|
|
131
|
+
def splittable?
|
|
132
|
+
!quoted
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# $name / ${...}: value is the ParamRef.
|
|
137
|
+
class ParamSegment < DynamicSegment
|
|
138
|
+
extend T::Sig
|
|
139
|
+
extend T::Generic
|
|
140
|
+
|
|
141
|
+
Value = type_member { { fixed: ParamRef } }
|
|
142
|
+
|
|
143
|
+
sig { params(executor: Executor).returns(String) }
|
|
144
|
+
def expand(executor)
|
|
145
|
+
Expansion::ParameterExpander.new(executor, value, quoted: quoted).expand
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# $@ (always) and unquoted $* expand to one field per positional parameter.
|
|
149
|
+
sig { returns(T::Boolean) }
|
|
150
|
+
def splat?
|
|
151
|
+
!value.op && (value.name == '@' || (value.name == '*' && !quoted))
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
sig { params(executor: Executor, parts: T::Array[Expansion::FieldPart]).void }
|
|
155
|
+
def append_field_parts(executor, parts)
|
|
156
|
+
parts.concat(Expansion::ParameterExpander.new(executor, value, quoted: quoted).expand_parts)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
sig { returns(String) }
|
|
160
|
+
def canon
|
|
161
|
+
value.canon
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# $(...) / `...`: value is the command-substitution source.
|
|
166
|
+
class CommandSegment < DynamicSegment
|
|
167
|
+
extend T::Sig
|
|
168
|
+
extend T::Generic
|
|
169
|
+
|
|
170
|
+
Value = type_member { { fixed: String } }
|
|
171
|
+
|
|
172
|
+
sig { params(executor: Executor).returns(String) }
|
|
173
|
+
def expand(executor)
|
|
174
|
+
Expansion::CommandSubstitution.new(executor, value).expand
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# dash elides a command substitution's body from the job text.
|
|
178
|
+
sig { returns(String) }
|
|
179
|
+
def canon
|
|
180
|
+
'$(...)'
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# $((...)): value is the arithmetic source.
|
|
185
|
+
class ArithSegment < DynamicSegment
|
|
186
|
+
extend T::Sig
|
|
187
|
+
extend T::Generic
|
|
188
|
+
|
|
189
|
+
Value = type_member { { fixed: String } }
|
|
190
|
+
|
|
191
|
+
sig { params(executor: Executor).returns(String) }
|
|
192
|
+
def expand(executor)
|
|
193
|
+
Expansion::ArithmeticExpander.new(executor, value).expand
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# An arithmetic expansion keeps its raw source in the job text.
|
|
197
|
+
sig { returns(String) }
|
|
198
|
+
def canon
|
|
199
|
+
"$((#{value}))"
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
# Runs an asynchronous list entry in a forked subshell. The parent records
|
|
6
|
+
# the child pid for $! and in the job table (where the wait builtin finds
|
|
7
|
+
# it) and immediately returns success (launch semantics); the child
|
|
8
|
+
# isolates itself per POSIX (stdin from /dev/null, SIGINT/SIGQUIT ignored)
|
|
9
|
+
# and resolves shell-control exceptions to an exit status like a subshell.
|
|
10
|
+
class BackgroundRunner
|
|
11
|
+
extend T::Sig
|
|
12
|
+
|
|
13
|
+
sig { params(executor: Executor, body: AST::Node).void }
|
|
14
|
+
def initialize(executor, body)
|
|
15
|
+
@executor = executor
|
|
16
|
+
@body = body
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
sig { returns(Status) }
|
|
20
|
+
def call
|
|
21
|
+
pid = @executor.job_control.launch_background { run_child }
|
|
22
|
+
@executor.state.record_background_pid(pid)
|
|
23
|
+
@executor.jobs.record(pid, text: @executor.job_control.job_text(@body))
|
|
24
|
+
Status.success
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
sig { returns(Status) }
|
|
28
|
+
def run_body
|
|
29
|
+
isolate
|
|
30
|
+
SubshellRunner.new(@executor, @body).run_body
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
# POSIX 2.9.3.1 / 2.11 with job control disabled: an async child reads
|
|
36
|
+
# stdin from /dev/null (its own redirections may rebind it) and starts
|
|
37
|
+
# with SIGINT/SIGQUIT ignored. Both apply ONLY without job control — under
|
|
38
|
+
# set -m the job sits in its own process group, so POSIX (and dash,
|
|
39
|
+
# probed) leave stdin and SIGINT alone; monitored? is read before
|
|
40
|
+
# enter_subshell switches the machinery off for this child. The subshell
|
|
41
|
+
# entry (trap reset + job-table clear) must run first — an interactive
|
|
42
|
+
# session's base handlers reinstall OS defaults as they drop, which would
|
|
43
|
+
# undo the ignores. The immediate repeat inside SubshellRunner#run_body is
|
|
44
|
+
# harmless because isolation creates no reset-sensitive shell state between
|
|
45
|
+
# entries: no body, child job, pending signal, caught trap or EXIT action.
|
|
46
|
+
# Raw dispositions and stdin are deliberately installed there, and the
|
|
47
|
+
# second entry does not reset them. This is not general idempotence. The
|
|
48
|
+
# ignores are a real SIG_IGN, kept off the trap table: they
|
|
49
|
+
# survive exec and nested subshells, while `trap` overrides them and
|
|
50
|
+
# `trap - INT` restores the OS default (dash-verified).
|
|
51
|
+
sig { void }
|
|
52
|
+
def isolate
|
|
53
|
+
monitored = @executor.job_control.monitored?
|
|
54
|
+
@executor.enter_subshell
|
|
55
|
+
return if monitored
|
|
56
|
+
|
|
57
|
+
%w[INT QUIT].each { |name| @executor.system.trap_signal(name, 'IGNORE') { nil } }
|
|
58
|
+
@executor.replace_io(@executor.io.with(0, @executor.system.open_file(File::NULL, 'r')))
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
sig { returns(T.noreturn) }
|
|
62
|
+
def run_child
|
|
63
|
+
# :nocov:
|
|
64
|
+
@executor.system.exit!(run_body.exitstatus)
|
|
65
|
+
# :nocov:
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'strscan'
|
|
5
|
+
|
|
6
|
+
module Rush
|
|
7
|
+
# One shell-pattern bracket expression. It recognizes the nested POSIX
|
|
8
|
+
# character-class, equivalence-class and collating-symbol delimiters so their
|
|
9
|
+
# inner `]` does not terminate the outer expression.
|
|
10
|
+
class BracketExpression
|
|
11
|
+
extend T::Sig
|
|
12
|
+
|
|
13
|
+
CLASS_NAMES = %w[alnum alpha blank cntrl digit graph lower print punct space upper xdigit].freeze
|
|
14
|
+
|
|
15
|
+
sig { returns(String) }
|
|
16
|
+
attr_reader :source
|
|
17
|
+
|
|
18
|
+
sig { returns(Integer) }
|
|
19
|
+
attr_reader :finish
|
|
20
|
+
|
|
21
|
+
sig { params(pattern: String, start: Integer).returns(T.nilable(BracketExpression)) }
|
|
22
|
+
def self.parse(pattern, start)
|
|
23
|
+
finish, special = BracketScanner.new(pattern, start).call
|
|
24
|
+
return new(T.must(pattern[start...finish]), finish, special, true) if finish
|
|
25
|
+
|
|
26
|
+
new(T.must(pattern[start..]), pattern.length, true, false) if special
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
sig { params(source: String, finish: Integer, special: T::Boolean, closed: T::Boolean).void }
|
|
30
|
+
def initialize(source, finish, special, closed)
|
|
31
|
+
@source = source
|
|
32
|
+
@finish = finish
|
|
33
|
+
@special = special
|
|
34
|
+
@closed = closed
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
sig { returns(T::Boolean) }
|
|
38
|
+
def special?
|
|
39
|
+
@special
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
sig { returns(String) }
|
|
43
|
+
def glob_source
|
|
44
|
+
special? ? '?' : source
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
sig { params(regexp: String, glob: String, scanner: StringScanner).void }
|
|
48
|
+
def append_to(regexp, glob, scanner)
|
|
49
|
+
regexp << regex
|
|
50
|
+
glob << glob_source
|
|
51
|
+
scanner.pos = finish
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Ruby regexp supports the twelve required POSIX named classes. The two
|
|
55
|
+
# collation forms have a portable one-character meaning in the POSIX locale;
|
|
56
|
+
# multi-character locale collation is deliberately left invalid here.
|
|
57
|
+
sig { returns(String) }
|
|
58
|
+
def regex
|
|
59
|
+
raise RegexpError unless @closed
|
|
60
|
+
|
|
61
|
+
"[#{regexp_body}]"
|
|
62
|
+
rescue RegexpError
|
|
63
|
+
'(?!)'
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
sig { returns(String) }
|
|
69
|
+
def regexp_body
|
|
70
|
+
body = collating(classes(negation(source.delete_prefix('[').delete_suffix(']'))))
|
|
71
|
+
escape_brackets(body)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
sig { params(body: String).returns(String) }
|
|
75
|
+
def escape_brackets(body)
|
|
76
|
+
body = body.sub(/\A(\^?)\]/) { "#{Regexp.last_match(1)}\\]" }
|
|
77
|
+
body.gsub(/(?<!\\)\[(?!:)/, '\\\\[')
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
sig { params(body: String).returns(String) }
|
|
81
|
+
def negation(body)
|
|
82
|
+
body.sub(/\A[!^]/, '^')
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
sig { params(body: String).returns(String) }
|
|
86
|
+
def classes(body)
|
|
87
|
+
body.gsub(/\[:([a-z]+):\]/) do
|
|
88
|
+
name = Regexp.last_match(1)
|
|
89
|
+
raise RegexpError unless CLASS_NAMES.include?(name)
|
|
90
|
+
|
|
91
|
+
"[:#{name}:]"
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
sig { params(body: String).returns(String) }
|
|
96
|
+
def collating(body)
|
|
97
|
+
body.gsub(/\[([=.])(.*?)\1\]/) { collating_element(T.must(Regexp.last_match(2))) }
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
sig { params(element: String).returns(String) }
|
|
101
|
+
def collating_element(element)
|
|
102
|
+
raise RegexpError unless element.each_char.one?
|
|
103
|
+
|
|
104
|
+
Regexp.escape(element)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
# Finds the outer close of a shell bracket expression without mistaking the
|
|
6
|
+
# `]` inside POSIX `[:class:]`, `[=equiv=]` or `[.collating.]` for it.
|
|
7
|
+
class BracketScanner
|
|
8
|
+
extend T::Sig
|
|
9
|
+
|
|
10
|
+
TOKEN_ENDS = { ':' => ':]', '=' => '=]', '.' => '.]' }.freeze
|
|
11
|
+
|
|
12
|
+
sig { params(pattern: String, start: Integer).void }
|
|
13
|
+
def initialize(pattern, start)
|
|
14
|
+
@pattern = pattern
|
|
15
|
+
@index = first_index(start)
|
|
16
|
+
@special = false
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
sig { returns([T.nilable(Integer), T::Boolean]) }
|
|
20
|
+
def call
|
|
21
|
+
advance until complete?
|
|
22
|
+
[finish, @special]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
sig { params(start: Integer).returns(Integer) }
|
|
28
|
+
def first_index(start)
|
|
29
|
+
index = start + 1
|
|
30
|
+
index += 1 if %w[! ^].include?(@pattern[index])
|
|
31
|
+
@pattern[index] == ']' ? index + 1 : index
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
sig { returns(T::Boolean) }
|
|
35
|
+
def complete?
|
|
36
|
+
current = @pattern[@index]
|
|
37
|
+
!current || current == ']'
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
sig { void }
|
|
41
|
+
def advance
|
|
42
|
+
token_finish = nested_finish
|
|
43
|
+
@special = true if token_finish
|
|
44
|
+
@index = token_finish || escaped_finish || (@index + 1)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
sig { returns(T.nilable(Integer)) }
|
|
48
|
+
def escaped_finish
|
|
49
|
+
@index + 2 if @pattern[@index] == '\\' && @pattern[@index + 1]
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
sig { returns(T.nilable(Integer)) }
|
|
53
|
+
def nested_finish
|
|
54
|
+
ending = TOKEN_ENDS[@pattern[@index + 1].to_s] if @pattern[@index] == '['
|
|
55
|
+
found = @pattern.index(ending, @index + 2) if ending
|
|
56
|
+
found + 2 if found
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
sig { returns(T.nilable(Integer)) }
|
|
60
|
+
def finish
|
|
61
|
+
@index + 1 if @index < @pattern.length
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
module Builtins
|
|
6
|
+
# `alias [name[=value]...]` defines or prints aliases. With no operands it
|
|
7
|
+
# lists every alias as a single-quoted `name=value` on stdout. An operand
|
|
8
|
+
# containing `=` past its first character defines (the name is everything
|
|
9
|
+
# before that `=`); a bare name prints that alias, or reports `alias: NAME not
|
|
10
|
+
# found` on stderr and yields status 1 while still processing the rest.
|
|
11
|
+
class Alias < Base
|
|
12
|
+
extend T::Sig
|
|
13
|
+
|
|
14
|
+
sig { returns(Status) }
|
|
15
|
+
def call
|
|
16
|
+
if operands.empty?
|
|
17
|
+
list
|
|
18
|
+
else
|
|
19
|
+
operands.reduce(success) { |status, operand| keep(status, handle(operand)) }
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
sig { params(operand: String).returns(Status) }
|
|
26
|
+
def handle(operand)
|
|
27
|
+
name, sep, value = operand.partition('=')
|
|
28
|
+
sep.empty? || name.empty? ? query(operand) : define(name, value)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
sig { params(name: String, value: String).returns(Status) }
|
|
32
|
+
def define(name, value)
|
|
33
|
+
aliases.define(name, value)
|
|
34
|
+
success
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
sig { params(name: String).returns(Status) }
|
|
38
|
+
def query(name)
|
|
39
|
+
value = aliases.value(name)
|
|
40
|
+
return show(name, value) if value
|
|
41
|
+
|
|
42
|
+
stderr.puts("alias: #{name} not found")
|
|
43
|
+
failure
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
sig { returns(Status) }
|
|
47
|
+
def list
|
|
48
|
+
aliases.listing.each { |name, value| show(name, value) }
|
|
49
|
+
success
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
sig { params(name: String, value: String).returns(Status) }
|
|
53
|
+
def show(name, value)
|
|
54
|
+
stdout.puts(single_quote("#{name}=#{value}"))
|
|
55
|
+
success
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
sig { params(text: String).returns(String) }
|
|
59
|
+
def single_quote(text)
|
|
60
|
+
"'#{text.gsub("'", %q('"'"'))}'"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
sig { params(status: Status, result: Status).returns(Status) }
|
|
64
|
+
def keep(status, result)
|
|
65
|
+
status.success? ? result : status
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
sig { returns(AliasTable) }
|
|
69
|
+
def aliases
|
|
70
|
+
executor.state.aliases
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
module Builtins
|
|
6
|
+
# Base for builtins. Subclasses implement #call returning a Status. Streams
|
|
7
|
+
# come from the per-command IoTable so redirections apply to builtins too.
|
|
8
|
+
class Base
|
|
9
|
+
extend T::Sig
|
|
10
|
+
|
|
11
|
+
# A signed decimal integer (surrounding blanks allowed); the caller's min
|
|
12
|
+
# enforces whether negative or zero values are valid for this operand. dash
|
|
13
|
+
# parses it into a C int, so a value past INT_MAX overflows and is rejected
|
|
14
|
+
# like a non-numeric one.
|
|
15
|
+
NUMERIC_OPERAND = /\A\s*[+-]?\d+\s*\z/
|
|
16
|
+
INT_MAX = 2_147_483_647
|
|
17
|
+
|
|
18
|
+
sig do
|
|
19
|
+
params(executor: Executor, argv: T::Array[String], io: IoTable,
|
|
20
|
+
environment: T.nilable(T::Hash[String, String])).void
|
|
21
|
+
end
|
|
22
|
+
def initialize(executor, argv, io, environment = nil)
|
|
23
|
+
@executor = executor
|
|
24
|
+
@argv = argv
|
|
25
|
+
@io = io
|
|
26
|
+
@environment = environment
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
sig { returns(Status) }
|
|
30
|
+
def call
|
|
31
|
+
raise NotImplementedError
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
sig { returns(Executor) }
|
|
37
|
+
attr_reader :executor
|
|
38
|
+
|
|
39
|
+
sig { returns(T::Array[String]) }
|
|
40
|
+
attr_reader :argv
|
|
41
|
+
|
|
42
|
+
sig { returns(IoTable) }
|
|
43
|
+
attr_reader :io
|
|
44
|
+
|
|
45
|
+
# The exported environment assembled for this simple-command invocation,
|
|
46
|
+
# including temporary prefix assignments. Most builtins do not consume it;
|
|
47
|
+
# `command` forwards it when its nested target is external.
|
|
48
|
+
sig { returns(T::Hash[String, String]) }
|
|
49
|
+
def command_environment
|
|
50
|
+
@environment || executor.state.variables.exported
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
sig { returns(T::Array[String]) }
|
|
54
|
+
def operands
|
|
55
|
+
argv.drop(1)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
sig { returns(T.untyped) }
|
|
59
|
+
def stdout
|
|
60
|
+
io.get(1)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
sig { returns(T.untyped) }
|
|
64
|
+
def stderr
|
|
65
|
+
io.get(2)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
sig { returns(Status) }
|
|
69
|
+
def success
|
|
70
|
+
Status.success
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
sig { params(code: Integer).returns(Status) }
|
|
74
|
+
def failure(code = 1)
|
|
75
|
+
Status.failure(code)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Parse a numeric operand for a special builtin: an exit code for
|
|
79
|
+
# exit/return (min 0), or a loop level for break/continue (min 1). dash
|
|
80
|
+
# rejects a non-numeric, too-small or out-of-range value with a
|
|
81
|
+
# special-builtin error (which aborts a non-interactive shell).
|
|
82
|
+
sig { params(text: String, min: Integer).returns(Integer) }
|
|
83
|
+
def numeric_operand(text, min: 0)
|
|
84
|
+
value = Integer(text, 10) if text.match?(NUMERIC_OPERAND)
|
|
85
|
+
return T.must(value) if legal_operand?(value, min)
|
|
86
|
+
|
|
87
|
+
raise BuiltinError, "#{argv.first}: Illegal number: #{text}"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
sig { params(value: T.nilable(Integer), min: Integer).returns(T::Boolean) }
|
|
91
|
+
def legal_operand?(value, min)
|
|
92
|
+
!!value&.between?(min, INT_MAX)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
module Builtins
|
|
6
|
+
# `break [n]` — exit the n-th enclosing loop (default 1); the shared
|
|
7
|
+
# POSIX 2.9.5 semantics live in LoopJump.
|
|
8
|
+
class Break < LoopJump
|
|
9
|
+
extend T::Sig
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
sig { returns(T.class_of(LoopControl)) }
|
|
14
|
+
def signal
|
|
15
|
+
BreakSignal
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|