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,77 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
# Resolves shell parameters from the namespaces a shell exposes: ordinary
|
|
6
|
+
# variables, special parameters ($?, $#, $$, $0, $@/$*, $-, $!), and positional
|
|
7
|
+
# parameters ($1, $2, ...). $$ is the original shell pid stored in ShellState,
|
|
8
|
+
# so forked subshells and command substitutions preserve it like POSIX shells.
|
|
9
|
+
class ShellParameters
|
|
10
|
+
extend T::Sig
|
|
11
|
+
|
|
12
|
+
sig { params(state: T.untyped).void }
|
|
13
|
+
def initialize(state)
|
|
14
|
+
@state = state
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
sig { params(parameter: String).returns(T.nilable(String)) }
|
|
18
|
+
def resolve(parameter)
|
|
19
|
+
return @state.pids.shell.to_s if parameter == '$'
|
|
20
|
+
|
|
21
|
+
special = special_parameters.fetch(parameter, nil)
|
|
22
|
+
special ? special.call : ordinary_or_positional(parameter)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
sig { returns(T::Hash[String, T.proc.returns(T.nilable(String))]) }
|
|
28
|
+
def special_parameters
|
|
29
|
+
{ '?' => -> { status }, '#' => -> { count }, '0' => -> { @state.name },
|
|
30
|
+
'@' => -> { positional_all }, '*' => -> { positional_all }, '-' => -> { options },
|
|
31
|
+
'!' => -> { background } }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
sig { returns(String) }
|
|
35
|
+
def status
|
|
36
|
+
@state.last_status.exitstatus.to_s
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
sig { returns(String) }
|
|
40
|
+
def count
|
|
41
|
+
@state.positional.size.to_s
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
sig { returns(String) }
|
|
45
|
+
def positional_all
|
|
46
|
+
@state.positional.join(separator)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
sig { returns(String) }
|
|
50
|
+
def separator
|
|
51
|
+
ifs = @state.variables.get('IFS')
|
|
52
|
+
ifs ? (ifs.each_char.first || '') : ' '
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
sig { returns(String) }
|
|
56
|
+
def options
|
|
57
|
+
@state.options.letters
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
sig { returns(T.nilable(String)) }
|
|
61
|
+
def background
|
|
62
|
+
@state.last_background_pid&.to_s
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
sig { params(parameter: String).returns(T.nilable(String)) }
|
|
66
|
+
def ordinary_or_positional(parameter)
|
|
67
|
+
return positional(Integer(parameter, 10)) if parameter.match?(/\A\d+\z/)
|
|
68
|
+
|
|
69
|
+
@state.variables.get(parameter)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
sig { params(index: Integer).returns(T.nilable(String)) }
|
|
73
|
+
def positional(index)
|
|
74
|
+
@state.positional[index - 1]
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
# A shell `case`/parameter/pathname pattern. Ruby handles ordinary patterns;
|
|
6
|
+
# patterns containing POSIX bracket subforms are compiled to a Ruby regexp,
|
|
7
|
+
# while #glob_source broadens those brackets for Dir.glob candidate discovery.
|
|
8
|
+
# :reek:InstanceVariableAssumption -- @scanner is initialized by PatternScanner#initialize
|
|
9
|
+
class ShellPattern < PatternScanner
|
|
10
|
+
extend T::Sig
|
|
11
|
+
|
|
12
|
+
sig { returns(String) }
|
|
13
|
+
attr_reader :glob_source
|
|
14
|
+
|
|
15
|
+
sig { params(source: String).void }
|
|
16
|
+
def initialize(source)
|
|
17
|
+
@regexp = +''
|
|
18
|
+
@glob_source = +''
|
|
19
|
+
@extended = false
|
|
20
|
+
super
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
sig { returns(T::Boolean) }
|
|
24
|
+
def extended?
|
|
25
|
+
@extended
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
sig { params(text: String).returns(T::Boolean) }
|
|
29
|
+
def match?(text)
|
|
30
|
+
return File.fnmatch(@scanner.string, text, File::FNM_DOTMATCH) unless extended?
|
|
31
|
+
|
|
32
|
+
Regexp.new("\\A#{@regexp}\\z", Regexp::MULTILINE).match?(text)
|
|
33
|
+
rescue RegexpError
|
|
34
|
+
false
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
sig { params(text: String).returns(T::Boolean) }
|
|
38
|
+
def ===(text)
|
|
39
|
+
match?(text)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
sig { void }
|
|
45
|
+
def append_escape
|
|
46
|
+
@scanner.getch
|
|
47
|
+
escaped = @scanner.getch
|
|
48
|
+
@glob_source << '\\' << escaped.to_s
|
|
49
|
+
@regexp << Regexp.escape(escaped || '\\')
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
sig { void }
|
|
53
|
+
def append_wildcard
|
|
54
|
+
char = @scanner.getch.to_s
|
|
55
|
+
@glob_source << char
|
|
56
|
+
@regexp << (char == '*' ? '.*' : '.')
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
sig { void }
|
|
60
|
+
def append_bracket
|
|
61
|
+
bracket = BracketExpression.parse(@scanner.string, @scanner.pos)
|
|
62
|
+
return append_literal unless bracket
|
|
63
|
+
|
|
64
|
+
append_expression(bracket)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
sig { params(bracket: BracketExpression).void }
|
|
68
|
+
def append_expression(bracket)
|
|
69
|
+
bracket.append_to(@regexp, @glob_source, @scanner)
|
|
70
|
+
@extended = bracket.special? || @extended
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
sig { void }
|
|
74
|
+
def append_literal
|
|
75
|
+
char = @scanner.getch.to_s
|
|
76
|
+
@glob_source << char
|
|
77
|
+
@regexp << Regexp.escape(char)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
# Original shell and parent process ids used for $$ and PPID.
|
|
6
|
+
ShellProcessIds = Data.define(:shell, :parent) do
|
|
7
|
+
extend T::Sig
|
|
8
|
+
|
|
9
|
+
sig { params(system: SystemCalls).returns(ShellProcessIds) }
|
|
10
|
+
def self.for(system)
|
|
11
|
+
new(system.pid, system.ppid)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# The mutable shell state threaded through execution: shell variables,
|
|
16
|
+
# the last command's status ($?, the one field with behaviour here), the shell
|
|
17
|
+
# name ($0), and the session sub-objects it bundles for the rest of the
|
|
18
|
+
# interpreter to drive directly — LoopNesting, Options, Positional, the
|
|
19
|
+
# function/alias/trap tables and the command-location cache.
|
|
20
|
+
class ShellState
|
|
21
|
+
extend T::Sig
|
|
22
|
+
|
|
23
|
+
sig { returns(FunctionTable) }
|
|
24
|
+
attr_reader :functions
|
|
25
|
+
|
|
26
|
+
sig { returns(TrapTable) }
|
|
27
|
+
attr_reader :traps
|
|
28
|
+
|
|
29
|
+
sig { returns(AliasTable) }
|
|
30
|
+
attr_reader :aliases
|
|
31
|
+
|
|
32
|
+
sig { returns(T::Hash[String, String]) }
|
|
33
|
+
attr_reader :command_hash
|
|
34
|
+
|
|
35
|
+
sig { returns(String) }
|
|
36
|
+
attr_reader :name
|
|
37
|
+
|
|
38
|
+
# $$ and PPID stay the original ids in forked children, like POSIX shells.
|
|
39
|
+
sig { returns(ShellProcessIds) }
|
|
40
|
+
attr_reader :pids
|
|
41
|
+
|
|
42
|
+
sig { returns(ShellVariables) }
|
|
43
|
+
attr_reader :variables
|
|
44
|
+
|
|
45
|
+
sig { returns(LoopNesting) }
|
|
46
|
+
attr_reader :loops
|
|
47
|
+
|
|
48
|
+
sig { returns(Options) }
|
|
49
|
+
attr_reader :options
|
|
50
|
+
|
|
51
|
+
sig { returns(Status) }
|
|
52
|
+
attr_reader :last_status
|
|
53
|
+
|
|
54
|
+
sig { returns(T.nilable(Integer)) }
|
|
55
|
+
attr_reader :last_background_pid
|
|
56
|
+
|
|
57
|
+
sig { returns(Positional) }
|
|
58
|
+
attr_reader :positional
|
|
59
|
+
|
|
60
|
+
sig { returns(GetoptsState) }
|
|
61
|
+
attr_reader :getopts
|
|
62
|
+
|
|
63
|
+
# The one wiring point for every shell sub-table. Thirteen tables put the
|
|
64
|
+
# ABC size at ~18 from breadth alone — 13 assignments and 13 constructor
|
|
65
|
+
# sends, zero branches — and going lower would mean bundling namespaces
|
|
66
|
+
# POSIX keeps distinct (aliases are pre-parse substitution, not command
|
|
67
|
+
# search), so the disable stays as the measured wiring floor.
|
|
68
|
+
# rubocop:disable Metrics/AbcSize
|
|
69
|
+
sig { params(environment: Environment, name: String, positional: T::Array[String], pids: ShellProcessIds).void }
|
|
70
|
+
def initialize(environment: Environment.new, name: 'rush', positional: [], pids: ShellProcessIds.new(0, 0))
|
|
71
|
+
@name = name
|
|
72
|
+
@pids = pids
|
|
73
|
+
@variables = ShellVariables.new(environment)
|
|
74
|
+
@traps = TrapTable.new
|
|
75
|
+
@last_status = Status.success
|
|
76
|
+
@last_background_pid = T.let(nil, T.nilable(Integer))
|
|
77
|
+
@loops = LoopNesting.new
|
|
78
|
+
@options = Options.new
|
|
79
|
+
@positional = Positional.new(positional)
|
|
80
|
+
@getopts = GetoptsState.new
|
|
81
|
+
@functions = FunctionTable.new
|
|
82
|
+
@aliases = AliasTable.new
|
|
83
|
+
@command_hash = {}
|
|
84
|
+
seed_variables
|
|
85
|
+
end
|
|
86
|
+
# rubocop:enable Metrics/AbcSize
|
|
87
|
+
|
|
88
|
+
# A stateless view over this state, built on demand like Executor#job_control.
|
|
89
|
+
sig { returns(ShellParameters) }
|
|
90
|
+
def parameters
|
|
91
|
+
ShellParameters.new(self)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
sig do
|
|
95
|
+
type_parameters(:U)
|
|
96
|
+
.params(args: T::Array[String], blk: T.proc.returns(T.type_parameter(:U)))
|
|
97
|
+
.returns(T.type_parameter(:U))
|
|
98
|
+
end
|
|
99
|
+
def with_function_frame(args, &blk)
|
|
100
|
+
FunctionFrame.new(variables: @variables, loops: @loops, positional: @positional).call(args, &blk)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
sig do
|
|
104
|
+
type_parameters(:U)
|
|
105
|
+
.params(blk: T.proc.returns(T.type_parameter(:U)))
|
|
106
|
+
.returns(T.type_parameter(:U))
|
|
107
|
+
end
|
|
108
|
+
def with_loop(&blk)
|
|
109
|
+
@loops.enter
|
|
110
|
+
yield
|
|
111
|
+
ensure
|
|
112
|
+
@loops.leave
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
sig do
|
|
116
|
+
type_parameters(:U)
|
|
117
|
+
.params(blk: T.proc.returns(T.type_parameter(:U)))
|
|
118
|
+
.returns(T.type_parameter(:U))
|
|
119
|
+
end
|
|
120
|
+
def preserve_status(&blk)
|
|
121
|
+
saved = @last_status
|
|
122
|
+
yield
|
|
123
|
+
ensure
|
|
124
|
+
@last_status = T.must(saved)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Flip an ordinary shell option, keeping the side effects options carry in
|
|
128
|
+
# sync (allexport mirrors onto the variable table). Invocation also uses
|
|
129
|
+
# this before an executor exists; live :monitor changes instead belong to
|
|
130
|
+
# JobControl, which owns the signal/terminal/process-group side effects.
|
|
131
|
+
sig { params(option: Symbol, enabled: T::Boolean).void }
|
|
132
|
+
def set_option(option, enabled)
|
|
133
|
+
@options.set(option, enabled)
|
|
134
|
+
@variables.allexport = enabled if option == :allexport
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# The last command's exit status ($?), recorded after each command runs.
|
|
138
|
+
sig { params(status: Status).returns(Status) }
|
|
139
|
+
def record_status(status)
|
|
140
|
+
@last_status = status
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
sig { params(pid: Integer).returns(Integer) }
|
|
144
|
+
def record_background_pid(pid)
|
|
145
|
+
@last_background_pid = pid
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
sig { params(line: Integer).void }
|
|
149
|
+
def record_lineno(line)
|
|
150
|
+
@variables.update_lineno(line)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
private
|
|
154
|
+
|
|
155
|
+
# The variables a POSIX shell is born with: PPID (2.5.3) and getopts'
|
|
156
|
+
# OPTIND, initialized to 1 at shell startup.
|
|
157
|
+
sig { void }
|
|
158
|
+
def seed_variables
|
|
159
|
+
@variables.assign('PPID', @pids.parent.to_s)
|
|
160
|
+
@variables.assign('OPTIND', '1')
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'forwardable'
|
|
5
|
+
|
|
6
|
+
module Rush
|
|
7
|
+
# Semantic facade for shell variables: ordinary get/assign/export/readonly
|
|
8
|
+
# operations plus dynamic local scopes and logical PWD tracking delegated to
|
|
9
|
+
# Scope. Environment and Scope remain storage details behind this API.
|
|
10
|
+
class ShellVariables
|
|
11
|
+
extend T::Sig
|
|
12
|
+
extend Forwardable
|
|
13
|
+
|
|
14
|
+
def_delegators :@environment, :get, :export, :readonly, :unset, :exported, :update_lineno, :with_temporary,
|
|
15
|
+
:validate_assignment
|
|
16
|
+
def_delegators :@scope, :pwd, :move_to, :seed_pwd, :current_pwd,
|
|
17
|
+
:begin_scope, :end_scope, :in_function?, :declare_local
|
|
18
|
+
|
|
19
|
+
sig { params(environment: Environment).void }
|
|
20
|
+
def initialize(environment)
|
|
21
|
+
@environment = environment
|
|
22
|
+
@scope = Scope.new(environment)
|
|
23
|
+
@assignments = Assignments.new(self)
|
|
24
|
+
@allexport = false
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
sig { params(enabled: T::Boolean).void }
|
|
28
|
+
def allexport=(enabled)
|
|
29
|
+
@allexport = enabled == true
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
sig { returns(T::Array[String]) }
|
|
33
|
+
def locale_settings
|
|
34
|
+
%w[LC_COLLATE LC_CTYPE].map { |category| locale(category) }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# The locale name for one category: a non-empty LC_ALL overrides the
|
|
38
|
+
# category-specific variable, then LANG, with the POSIX locale as default.
|
|
39
|
+
sig { params(category: String).returns(String) }
|
|
40
|
+
def locale(category)
|
|
41
|
+
names = ['LC_ALL', category, 'LANG']
|
|
42
|
+
names.filter_map { |name| get(name) }.reject(&:empty?).first || 'C'
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
sig { params(name: String, value: String).returns(String) }
|
|
46
|
+
def assign(name, value)
|
|
47
|
+
@environment.assign(name, value)
|
|
48
|
+
export(name) if @allexport
|
|
49
|
+
value
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
sig { params(text: String).void }
|
|
53
|
+
def export_operand(text)
|
|
54
|
+
export(@assignments.apply(text))
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
sig { params(text: String).void }
|
|
58
|
+
def readonly_operand(text)
|
|
59
|
+
readonly(@assignments.apply(text))
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
sig { params(text: String).void }
|
|
63
|
+
def declare_local_operand(text)
|
|
64
|
+
@assignments.apply_local(text)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
# dash's printsignal(): when a foreground wait reaps a child that died by a
|
|
6
|
+
# signal other than SIGINT/SIGPIPE, the shell writes the strsignal
|
|
7
|
+
# description — "Killed", "Terminated", " (core dumped)" suffixed when the
|
|
8
|
+
# OS recorded a dump — to the stderr in effect at that wait. The wait
|
|
9
|
+
# builtin reports the same for pid/%id operands; a bare `wait` never
|
|
10
|
+
# reports (probed, rush-hkp). Methods are explicit singletons, like
|
|
11
|
+
# Signals (module_function leaves unkillable instance-side copies).
|
|
12
|
+
module SignalReport
|
|
13
|
+
extend T::Sig
|
|
14
|
+
|
|
15
|
+
# dash's exclusions: SIGINT ends interactive commands routinely and
|
|
16
|
+
# SIGPIPE is the normal fate of a producer feeding a finished consumer.
|
|
17
|
+
SILENT = T.let([Signals.number('INT'), Signals.number('PIPE')].freeze, T::Array[Integer])
|
|
18
|
+
|
|
19
|
+
# Report onto err and hand the status back, so reap sites stay
|
|
20
|
+
# expressions. A closed stderr swallows the report, not the status.
|
|
21
|
+
sig { params(status: Status, err: T.untyped).returns(Status) }
|
|
22
|
+
def self.report(status, err)
|
|
23
|
+
err.puts(line(status)) if report?(status)
|
|
24
|
+
status
|
|
25
|
+
rescue Errno::EBADF
|
|
26
|
+
status
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
sig { params(status: Status).returns(T::Boolean) }
|
|
30
|
+
def self.report?(status)
|
|
31
|
+
signal = status.termsig
|
|
32
|
+
!!signal && !SILENT.include?(signal)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
sig { params(status: Status).returns(String) }
|
|
36
|
+
def self.line(status)
|
|
37
|
+
description = Signals.description(T.must(status.termsig))
|
|
38
|
+
status.coredump? ? "#{description} (core dumped)" : description
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
data/lib/rush/signals.rb
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
# Resolves a trap signal spec to a single canonical name. Numbers map through
|
|
6
|
+
# the POSIX signal table, names match case-insensitively and without a "SIG"
|
|
7
|
+
# prefix, and 0/EXIT denote the pseudo-signal that fires when the shell exits.
|
|
8
|
+
# An unknown spec resolves to nil so `trap` can report "bad trap" (dash parity).
|
|
9
|
+
# Methods are explicit singletons: module_function would define a second,
|
|
10
|
+
# instance-side copy that callers never reach — dead weight mutant proved
|
|
11
|
+
# unkillable (journal, rush-211.8).
|
|
12
|
+
module Signals
|
|
13
|
+
extend T::Sig
|
|
14
|
+
|
|
15
|
+
EXIT = 'EXIT'
|
|
16
|
+
NUMBERS = {
|
|
17
|
+
0 => EXIT, 1 => 'HUP', 2 => 'INT', 3 => 'QUIT', 4 => 'ILL', 5 => 'TRAP',
|
|
18
|
+
6 => 'ABRT', 7 => 'BUS', 8 => 'FPE', 9 => 'KILL', 10 => 'USR1',
|
|
19
|
+
11 => 'SEGV', 12 => 'USR2', 13 => 'PIPE', 14 => 'ALRM', 15 => 'TERM',
|
|
20
|
+
17 => 'CHLD', 18 => 'CONT', 19 => 'STOP', 20 => 'TSTP', 21 => 'TTIN',
|
|
21
|
+
22 => 'TTOU', 23 => 'URG', 24 => 'XCPU', 25 => 'XFSZ', 26 => 'VTALRM',
|
|
22
|
+
27 => 'PROF', 28 => 'WINCH', 29 => 'IO', 30 => 'PWR', 31 => 'SYS'
|
|
23
|
+
}.freeze
|
|
24
|
+
NAMES = NUMBERS.invert.freeze
|
|
25
|
+
|
|
26
|
+
sig { params(spec: String).returns(T.nilable(String)) }
|
|
27
|
+
def self.decode(spec)
|
|
28
|
+
return NUMBERS.fetch(Integer(spec, 10), nil) if spec.match?(/\A\d+\z/)
|
|
29
|
+
|
|
30
|
+
name = spec.upcase
|
|
31
|
+
NAMES.key?(name) ? name : nil
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
sig { params(name: String).returns(Integer) }
|
|
35
|
+
def self.number(name)
|
|
36
|
+
NAMES.fetch(name)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# strsignal(3)-style descriptions, as dash's jobs listing prints for a
|
|
40
|
+
# signalled job ("Killed", "Terminated"). Only the common ones are
|
|
41
|
+
# spelled out; the rest fall back to the signal name.
|
|
42
|
+
DESCRIPTIONS = {
|
|
43
|
+
1 => 'Hangup', 2 => 'Interrupt', 3 => 'Quit', 4 => 'Illegal instruction',
|
|
44
|
+
5 => 'Trace/breakpoint trap', 6 => 'Aborted', 7 => 'Bus error',
|
|
45
|
+
8 => 'Floating point exception', 9 => 'Killed', 10 => 'User defined signal 1',
|
|
46
|
+
11 => 'Segmentation fault', 12 => 'User defined signal 2', 13 => 'Broken pipe',
|
|
47
|
+
14 => 'Alarm clock', 15 => 'Terminated'
|
|
48
|
+
}.freeze
|
|
49
|
+
|
|
50
|
+
sig { params(number: Integer).returns(String) }
|
|
51
|
+
def self.description(number)
|
|
52
|
+
DESCRIPTIONS.fetch(number) { NUMBERS.fetch(number, "Signal #{number}") }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# strsignal(3) again, for the stop signals as the jobs listing shows a
|
|
56
|
+
# Stopped entry: plain for ^Z's TSTP, qualified for the others (dash
|
|
57
|
+
# prints the same strings, probed).
|
|
58
|
+
STOPPED = {
|
|
59
|
+
19 => 'Stopped (signal)', 20 => 'Stopped',
|
|
60
|
+
21 => 'Stopped (tty input)', 22 => 'Stopped (tty output)'
|
|
61
|
+
}.freeze
|
|
62
|
+
|
|
63
|
+
sig { params(number: Integer).returns(String) }
|
|
64
|
+
def self.stop_description(number)
|
|
65
|
+
STOPPED.fetch(number, 'Stopped')
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
data/lib/rush/source.rb
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
# Runs non-interactive shell source from `-c`, a script file, or stdin,
|
|
6
|
+
# command by command. It owns the batch-mode error policy: fatal
|
|
7
|
+
# syntax/expansion/builtin errors publish status 2, run the EXIT trap, and
|
|
8
|
+
# stop the remaining input.
|
|
9
|
+
class Source < ProgramSession
|
|
10
|
+
extend T::Sig
|
|
11
|
+
|
|
12
|
+
sig { params(invocation: Invocation, system: SystemCalls, state: ShellState, startup: T.nilable(Startup)).void }
|
|
13
|
+
def initialize(invocation, system, state:, startup: nil)
|
|
14
|
+
@source = T.let(invocation.source, String)
|
|
15
|
+
super(system, state: state, startup: startup)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
sig { returns(Integer) }
|
|
19
|
+
def run
|
|
20
|
+
super
|
|
21
|
+
rescue Error => e
|
|
22
|
+
handle_error(e)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
sig { params(_continuation: T::Boolean).returns(T.nilable(String)) }
|
|
28
|
+
def next_line(_continuation)
|
|
29
|
+
source_line.tap { |line| echo_verbose(line) }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Under `set -v` (verbose) each input line is written to stderr as it is read,
|
|
33
|
+
# before it runs, so a `set -v`/`set +v` toggles which later lines echo (POSIX).
|
|
34
|
+
sig { params(line: T.nilable(String)).void }
|
|
35
|
+
def echo_verbose(line)
|
|
36
|
+
system.stderr.print(line) if line && executor.state.options.on?(:verbose)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
sig { returns(T.nilable(String)) }
|
|
40
|
+
def source_line
|
|
41
|
+
source_lines.next
|
|
42
|
+
rescue StopIteration
|
|
43
|
+
nil
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
sig { returns(Enumerator) }
|
|
47
|
+
def source_lines
|
|
48
|
+
@source_lines ||= source.each_line
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# A `return` not caught by a function or dot script acts like `exit` with
|
|
52
|
+
# that code in a non-interactive shell (POSIX): re-raise as ExitSignal so it
|
|
53
|
+
# settles the status and fires the EXIT trap. A stray break/continue no longer
|
|
54
|
+
# reaches here — with no enclosing loop the builtin is a no-op.
|
|
55
|
+
sig { params(program: AST::List).void }
|
|
56
|
+
def execute(program)
|
|
57
|
+
super
|
|
58
|
+
rescue ReturnSignal => e
|
|
59
|
+
raise ExitSignal, e.code
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
sig { params(error: Error).returns(Integer) }
|
|
63
|
+
def handle_error(error)
|
|
64
|
+
decision = ErrorPolicy.decision(:batch, error)
|
|
65
|
+
return executor.trap_runner.run_exit_trap(130) if decision == :interrupt130
|
|
66
|
+
return abort_with(error) if decision == :abort2
|
|
67
|
+
|
|
68
|
+
raise error
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# A fatal error (syntax/expansion/readonly): report it, publish 2 as $?, then
|
|
72
|
+
# fire the EXIT trap (which may override the code via `exit`) — like dash.
|
|
73
|
+
sig { params(error: StandardError).returns(Integer) }
|
|
74
|
+
def abort_with(error)
|
|
75
|
+
report(error)
|
|
76
|
+
executor.trap_runner.run_exit_trap(2)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
sig { returns(String) }
|
|
80
|
+
attr_reader :source
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
# Tracks absolute source line numbers while ProgramReader buffers one complete
|
|
6
|
+
# program. Each Lexer sees only the current buffer, so the counter supplies the
|
|
7
|
+
# offset that makes word/source-line metadata absolute within the caller's
|
|
8
|
+
# source stream.
|
|
9
|
+
class SourceLineCounter
|
|
10
|
+
extend T::Sig
|
|
11
|
+
|
|
12
|
+
sig { void }
|
|
13
|
+
def initialize
|
|
14
|
+
@line_number = 0
|
|
15
|
+
@buffer_start = 1
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
sig { params(line: String).void }
|
|
19
|
+
def start(line)
|
|
20
|
+
@buffer_start = @line_number + 1
|
|
21
|
+
advance(line)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
sig { params(line: String).void }
|
|
25
|
+
def continue(line)
|
|
26
|
+
advance(line)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
sig { returns(Integer) }
|
|
30
|
+
def offset
|
|
31
|
+
@buffer_start - 1
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
sig { params(line: String).void }
|
|
37
|
+
def advance(line)
|
|
38
|
+
@line_number += line.each_line.count
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
# Runs shell text command by command in the current shell — the engine shared
|
|
6
|
+
# by `eval` and `.`. Reading and executing incrementally (via ProgramReader)
|
|
7
|
+
# lets each command shape how the next is parsed: an `alias` or function
|
|
8
|
+
# defined on one line takes effect on the following lines, and a syntax error
|
|
9
|
+
# only stops the commands after it (the earlier ones have already run), exactly
|
|
10
|
+
# as dash reads its input. The result starts at success and is updated by each
|
|
11
|
+
# non-empty command, so empty input is status 0 while $? stays live for the
|
|
12
|
+
# commands themselves (POSIX: eval and dot inherit the current $?).
|
|
13
|
+
class SourceRunner
|
|
14
|
+
extend T::Sig
|
|
15
|
+
|
|
16
|
+
sig { params(executor: Executor, text: String).void }
|
|
17
|
+
def initialize(executor, text)
|
|
18
|
+
@executor = executor
|
|
19
|
+
@lines = text.each_line.to_a
|
|
20
|
+
@reader = ProgramReader.new(aliases: executor.state.aliases) { @lines.shift }
|
|
21
|
+
@result = Status.success
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
sig { returns(Status) }
|
|
25
|
+
def run
|
|
26
|
+
loop { break if advance == :eof }
|
|
27
|
+
@result
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
# Read the next complete program and run it, returning it (or :eof) so `run`
|
|
33
|
+
# knows when the input is exhausted.
|
|
34
|
+
sig { returns(T.any(AST::List, Symbol)) }
|
|
35
|
+
def advance
|
|
36
|
+
program = @reader.next_program
|
|
37
|
+
execute(T.cast(program, AST::List)) unless program == :eof
|
|
38
|
+
program
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
sig { params(program: AST::List).void }
|
|
42
|
+
def execute(program)
|
|
43
|
+
@executor.run(program)
|
|
44
|
+
@result = @executor.state.last_status unless program.empty?
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|