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,37 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
# Calls a shell function: bind the arguments as the positional parameters, run
|
|
6
|
+
# the body in the current shell, and restore the previous positionals. A scope
|
|
7
|
+
# is opened so `local` declarations are undone on return. `return` unwinds to
|
|
8
|
+
# here as a ReturnSignal.
|
|
9
|
+
class FunctionRunner
|
|
10
|
+
extend T::Sig
|
|
11
|
+
|
|
12
|
+
sig { params(executor: Executor, body: AST::Node, args: T::Array[String]).void }
|
|
13
|
+
def initialize(executor, body, args)
|
|
14
|
+
@executor = executor
|
|
15
|
+
@state = executor.state
|
|
16
|
+
@body = body
|
|
17
|
+
@args = args
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# A function body runs inside a ShellState-owned function frame: locals are
|
|
21
|
+
# scoped, positionals are rebound, and loop depth is reset (POSIX 2.9.5).
|
|
22
|
+
sig { returns(Status) }
|
|
23
|
+
def call
|
|
24
|
+
@state.with_function_frame(@args) { invoke }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
sig { returns(Status) }
|
|
30
|
+
def invoke
|
|
31
|
+
@executor.run(@body)
|
|
32
|
+
@state.last_status
|
|
33
|
+
rescue ReturnSignal => e
|
|
34
|
+
Status.new(e.code)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
# Name -> function body (an AST node), defined by function definitions and
|
|
6
|
+
# looked up during command dispatch.
|
|
7
|
+
class FunctionTable
|
|
8
|
+
extend T::Sig
|
|
9
|
+
|
|
10
|
+
sig { void }
|
|
11
|
+
def initialize
|
|
12
|
+
@functions = {}
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
sig { params(name: String, body: AST::Node).returns(AST::Node) }
|
|
16
|
+
def define(name, body)
|
|
17
|
+
@functions[name] = body
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
sig { params(name: String).returns(T.nilable(AST::Node)) }
|
|
21
|
+
def undefine(name)
|
|
22
|
+
@functions.delete(name)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
sig { params(name: String).returns(AST::Node) }
|
|
26
|
+
def fetch(name)
|
|
27
|
+
@functions.fetch(name)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
sig { params(name: String).returns(T::Boolean) }
|
|
31
|
+
def key?(name)
|
|
32
|
+
@functions.key?(name)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
# Normal vs silent getopts error-mode formatting for bad options and missing
|
|
6
|
+
# arguments.
|
|
7
|
+
class GetoptsErrorMode
|
|
8
|
+
extend T::Sig
|
|
9
|
+
|
|
10
|
+
sig { returns(String) }
|
|
11
|
+
attr_reader :missing_name
|
|
12
|
+
|
|
13
|
+
sig { params(mode: Symbol).void }
|
|
14
|
+
def initialize(mode)
|
|
15
|
+
@missing_name = mode == :silent ? ':' : '?'
|
|
16
|
+
@keep_optarg = mode == :silent
|
|
17
|
+
@diagnostic = mode == :normal
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
sig { params(value: String).returns(T.nilable(String)) }
|
|
21
|
+
def optarg(value)
|
|
22
|
+
value if @keep_optarg
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
sig { params(text: String).returns(T.nilable(String)) }
|
|
26
|
+
def message(text)
|
|
27
|
+
text if @diagnostic
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# The getopts optstring operand (POSIX `getopts optstring name [arg...]`):
|
|
32
|
+
# a leading colon selects silent error reporting, then each option letter,
|
|
33
|
+
# `:`-suffixed when it requires an argument. Owns which letters are legal
|
|
34
|
+
# and which take arguments; the parser keeps the cursor choreography.
|
|
35
|
+
class Optstring
|
|
36
|
+
extend T::Sig
|
|
37
|
+
|
|
38
|
+
sig { returns(GetoptsErrorMode) }
|
|
39
|
+
attr_reader :error_mode
|
|
40
|
+
|
|
41
|
+
sig { params(text: String).void }
|
|
42
|
+
def initialize(text)
|
|
43
|
+
@error_mode = GetoptsErrorMode.new(text.start_with?(':') ? :silent : :normal)
|
|
44
|
+
@letters = text.delete_prefix(':')
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
sig { params(option: String).returns(T::Boolean) }
|
|
48
|
+
def valid?(option)
|
|
49
|
+
!!(option != ':' && index(option))
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
sig { params(option: String).returns(T::Boolean) }
|
|
53
|
+
def requires_argument?(option)
|
|
54
|
+
position = index(option)
|
|
55
|
+
!!(position && @letters[position + 1] == ':')
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
sig { params(option: String).returns(T.nilable(Integer)) }
|
|
61
|
+
def index(option)
|
|
62
|
+
@letters.index(option)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Pure parser for one POSIX getopts step. It walks the argv words and the
|
|
67
|
+
# hidden cluster cursor against the Optstring's semantics; the builtin
|
|
68
|
+
# applies the returned variable updates.
|
|
69
|
+
class GetoptsParser
|
|
70
|
+
extend T::Sig
|
|
71
|
+
|
|
72
|
+
# Sentinel optarg: leave OPTARG untouched (a real optarg is always a
|
|
73
|
+
# String, so a Symbol can never collide with one).
|
|
74
|
+
KEEP = :keep
|
|
75
|
+
# The variable updates and status produced by one getopts step.
|
|
76
|
+
Result = Data.define(:name, :optarg, :optind, :message, :exitstatus)
|
|
77
|
+
# The option character plus the argv word it came from.
|
|
78
|
+
Option = Data.define(:argument, :name)
|
|
79
|
+
|
|
80
|
+
sig { params(state: GetoptsState, optstring: String, arguments: T::Array[String], optind: Integer).void }
|
|
81
|
+
def initialize(state:, optstring:, arguments:, optind:)
|
|
82
|
+
@state = state
|
|
83
|
+
@optstring = Optstring.new(optstring)
|
|
84
|
+
@error_mode = @optstring.error_mode
|
|
85
|
+
@arguments = arguments
|
|
86
|
+
@state.prepare(arguments, optind)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
sig { returns(Result) }
|
|
90
|
+
def call
|
|
91
|
+
arg = @state.current(@arguments)
|
|
92
|
+
return finish if !arg || arg == '-' || !arg.start_with?('-')
|
|
93
|
+
return finish_double_dash if arg == '--'
|
|
94
|
+
|
|
95
|
+
dispatch(option(arg))
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
private
|
|
99
|
+
|
|
100
|
+
sig { params(option: Option).returns(Result) }
|
|
101
|
+
def dispatch(option)
|
|
102
|
+
name = option.name
|
|
103
|
+
return invalid(option) unless @optstring.valid?(name)
|
|
104
|
+
return argument_value(option) if @optstring.requires_argument?(name)
|
|
105
|
+
|
|
106
|
+
found(name, '')
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
sig { params(name: String, value: String).returns(Result) }
|
|
110
|
+
def found(name, value)
|
|
111
|
+
consume_found(value)
|
|
112
|
+
result(name, value, nil, 0)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
sig { params(option: Option).returns(Result) }
|
|
116
|
+
def invalid(option)
|
|
117
|
+
name = option.name
|
|
118
|
+
@state.consume_option(option.argument)
|
|
119
|
+
error_result('?', name, "Illegal option -#{name}")
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
sig { params(option: Option).returns(Result) }
|
|
123
|
+
def missing(option)
|
|
124
|
+
consume_missing(option.argument)
|
|
125
|
+
error_result(@error_mode.missing_name, option.name, "No arg for -#{option.name} option")
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
sig { returns(Result) }
|
|
129
|
+
def finish
|
|
130
|
+
@state.finish
|
|
131
|
+
result('?', KEEP, nil, 1)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
sig { returns(Result) }
|
|
135
|
+
def finish_double_dash
|
|
136
|
+
@state.finish_double_dash
|
|
137
|
+
result('?', KEEP, nil, 1)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
sig { params(value: String).void }
|
|
141
|
+
def consume_found(value)
|
|
142
|
+
current = T.must(@state.current(@arguments))
|
|
143
|
+
return @state.consume_option(current) unless @optstring.requires_argument?(@state.option(current))
|
|
144
|
+
|
|
145
|
+
attached_value?(current, value) ? @state.consume_attached_argument : @state.consume_detached_argument
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
sig { params(_argument: String).void }
|
|
149
|
+
def consume_missing(_argument)
|
|
150
|
+
@state.consume_attached_argument
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
sig { params(option: Option).returns(Result) }
|
|
154
|
+
def argument_value(option)
|
|
155
|
+
return found(option.name, attached_argument(option.argument)) if attached?(option.argument)
|
|
156
|
+
|
|
157
|
+
next_argument ? found(option.name, T.must(next_argument)) : missing(option)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
sig { params(name: String, silent_optarg: String, normal_message: String).returns(Result) }
|
|
161
|
+
def error_result(name, silent_optarg, normal_message)
|
|
162
|
+
result(name, @error_mode.optarg(silent_optarg), @error_mode.message(normal_message), 0)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
sig { params(name: String, optarg: T.untyped, message: T.nilable(String), exitstatus: Integer).returns(Result) }
|
|
166
|
+
def result(name, optarg, message, exitstatus)
|
|
167
|
+
Result.new(name: name, optarg: optarg, optind: @state.optind, message: message, exitstatus: exitstatus)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
sig { params(argument: String).returns(Option) }
|
|
171
|
+
def option(argument)
|
|
172
|
+
Option.new(argument: argument, name: @state.option(argument))
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
sig { params(argument: String).returns(String) }
|
|
176
|
+
def attached_argument(argument)
|
|
177
|
+
argument[(@state.cursor + 1)..].to_s
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
sig { params(argument: String, value: String).returns(T::Boolean) }
|
|
181
|
+
def attached_value?(argument, value)
|
|
182
|
+
!value.empty? && attached?(argument)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
sig { params(argument: String).returns(T::Boolean) }
|
|
186
|
+
def attached?(argument)
|
|
187
|
+
@state.cursor < argument.length - 1
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
sig { returns(T.nilable(String)) }
|
|
191
|
+
def next_argument
|
|
192
|
+
@arguments[@state.current_index + 1]
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
# Cursor state for POSIX getopts. OPTIND is public and 1-based, but while
|
|
6
|
+
# parsing an option cluster such as -ab dash already publishes OPTIND=2 after
|
|
7
|
+
# returning -a; the private cursor still points at b in argv[0]. This object
|
|
8
|
+
# keeps that hidden intra-argument cursor and resets it when OPTIND or the
|
|
9
|
+
# argument vector changes.
|
|
10
|
+
class GetoptsState
|
|
11
|
+
extend T::Sig
|
|
12
|
+
|
|
13
|
+
sig { returns(Integer) }
|
|
14
|
+
attr_reader :current_index, :cursor, :optind
|
|
15
|
+
|
|
16
|
+
sig { void }
|
|
17
|
+
def initialize
|
|
18
|
+
@signature = nil
|
|
19
|
+
@current_index = 0
|
|
20
|
+
@cursor = 1
|
|
21
|
+
@optind = 1
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
sig { params(arguments: T::Array[String], optind: Integer).void }
|
|
25
|
+
def prepare(arguments, optind)
|
|
26
|
+
signature = arguments.join("\0")
|
|
27
|
+
reset(signature, optind) if signature != @signature || optind != @optind
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
sig { params(arguments: T::Array[String]).returns(T.nilable(String)) }
|
|
31
|
+
def current(arguments)
|
|
32
|
+
arguments[@current_index]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
sig { params(argument: String).returns(String) }
|
|
36
|
+
def option(argument)
|
|
37
|
+
T.must(argument[@cursor])
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
sig { params(argument: String).void }
|
|
41
|
+
def consume_option(argument)
|
|
42
|
+
cluster_remains?(argument) ? consume_cluster : consume_final_option
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
sig { params(argument: String).returns(T::Boolean) }
|
|
46
|
+
def cluster_remains?(argument)
|
|
47
|
+
@cursor < argument.length - 1
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
sig { void }
|
|
51
|
+
def consume_cluster
|
|
52
|
+
@cursor += 1
|
|
53
|
+
@optind = @current_index + 2
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
sig { void }
|
|
57
|
+
def consume_final_option
|
|
58
|
+
@current_index += 1
|
|
59
|
+
@cursor = 1
|
|
60
|
+
publish
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
sig { void }
|
|
64
|
+
def consume_attached_argument
|
|
65
|
+
@current_index += 1
|
|
66
|
+
@cursor = 1
|
|
67
|
+
publish
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
sig { void }
|
|
71
|
+
def consume_detached_argument
|
|
72
|
+
@current_index += 2
|
|
73
|
+
@cursor = 1
|
|
74
|
+
publish
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
sig { void }
|
|
78
|
+
def finish
|
|
79
|
+
@cursor = 1
|
|
80
|
+
publish
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
sig { void }
|
|
84
|
+
def finish_double_dash
|
|
85
|
+
@current_index += 1
|
|
86
|
+
finish
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
sig { params(signature: String, optind: Integer).void }
|
|
92
|
+
def reset(signature, optind)
|
|
93
|
+
@signature = signature
|
|
94
|
+
@current_index = [optind - 1, 0].max
|
|
95
|
+
@cursor = 1
|
|
96
|
+
publish
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
sig { void }
|
|
100
|
+
def publish
|
|
101
|
+
@optind = @current_index + 1
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
# A here-document redirect target. The lexer creates it when it sees `<<word`
|
|
6
|
+
# (recording the delimiter, whether it was quoted, and the `<<-` tab strip),
|
|
7
|
+
# then fills #body — an AST::Word — once it collects the body lines at the next
|
|
8
|
+
# newline. It quacks like a Word (responds to #segments) so the expansion
|
|
9
|
+
# pipeline expands the body as a single field at execution time.
|
|
10
|
+
class HereDoc
|
|
11
|
+
extend T::Sig
|
|
12
|
+
|
|
13
|
+
# Immutable metadata captured from the delimiter token.
|
|
14
|
+
Definition = Data.define(:delimiter, :quoted, :strip, :source_line)
|
|
15
|
+
|
|
16
|
+
sig { returns(AST::Word) }
|
|
17
|
+
attr_reader :body
|
|
18
|
+
|
|
19
|
+
sig { params(delimiter: String, quoted: T::Boolean, strip: T::Boolean, source_line: Integer).void }
|
|
20
|
+
def initialize(delimiter:, quoted:, strip:, source_line: 1)
|
|
21
|
+
@definition = Definition.new(delimiter, quoted, strip, source_line)
|
|
22
|
+
@body = AST::Word.new([], source_line: source_line)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
sig { returns(String) }
|
|
26
|
+
def delimiter
|
|
27
|
+
@definition.delimiter
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
sig { returns(T::Boolean) }
|
|
31
|
+
def quoted
|
|
32
|
+
@definition.quoted
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
sig { returns(T::Boolean) }
|
|
36
|
+
def strip
|
|
37
|
+
@definition.strip
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
sig { returns(Integer) }
|
|
41
|
+
def source_line
|
|
42
|
+
@definition.source_line
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# The lexer fills the body — an AST::Word — once it has gathered the body
|
|
46
|
+
# lines at the next newline; it is the only writer, so #body stays read-only.
|
|
47
|
+
sig { params(body: AST::Word).void }
|
|
48
|
+
def fill(body)
|
|
49
|
+
@body = body
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
sig { returns(T::Array[AST::WordSegment[T.untyped]]) }
|
|
53
|
+
def segments
|
|
54
|
+
@body.segments
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
# The interactive shell's default signal dispositions (POSIX: "ignored in a
|
|
6
|
+
# manner that allows trap to work"; dash is the oracle): SIGINT interrupts
|
|
7
|
+
# the current line — its handler raises Interrupted, which the session turns
|
|
8
|
+
# into $?=130 and a fresh prompt — while SIGQUIT and SIGTERM are swallowed by
|
|
9
|
+
# no-op handlers. All three are handler blocks rather than SIG_IGN, so
|
|
10
|
+
# children spawned via exec revert to the OS defaults. `trap` overrides
|
|
11
|
+
# them; `trap -` and subshells restore through TrapRunner's base table.
|
|
12
|
+
class InteractiveSignals
|
|
13
|
+
extend T::Sig
|
|
14
|
+
|
|
15
|
+
# Non-lambda procs: Signal.trap calls handlers with the signal number, and
|
|
16
|
+
# a strict lambda would die on the extra argument.
|
|
17
|
+
sig { params(executor: Executor).void }
|
|
18
|
+
def self.install(executor)
|
|
19
|
+
executor.trap_runner.install_base(
|
|
20
|
+
'INT' => proc { raise Interrupted, 'interrupted' },
|
|
21
|
+
'QUIT' => proc {},
|
|
22
|
+
'TERM' => proc {}
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
# Consumes the leading -/+ option clusters of an `sh` argv — the set-builtin
|
|
6
|
+
# letters plus -c, -i, -s and `-o name`, ending at `--` or the first operand —
|
|
7
|
+
# leaving the operands in place. Exposes the explicit option settings and
|
|
8
|
+
# whether -c was given.
|
|
9
|
+
class InvocationFlags
|
|
10
|
+
extend T::Sig
|
|
11
|
+
|
|
12
|
+
FLAGS = Options::LETTERS.merge('i' => :interactive, 's' => :stdin, 'l' => :login).freeze
|
|
13
|
+
|
|
14
|
+
sig { returns(T::Hash[Symbol, T::Boolean]) }
|
|
15
|
+
attr_reader :settings
|
|
16
|
+
|
|
17
|
+
sig { returns(T::Boolean) }
|
|
18
|
+
attr_reader :command
|
|
19
|
+
|
|
20
|
+
sig { params(argv: T::Array[String]).void }
|
|
21
|
+
def initialize(argv)
|
|
22
|
+
@argv = argv
|
|
23
|
+
@settings = T.let(
|
|
24
|
+
{}, #: Hash[Symbol, bool]
|
|
25
|
+
T::Hash[Symbol, T::Boolean]
|
|
26
|
+
)
|
|
27
|
+
@command = T.let(false, T::Boolean)
|
|
28
|
+
consume_clusters
|
|
29
|
+
# `--` or the obsolescent lone `-` ends the options and is consumed
|
|
30
|
+
# (POSIX sh synopsis; dash-verified: `sh -` reads stdin, `sh - file`
|
|
31
|
+
# runs the file, while a `-` after the -c string stays $0).
|
|
32
|
+
@argv.shift if ['--', '-'].include?(@argv.first)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
sig { void }
|
|
38
|
+
def consume_clusters
|
|
39
|
+
while (cluster = OptionCluster.parse(@argv.first))
|
|
40
|
+
@argv.shift
|
|
41
|
+
apply_cluster(cluster)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
sig { params(cluster: OptionCluster).void }
|
|
46
|
+
def apply_cluster(cluster)
|
|
47
|
+
cluster.each_letter { |letter, sign| apply_letter(letter, sign) }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
sig { params(letter: String, sign: String).void }
|
|
51
|
+
def apply_letter(letter, sign)
|
|
52
|
+
case letter
|
|
53
|
+
when 'c' then enable_command(sign)
|
|
54
|
+
when 'o' then apply_long(sign, @argv.shift)
|
|
55
|
+
else apply_flag(letter, sign)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
sig { params(sign: String).void }
|
|
60
|
+
def enable_command(sign)
|
|
61
|
+
raise InvocationError, "Illegal option #{sign}c" unless sign == '-'
|
|
62
|
+
|
|
63
|
+
@command = true
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
sig { params(sign: String, option_name: T.nilable(String)).void }
|
|
67
|
+
def apply_long(sign, option_name)
|
|
68
|
+
raise InvocationError, "#{sign}o requires an argument" unless option_name
|
|
69
|
+
|
|
70
|
+
option = Options::LONG.fetch(option_name) do
|
|
71
|
+
raise InvocationError, "Illegal option #{sign}o #{option_name}"
|
|
72
|
+
end
|
|
73
|
+
@settings[option] = sign == '-'
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
sig { params(letter: String, sign: String).void }
|
|
77
|
+
def apply_flag(letter, sign)
|
|
78
|
+
option = FLAGS.fetch(letter) { raise InvocationError, "Illegal option #{sign}#{letter}" }
|
|
79
|
+
@settings[option] = sign == '-'
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Parsed `sh` invocation: option flags (via InvocationFlags), then operands.
|
|
84
|
+
# Picks the program input — -c command string, script file, or stdin — and
|
|
85
|
+
# decides POSIX interactivity (-i, or no operands, no -c, and stdin and
|
|
86
|
+
# stderr both attached to a terminal), building the session it all implies.
|
|
87
|
+
class Invocation
|
|
88
|
+
extend T::Sig
|
|
89
|
+
|
|
90
|
+
sig { params(argv: T::Array[String], system: SystemCalls).void }
|
|
91
|
+
def initialize(argv, system)
|
|
92
|
+
@system = system
|
|
93
|
+
rest = argv.dup
|
|
94
|
+
@flags = T.let(InvocationFlags.new(rest), InvocationFlags)
|
|
95
|
+
@operands = T.let(rest, T::Array[String])
|
|
96
|
+
@input = T.let(build_input, T.any(CommandInput, ScriptInput, StdinInput))
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# The session this invocation implies, wired to its shell state and
|
|
100
|
+
# startup files (login profiles, interactive ENV).
|
|
101
|
+
sig { returns(ProgramSession) }
|
|
102
|
+
def session
|
|
103
|
+
state = shell_state
|
|
104
|
+
return Repl.new(@system, state: state, startup: startup) if repl?
|
|
105
|
+
|
|
106
|
+
Source.new(self, @system, state: state, startup: startup)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
sig { returns(T::Boolean) }
|
|
110
|
+
def repl?
|
|
111
|
+
interactive? && stdin_source?
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
sig { returns(T::Boolean) }
|
|
115
|
+
def interactive?
|
|
116
|
+
@flags.settings.fetch(:interactive) { auto_interactive? }
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# A login shell: -l, or invoked with a leading '-' in the program name
|
|
120
|
+
# (how login(1) execs a shell). dash accepts -l the same way; POSIX leaves
|
|
121
|
+
# login files unspecified, so the oracle decides.
|
|
122
|
+
sig { returns(T::Boolean) }
|
|
123
|
+
def login?
|
|
124
|
+
@flags.settings.fetch(:login) { @system.program_name.start_with?('-') }
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
sig { returns(String) }
|
|
128
|
+
def name
|
|
129
|
+
@input.name
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
sig { returns(T::Array[String]) }
|
|
133
|
+
def positionals
|
|
134
|
+
@input.positionals
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# The option flags the session starts with: the explicit -/+ settings plus
|
|
138
|
+
# the ones POSIX derives implicitly (s for a stdin source, i when
|
|
139
|
+
# interactive, and monitor defaulting to interactivity — dash's unset
|
|
140
|
+
# mflag resolves to iflag, probed: `dash -i` on a tty reports smi), so $-
|
|
141
|
+
# reports them like dash does.
|
|
142
|
+
sig { returns(T::Hash[Symbol, T::Boolean]) }
|
|
143
|
+
def shell_flags
|
|
144
|
+
{ monitor: interactive? }.merge(@flags.settings).merge(stdin: stdin_source?, interactive: interactive?)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# The program text (read lazily: a REPL session never gets here).
|
|
148
|
+
sig { returns(String) }
|
|
149
|
+
def source
|
|
150
|
+
@input.source
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
private
|
|
154
|
+
|
|
155
|
+
sig { returns(T.any(CommandInput, ScriptInput, StdinInput)) }
|
|
156
|
+
def build_input
|
|
157
|
+
return CommandInput.new(@operands) if @flags.command
|
|
158
|
+
return StdinInput.new(@operands, @system) if stdin_source?
|
|
159
|
+
|
|
160
|
+
ScriptInput.new(@operands, @system)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
sig { returns(ShellState) }
|
|
164
|
+
def shell_state
|
|
165
|
+
state = ShellState.new(name: name, positional: positionals, pids: ShellProcessIds.for(@system))
|
|
166
|
+
shell_flags.each { |option, enabled| state.set_option(option, enabled) }
|
|
167
|
+
state
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
sig { returns(Startup) }
|
|
171
|
+
def startup
|
|
172
|
+
Startup.new(login: login?, interactive: interactive?)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# POSIX sh: with no -i, interactive only when reading stdin implicitly on a
|
|
176
|
+
# terminal — no operands, and stdin and stderr both ttys. (-c with no
|
|
177
|
+
# command string never gets to run: #source rejects it first.)
|
|
178
|
+
sig { returns(T::Boolean) }
|
|
179
|
+
def auto_interactive?
|
|
180
|
+
@operands.empty? && @system.tty? && @system.stderr_tty?
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
sig { returns(T::Boolean) }
|
|
184
|
+
def stdin_source?
|
|
185
|
+
return false if @flags.command
|
|
186
|
+
|
|
187
|
+
@flags.settings.fetch(:stdin) { @operands.empty? }
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|