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,258 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
module Builtins
|
|
6
|
+
# One ulimit resource: option flag, dash list label, SystemCalls key, and display/set scale.
|
|
7
|
+
UlimitResource = Data.define(:flag, :label, :resource, :scale)
|
|
8
|
+
# Parsed ulimit invocation: list-all flag, selected resource/limit side, and optional value.
|
|
9
|
+
UlimitRequest = Data.define(:all, :resource, :target, :explicit_target, :value)
|
|
10
|
+
# Parser error code plus optional option text for diagnostics.
|
|
11
|
+
UlimitParseError = Data.define(:kind, :detail)
|
|
12
|
+
ULIMIT_RESOURCE_LIST = [
|
|
13
|
+
UlimitResource.new('t', 'time(seconds)', :cpu, 1),
|
|
14
|
+
UlimitResource.new('f', 'file(blocks)', :fsize, 512),
|
|
15
|
+
UlimitResource.new('d', 'data(kbytes)', :data, 1024),
|
|
16
|
+
UlimitResource.new('s', 'stack(kbytes)', :stack, 1024),
|
|
17
|
+
UlimitResource.new('c', 'coredump(blocks)', :core, 512),
|
|
18
|
+
UlimitResource.new('m', 'memory(kbytes)', :rss, 1024),
|
|
19
|
+
UlimitResource.new('l', 'locked memory(kbytes)', :memlock, 1024),
|
|
20
|
+
UlimitResource.new('p', 'process', :nproc, 1),
|
|
21
|
+
UlimitResource.new('n', 'nofiles', :nofile, 1),
|
|
22
|
+
UlimitResource.new('v', 'vmemory(kbytes)', :as, 1024),
|
|
23
|
+
UlimitResource.new('w', 'locks', :locks, 1),
|
|
24
|
+
UlimitResource.new('r', 'rtprio', :rtprio, 1)
|
|
25
|
+
].freeze
|
|
26
|
+
ULIMIT_RESOURCES = ULIMIT_RESOURCE_LIST.to_h { |resource| [resource.flag, resource] }.freeze
|
|
27
|
+
ULIMIT_DEFAULT_RESOURCE = T.let(ULIMIT_RESOURCES.fetch('f'), UlimitResource)
|
|
28
|
+
ULIMIT_TARGETS = { 'H' => :hard, 'S' => :soft }.freeze
|
|
29
|
+
|
|
30
|
+
# Mutable parser for ulimit's compact option clusters (`-Hn`, `-Sn`, `-a`).
|
|
31
|
+
# It returns either a UlimitRequest or a small parse-error value that the
|
|
32
|
+
# builtin turns into the user-facing diagnostic.
|
|
33
|
+
class UlimitRequestBuilder
|
|
34
|
+
extend T::Sig
|
|
35
|
+
|
|
36
|
+
sig { params(resources: T::Hash[String, UlimitResource], default: UlimitResource).void }
|
|
37
|
+
def initialize(resources, default)
|
|
38
|
+
@resources = resources
|
|
39
|
+
@state = { all: false, resource: default, target: :soft, explicit_target: false }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
sig { params(args: T::Array[String]).returns(T.any(UlimitRequest, UlimitParseError)) }
|
|
43
|
+
def call(args)
|
|
44
|
+
rest = args.dup
|
|
45
|
+
value = extract_value(rest)
|
|
46
|
+
return error(:too_many) if rest.any? { |arg| !arg.start_with?('-') }
|
|
47
|
+
|
|
48
|
+
parse_options(rest, value)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
sig { params(args: T::Array[String]).returns(T.nilable(String)) }
|
|
54
|
+
def extract_value(args)
|
|
55
|
+
last = args.last
|
|
56
|
+
args.pop if last && !last.start_with?('-')
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
sig { params(args: T::Array[String], value: T.nilable(String)).returns(T.any(UlimitRequest, UlimitParseError)) }
|
|
60
|
+
def parse_options(args, value)
|
|
61
|
+
args.each { |arg| apply(arg).tap { |error| return error if error } }
|
|
62
|
+
return error(:too_many) if @state.fetch(:all) && value
|
|
63
|
+
|
|
64
|
+
request(value)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
sig { params(arg: String).returns(T.nilable(UlimitParseError)) }
|
|
68
|
+
def apply(arg)
|
|
69
|
+
return error(:illegal_option, arg) if arg == '-'
|
|
70
|
+
|
|
71
|
+
apply_flags(arg.delete_prefix('-'))
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
sig { params(flags: String).returns(T.nilable(UlimitParseError)) }
|
|
75
|
+
def apply_flags(flags)
|
|
76
|
+
flags.each_char do |flag|
|
|
77
|
+
apply_or_error(flag).tap { |error| return error if error }
|
|
78
|
+
end
|
|
79
|
+
nil
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
sig { params(flag: String).returns(T::Boolean) }
|
|
83
|
+
def known_flag?(flag)
|
|
84
|
+
flag == 'a' || @resources.key?(flag) || ULIMIT_TARGETS.key?(flag)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
sig { params(flag: String).returns(T.nilable(UlimitParseError)) }
|
|
88
|
+
def apply_or_error(flag)
|
|
89
|
+
return return_error(flag) unless known_flag?(flag)
|
|
90
|
+
|
|
91
|
+
apply_flag(flag)
|
|
92
|
+
nil
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
sig { params(flag: String).returns(UlimitParseError) }
|
|
96
|
+
def return_error(flag)
|
|
97
|
+
error(:illegal_option, "-#{flag}")
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
sig { params(flag: String).void }
|
|
101
|
+
def apply_flag(flag)
|
|
102
|
+
@state[:all] = true if flag == 'a'
|
|
103
|
+
@state[:resource] = @resources.fetch(flag) if @resources.key?(flag)
|
|
104
|
+
apply_target(flag) if ULIMIT_TARGETS.key?(flag)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
sig { params(flag: String).void }
|
|
108
|
+
def apply_target(flag)
|
|
109
|
+
@state[:target] = ULIMIT_TARGETS.fetch(flag)
|
|
110
|
+
@state[:explicit_target] = true
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
sig { params(value: T.nilable(String)).returns(UlimitRequest) }
|
|
114
|
+
def request(value)
|
|
115
|
+
UlimitRequest.new(
|
|
116
|
+
@state.fetch(:all), @state.fetch(:resource), @state.fetch(:target), @state.fetch(:explicit_target), value
|
|
117
|
+
)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
sig { params(kind: Symbol, detail: T.nilable(String)).returns(UlimitParseError) }
|
|
121
|
+
def error(kind, detail = nil)
|
|
122
|
+
UlimitParseError.new(kind, detail)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# `ulimit [-H|-S] [-a|-resource] [limit]` — query or set process resource
|
|
127
|
+
# limits. Values are printed in dash/POSIX units (blocks or kbytes for byte
|
|
128
|
+
# resources); `unlimited` maps to the platform's infinity value.
|
|
129
|
+
class Ulimit < Base
|
|
130
|
+
extend T::Sig
|
|
131
|
+
|
|
132
|
+
sig { returns(Status) }
|
|
133
|
+
def call
|
|
134
|
+
request = parse_request
|
|
135
|
+
return parse_error(request) if request.is_a?(UlimitParseError)
|
|
136
|
+
|
|
137
|
+
run_request(request)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
private
|
|
141
|
+
|
|
142
|
+
sig { returns(T.any(UlimitRequest, UlimitParseError)) }
|
|
143
|
+
def parse_request
|
|
144
|
+
UlimitRequestBuilder.new(ULIMIT_RESOURCES, ULIMIT_DEFAULT_RESOURCE).call(operands)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
sig { params(error: UlimitParseError).returns(Status) }
|
|
148
|
+
def parse_error(error)
|
|
149
|
+
return too_many if error.kind == :too_many
|
|
150
|
+
|
|
151
|
+
illegal_option(T.must(error.detail))
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
sig { params(request: UlimitRequest).returns(Status) }
|
|
155
|
+
def run_request(request)
|
|
156
|
+
return list_all(request) if request.all
|
|
157
|
+
return change_limit(request) if request.value
|
|
158
|
+
|
|
159
|
+
print_limit(request.resource, request.target)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
sig { params(request: UlimitRequest).returns(Status) }
|
|
163
|
+
def list_all(request)
|
|
164
|
+
ULIMIT_RESOURCE_LIST.each { |resource| print_list_entry(resource, request.target) }
|
|
165
|
+
success
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
sig { params(resource: UlimitResource, target: Symbol).void }
|
|
169
|
+
def print_list_entry(resource, target)
|
|
170
|
+
stdout.printf("%<label>-20s %<value>s\n", label: resource.label, value: limit_text(resource, target))
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
sig { params(request: UlimitRequest).returns(Status) }
|
|
174
|
+
def change_limit(request)
|
|
175
|
+
value = parse_value(T.must(request.value), request.resource)
|
|
176
|
+
return value if value.is_a?(Status)
|
|
177
|
+
|
|
178
|
+
apply_limit(request, value)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
sig { params(request: UlimitRequest, value: Integer).returns(Status) }
|
|
182
|
+
def apply_limit(request, value)
|
|
183
|
+
limits = limits_for(request, value)
|
|
184
|
+
executor.system.setrlimit(request.resource.resource, limits.fetch(0), limits.fetch(1))
|
|
185
|
+
success
|
|
186
|
+
rescue SystemCallError, ArgumentError
|
|
187
|
+
limit_error
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
sig { params(request: UlimitRequest, value: Integer).returns([Integer, Integer]) }
|
|
191
|
+
def limits_for(request, value)
|
|
192
|
+
soft, hard = executor.system.getrlimit(request.resource.resource)
|
|
193
|
+
limits_for_set(request, value, soft, hard)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
sig do
|
|
197
|
+
params(request: UlimitRequest, value: Integer, soft: Integer, hard: Integer).returns([Integer, Integer])
|
|
198
|
+
end
|
|
199
|
+
def limits_for_set(request, value, soft, hard)
|
|
200
|
+
return [value, hard] if request.explicit_target && request.target == :soft
|
|
201
|
+
return [soft, value] if request.explicit_target && request.target == :hard
|
|
202
|
+
|
|
203
|
+
[value, value]
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
sig { params(resource: UlimitResource, target: Symbol).returns(Status) }
|
|
207
|
+
def print_limit(resource, target)
|
|
208
|
+
stdout.puts(limit_text(resource, target))
|
|
209
|
+
success
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
sig { params(resource: UlimitResource, target: Symbol).returns(String) }
|
|
213
|
+
def limit_text(resource, target)
|
|
214
|
+
soft, hard = executor.system.getrlimit(resource.resource)
|
|
215
|
+
format_limit(target == :hard ? hard : soft, resource)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
sig { params(text: String, resource: UlimitResource).returns(T.any(Integer, Status)) }
|
|
219
|
+
def parse_value(text, resource)
|
|
220
|
+
return executor.system.infinity_limit if text == 'unlimited'
|
|
221
|
+
return Integer(text, 10) * resource.scale if text.match?(/\A\d+\z/)
|
|
222
|
+
|
|
223
|
+
illegal_number(text)
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
sig { params(value: Integer, resource: UlimitResource).returns(String) }
|
|
227
|
+
def format_limit(value, resource)
|
|
228
|
+
return 'unlimited' if value >= executor.system.infinity_limit
|
|
229
|
+
|
|
230
|
+
(value / resource.scale).to_s
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
sig { returns(Status) }
|
|
234
|
+
def limit_error
|
|
235
|
+
stderr.puts('rush: ulimit: error setting limit')
|
|
236
|
+
failure(2)
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
sig { returns(Status) }
|
|
240
|
+
def too_many
|
|
241
|
+
stderr.puts('rush: ulimit: too many arguments')
|
|
242
|
+
failure(2)
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
sig { params(option: String).returns(Status) }
|
|
246
|
+
def illegal_option(option)
|
|
247
|
+
stderr.puts("rush: ulimit: Illegal option #{option}")
|
|
248
|
+
failure(2)
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
sig { params(text: String).returns(Status) }
|
|
252
|
+
def illegal_number(text)
|
|
253
|
+
stderr.puts("rush: ulimit: Illegal number: #{text}")
|
|
254
|
+
failure(2)
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
module Builtins
|
|
6
|
+
# `umask [-S] [mode]` — print or set the file creation mask. Octal operands
|
|
7
|
+
# set the mask directly; symbolic operands edit the allowed permission bits
|
|
8
|
+
# (the complement of the mask), as shown by `umask -S`.
|
|
9
|
+
class Umask < Base
|
|
10
|
+
extend T::Sig
|
|
11
|
+
|
|
12
|
+
sig { returns(Status) }
|
|
13
|
+
def call
|
|
14
|
+
return illegal_option(operands.first) if invalid_option?
|
|
15
|
+
|
|
16
|
+
symbolic, mode = parse_operands
|
|
17
|
+
return change_mask(mode) if mode
|
|
18
|
+
return display_symbolic if symbolic
|
|
19
|
+
|
|
20
|
+
display_octal
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
sig { returns(Integer) }
|
|
26
|
+
def current_mask
|
|
27
|
+
executor.system.current_umask
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
sig { params(mode: String).returns(Status) }
|
|
31
|
+
def change_mask(mode)
|
|
32
|
+
parsed = UmaskMode.parse(mode, current_mask)
|
|
33
|
+
return illegal(mode) unless parsed
|
|
34
|
+
|
|
35
|
+
executor.system.change_umask(parsed)
|
|
36
|
+
success
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
sig { returns(T::Boolean) }
|
|
40
|
+
def invalid_option?
|
|
41
|
+
operands.any? { |arg| invalid_option_operand?(arg) }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
sig { params(arg: String).returns(T::Boolean) }
|
|
45
|
+
def invalid_option_operand?(arg)
|
|
46
|
+
arg.start_with?('-') && arg != '-S'
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
sig { returns([T::Boolean, T.nilable(String)]) }
|
|
50
|
+
def parse_operands
|
|
51
|
+
args = operands.reject { |arg| arg == '-S' }
|
|
52
|
+
[operands.include?('-S'), args.first]
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
sig { returns(Status) }
|
|
56
|
+
def display_symbolic
|
|
57
|
+
stdout.puts(UmaskMode.format_symbolic(current_mask))
|
|
58
|
+
success
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
sig { returns(Status) }
|
|
62
|
+
def display_octal
|
|
63
|
+
stdout.puts(UmaskMode.format_octal(current_mask))
|
|
64
|
+
success
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
sig { params(mode: String).returns(Status) }
|
|
68
|
+
def illegal(mode)
|
|
69
|
+
label = mode.match?(/\A[0-9]+\z/) ? 'number' : 'mode'
|
|
70
|
+
stderr.puts("rush: umask: Illegal #{label}: #{mode}")
|
|
71
|
+
failure(2)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
sig { params(option: T.nilable(String)).returns(Status) }
|
|
75
|
+
def illegal_option(option)
|
|
76
|
+
stderr.puts("rush: umask: Illegal option #{option}")
|
|
77
|
+
failure(2)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
module Builtins
|
|
6
|
+
# `unalias name...` removes each named alias; `unalias -a` removes them all.
|
|
7
|
+
# A name with no alias is reported as `unalias: NAME not found` on stderr with
|
|
8
|
+
# status 1, but the remaining names are still processed.
|
|
9
|
+
class Unalias < Base
|
|
10
|
+
extend T::Sig
|
|
11
|
+
|
|
12
|
+
sig { returns(Status) }
|
|
13
|
+
def call
|
|
14
|
+
return remove_all if operands.first == '-a'
|
|
15
|
+
|
|
16
|
+
operands.reduce(success) { |status, name| keep(status, remove(name)) }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
sig { returns(Status) }
|
|
22
|
+
def remove_all
|
|
23
|
+
aliases.clear
|
|
24
|
+
success
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
sig { params(name: String).returns(Status) }
|
|
28
|
+
def remove(name)
|
|
29
|
+
return success if aliases.remove(name)
|
|
30
|
+
|
|
31
|
+
stderr.puts("unalias: #{name} not found")
|
|
32
|
+
failure(1)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
sig { params(status: Status, result: Status).returns(Status) }
|
|
36
|
+
def keep(status, result)
|
|
37
|
+
status.success? ? result : status
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
sig { returns(AliasTable) }
|
|
41
|
+
def aliases
|
|
42
|
+
executor.state.aliases
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
module Builtins
|
|
6
|
+
# `unset [-fv] name ...` — remove each name. The default (and -v) unsets a
|
|
7
|
+
# shell variable; -f unsets a function. Unsetting an absent name succeeds.
|
|
8
|
+
class Unset < Base
|
|
9
|
+
extend T::Sig
|
|
10
|
+
|
|
11
|
+
sig { returns(Status) }
|
|
12
|
+
def call
|
|
13
|
+
names.each { |name| remove(name) }
|
|
14
|
+
success
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
sig { returns(T::Array[String]) }
|
|
20
|
+
def names
|
|
21
|
+
operands.first&.start_with?('-') ? operands.drop(1) : operands
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
sig { returns(T::Boolean) }
|
|
25
|
+
def function?
|
|
26
|
+
operands.first == '-f'
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
sig { params(name: String).void }
|
|
30
|
+
def remove(name)
|
|
31
|
+
function? ? state.functions.undefine(name) : state.variables.unset(name)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
sig { returns(ShellState) }
|
|
35
|
+
def state
|
|
36
|
+
executor.state
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
module Builtins
|
|
6
|
+
# `wait [pid | %id ...]`: with no operands, wait for every known
|
|
7
|
+
# background job (status 0); with operands, wait for each and return the
|
|
8
|
+
# last one's status — 127 when a pid is not a known child, and a %id that
|
|
9
|
+
# resolves to no job is a status-2 error (No such job / No current job,
|
|
10
|
+
# like dash). POSIX gives an unknown last pid its 127 even after a known
|
|
11
|
+
# one; dash instead keeps the last known operand's status — the standard
|
|
12
|
+
# wins (journal). A malformed operand reports on stderr with status 2,
|
|
13
|
+
# and wait is a regular builtin, so the shell carries on (dash-verified).
|
|
14
|
+
class Wait < Base
|
|
15
|
+
extend T::Sig
|
|
16
|
+
|
|
17
|
+
# dash's operand shape: an explicit + sign is accepted; a value past
|
|
18
|
+
# INT_MAX overflows like a non-numeric one; a leading dash reads as an
|
|
19
|
+
# (unsupported) option, except the bare `-`, which is a bad number.
|
|
20
|
+
NUMERIC = /\A\+?\d+\z/
|
|
21
|
+
|
|
22
|
+
sig { returns(Status) }
|
|
23
|
+
def call
|
|
24
|
+
args = operands
|
|
25
|
+
args = args.drop(1) if args.first == '--'
|
|
26
|
+
return all if args.empty?
|
|
27
|
+
|
|
28
|
+
each_pid(args)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
# No operands: collect every known job — a stopped one answers its
|
|
34
|
+
# 128+stopsig at once, so wait never blocks on it (dash-probed).
|
|
35
|
+
sig { returns(Status) }
|
|
36
|
+
def all
|
|
37
|
+
executor.jobs.ordered.each do |job|
|
|
38
|
+
status = interruptible_wait(job.pid)
|
|
39
|
+
return pending_status(T.must(status)) if executor.trap_runner.pending?
|
|
40
|
+
end
|
|
41
|
+
success
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Malformed pids are rejected up front (observationally equivalent to
|
|
45
|
+
# dash's lazy check: an already-waited job stays remembered, so a later
|
|
46
|
+
# wait answers the same); %ids validate through resolution itself.
|
|
47
|
+
sig { params(args: T::Array[String]).returns(Status) }
|
|
48
|
+
def each_pid(args)
|
|
49
|
+
malformed = args.find { |operand| !operand.start_with?('%') && !parse(operand) }
|
|
50
|
+
return bad(malformed) if malformed
|
|
51
|
+
|
|
52
|
+
statuses(args)
|
|
53
|
+
rescue JobError => e
|
|
54
|
+
no_job(e)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
sig { params(args: T::Array[String]).returns(Status) }
|
|
58
|
+
def statuses(args)
|
|
59
|
+
status = T.let(success, Status)
|
|
60
|
+
status = status_of(T.must(args.shift)) until args.empty? || executor.trap_runner.pending?
|
|
61
|
+
pending_status(status)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
sig { params(operand: String).returns(Status) }
|
|
65
|
+
def status_of(operand)
|
|
66
|
+
return awaited(job_pid(operand)) if operand.start_with?('%')
|
|
67
|
+
|
|
68
|
+
awaited(T.must(parse(operand)))
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
sig { params(operand: String).returns(Integer) }
|
|
72
|
+
def job_pid(operand)
|
|
73
|
+
JobSpec.resolve(executor.jobs, operand).pid
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
sig { params(error: JobError).returns(Status) }
|
|
77
|
+
def no_job(error)
|
|
78
|
+
stderr.puts("wait: #{error.message}")
|
|
79
|
+
failure(2)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
sig { params(operand: String).returns(T.nilable(Integer)) }
|
|
83
|
+
def parse(operand)
|
|
84
|
+
return unless operand.match?(NUMERIC)
|
|
85
|
+
|
|
86
|
+
value = Integer(operand, 10)
|
|
87
|
+
value if value <= INT_MAX
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# A pid/%id operand whose job died by a signal reports strsignal on
|
|
91
|
+
# stderr, like dash's wait-by-pid (probed; the bare `wait` in #all
|
|
92
|
+
# never reports, also probed).
|
|
93
|
+
sig { params(pid: Integer).returns(Status) }
|
|
94
|
+
def awaited(pid)
|
|
95
|
+
status = interruptible_wait(pid)
|
|
96
|
+
status ? SignalReport.report(status, stderr) : failure(127)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
sig { params(pid: Integer).returns(T.nilable(Status)) }
|
|
100
|
+
def interruptible_wait(pid)
|
|
101
|
+
executor.jobs.wait_for_interruptibly(pid) { executor.trap_runner.pending_exitstatus }
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
sig { params(status: Status).returns(Status) }
|
|
105
|
+
def pending_status(status)
|
|
106
|
+
code = executor.trap_runner.pending_exitstatus
|
|
107
|
+
code ? Status.new(code) : status
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
sig { params(operand: String).returns(Status) }
|
|
111
|
+
def bad(operand)
|
|
112
|
+
stderr.puts("wait: #{diagnosis(operand)}")
|
|
113
|
+
failure(2)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
sig { params(operand: String).returns(String) }
|
|
117
|
+
def diagnosis(operand)
|
|
118
|
+
option?(operand) ? "Illegal option #{operand}" : "Illegal number: #{operand}"
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
sig { params(operand: String).returns(T::Boolean) }
|
|
122
|
+
def option?(operand)
|
|
123
|
+
operand.start_with?('-') && operand != '-'
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
data/lib/rush/cli.rb
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
# Entry point: parse the invocation, run the session it implies (interactive
|
|
6
|
+
# REPL or non-interactive source), and return the process exit code. A
|
|
7
|
+
# malformed command line — unknown option, missing -c/-o argument, unreadable
|
|
8
|
+
# script file — reports on stderr and exits 2, like dash.
|
|
9
|
+
class CLI
|
|
10
|
+
extend T::Sig
|
|
11
|
+
|
|
12
|
+
sig { params(argv: T::Array[String], system: SystemCalls).returns(Integer) }
|
|
13
|
+
def self.run(argv, system: SystemCalls.new)
|
|
14
|
+
new(argv, system).run
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
sig { params(argv: T::Array[String], system: SystemCalls).void }
|
|
18
|
+
def initialize(argv, system)
|
|
19
|
+
@argv = argv
|
|
20
|
+
@system = system
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
sig { returns(Integer) }
|
|
24
|
+
def run
|
|
25
|
+
Invocation.new(@argv, @system).session.run
|
|
26
|
+
rescue InvocationError => e
|
|
27
|
+
@system.stderr.puts("rush: #{e.message}")
|
|
28
|
+
2
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
# Applies assignment words attached to a simple command. Bare assignments
|
|
6
|
+
# persist in shell variables; assignments before an external command overlay
|
|
7
|
+
# the exported environment without mutating the shell.
|
|
8
|
+
class CommandAssignments
|
|
9
|
+
extend T::Sig
|
|
10
|
+
|
|
11
|
+
sig { params(assignments: T::Array[AST::Assignment], expander: Expansion::Pipeline).void }
|
|
12
|
+
def initialize(assignments, expander)
|
|
13
|
+
@assignments = assignments
|
|
14
|
+
@expander = expander
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
sig { returns(T::Array[String]) }
|
|
18
|
+
def names
|
|
19
|
+
@assignments.map(&:name)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
sig { params(variables: ShellVariables).void }
|
|
23
|
+
def persist_to(variables)
|
|
24
|
+
@assignments.each { |assignment| variables.assign(assignment.name, expand(assignment)) }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
sig { params(variables: ShellVariables).returns(T::Hash[String, String]) }
|
|
28
|
+
def environment_for(variables)
|
|
29
|
+
@assignments.each_with_object(variables.exported) do |assignment, env|
|
|
30
|
+
env[assignment.name] = expand(assignment)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
sig { params(environment: T::Hash[String, String]).returns(T::Hash[String, String]) }
|
|
35
|
+
def temporary_environment(environment)
|
|
36
|
+
names.to_h { |name| [name, environment.fetch(name)] }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
sig { params(environment: T::Hash[String, String], variables: ShellVariables).void }
|
|
40
|
+
def persist_environment(environment, variables)
|
|
41
|
+
temporary_environment(environment).each { |name, value| variables.assign(name, value) }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
sig { params(variables: ShellVariables).void }
|
|
45
|
+
def validate(variables)
|
|
46
|
+
@assignments.each { |assignment| variables.validate_assignment(assignment.name) }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
sig { params(assignment: AST::Assignment).returns(String) }
|
|
52
|
+
def expand(assignment)
|
|
53
|
+
@expander.expand_value(assignment.value, tilde: :assignment)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|