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,87 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
module Builtins
|
|
6
|
+
# Resolves test/[ operands that name shell file descriptors through the
|
|
7
|
+
# command's logical IoTable. In production, a redirected File/pipe/tty has a
|
|
8
|
+
# real fileno; rewriting to that descriptor lets the kernel's existing
|
|
9
|
+
# File/IO predicates answer exactly as if the shell had applied dup2.
|
|
10
|
+
class FdOperand
|
|
11
|
+
extend T::Sig
|
|
12
|
+
|
|
13
|
+
PATH_OPERATORS = %w[-e -f -d -r -w -x -s -p -b -c -S -g -u].freeze
|
|
14
|
+
STANDARD_PATHS = { '/dev/stdin' => 0, '/dev/stdout' => 1, '/dev/stderr' => 2 }.freeze
|
|
15
|
+
FD_PATH = %r{\A/(?:dev|proc/self)/fd/(\d+)\z}
|
|
16
|
+
CLOSED_PATH = '/dev/fd/-'
|
|
17
|
+
CLOSED_DESCRIPTOR = '-1'
|
|
18
|
+
|
|
19
|
+
sig { params(io: IoTable).void }
|
|
20
|
+
def initialize(io)
|
|
21
|
+
@io = io
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
sig { params(operator: String, operand: String).returns(String) }
|
|
25
|
+
def resolve(operator, operand)
|
|
26
|
+
return descriptor(operand) if operator == '-t'
|
|
27
|
+
return operand unless PATH_OPERATORS.include?(operator)
|
|
28
|
+
|
|
29
|
+
path(operand)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
# Steep 2 crashes internally on &:to_s here, so keep the explicit block.
|
|
35
|
+
# rubocop:disable Style/SymbolProc
|
|
36
|
+
sig { params(operand: String).returns(String) }
|
|
37
|
+
def descriptor(operand)
|
|
38
|
+
fd = integer(operand)
|
|
39
|
+
fd ? rewrite(fd, operand, CLOSED_DESCRIPTOR) { |fileno| fileno.to_s } : operand
|
|
40
|
+
end
|
|
41
|
+
# rubocop:enable Style/SymbolProc
|
|
42
|
+
|
|
43
|
+
sig { params(operand: String).returns(String) }
|
|
44
|
+
def path(operand)
|
|
45
|
+
fd = path_fd(operand)
|
|
46
|
+
fd ? rewrite(fd, operand, CLOSED_PATH) { |fileno| "/dev/fd/#{fileno}" } : operand
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
sig do
|
|
50
|
+
params(fd: Integer, original: String, closed: String,
|
|
51
|
+
blk: T.proc.params(fileno: Integer).returns(String)).returns(String)
|
|
52
|
+
end
|
|
53
|
+
def rewrite(fd, original, closed, &blk)
|
|
54
|
+
entry = @io.entry(fd)
|
|
55
|
+
return original unless entry
|
|
56
|
+
return closed if entry.closed?
|
|
57
|
+
|
|
58
|
+
replacement(entry, original, &blk)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
sig do
|
|
62
|
+
params(entry: FdEntry, original: String,
|
|
63
|
+
blk: T.proc.params(fileno: Integer).returns(String)).returns(String)
|
|
64
|
+
end
|
|
65
|
+
def replacement(entry, original, &blk)
|
|
66
|
+
fileno = entry.stream.fileno
|
|
67
|
+
fileno ? yield(fileno) : original
|
|
68
|
+
rescue IOError, SystemCallError
|
|
69
|
+
original
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
sig { params(operand: String).returns(T.nilable(Integer)) }
|
|
73
|
+
def path_fd(operand)
|
|
74
|
+
standard = STANDARD_PATHS.fetch(operand, nil)
|
|
75
|
+
return standard if standard
|
|
76
|
+
|
|
77
|
+
match = FD_PATH.match(operand)
|
|
78
|
+
integer(match[1].to_s) if match
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
sig { params(text: String).returns(T.nilable(Integer)) }
|
|
82
|
+
def integer(text)
|
|
83
|
+
Integer(text, 10) if text.match?(/\A\s*\+?\d+\s*\z/)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
module Builtins
|
|
6
|
+
# `getopts optstring name [args...]` — parse one POSIX short option per call,
|
|
7
|
+
# storing the option in `name`, the next public position in OPTIND and any
|
|
8
|
+
# option argument in OPTARG. A leading ':' in optstring enables silent error
|
|
9
|
+
# mode for bad options / missing arguments.
|
|
10
|
+
class Getopts < Base
|
|
11
|
+
extend T::Sig
|
|
12
|
+
|
|
13
|
+
sig { returns(Status) }
|
|
14
|
+
def call
|
|
15
|
+
return usage if operands.size < 2
|
|
16
|
+
|
|
17
|
+
apply(GetoptsParser.new(state: state, optstring: optstring, arguments: arguments, optind: optind).call)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
sig { returns(Status) }
|
|
23
|
+
def usage
|
|
24
|
+
report('getopts: Usage: getopts optstring var [arg...]')
|
|
25
|
+
failure(2)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
sig { params(result: GetoptsParser::Result).returns(Status) }
|
|
29
|
+
def apply(result)
|
|
30
|
+
store(result)
|
|
31
|
+
report_message(result.message)
|
|
32
|
+
Status.new(result.exitstatus)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# The variable updates a getopts step produces: `name`, OPTARG, OPTIND.
|
|
36
|
+
sig { params(result: GetoptsParser::Result).void }
|
|
37
|
+
def store(result)
|
|
38
|
+
variables.assign(name, result.name)
|
|
39
|
+
update_optarg(result.optarg)
|
|
40
|
+
variables.assign('OPTIND', result.optind.to_s)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
sig { params(value: T.untyped).void }
|
|
44
|
+
def update_optarg(value)
|
|
45
|
+
return if value.is_a?(Symbol) # GetoptsParser::KEEP, the only Symbol optarg
|
|
46
|
+
|
|
47
|
+
value ? variables.assign('OPTARG', value) : variables.unset('OPTARG')
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
sig { params(message: T.nilable(String)).void }
|
|
51
|
+
def report_message(message)
|
|
52
|
+
report(message) if message
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
sig { params(message: String).void }
|
|
56
|
+
def report(message)
|
|
57
|
+
stderr.puts(message)
|
|
58
|
+
rescue Errno::EBADF
|
|
59
|
+
nil
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
sig { returns(Integer) }
|
|
63
|
+
def optind
|
|
64
|
+
value = Integer(variables.get('OPTIND') || '1', 10)
|
|
65
|
+
value.positive? ? value : 1
|
|
66
|
+
rescue ArgumentError
|
|
67
|
+
1
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
sig { returns(T::Array[String]) }
|
|
71
|
+
def arguments
|
|
72
|
+
operands.drop(2).empty? ? executor.state.positional.to_a : operands.drop(2)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
sig { returns(String) }
|
|
76
|
+
def optstring
|
|
77
|
+
operands.fetch(0)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
sig { returns(String) }
|
|
81
|
+
def name
|
|
82
|
+
operands.fetch(1)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
sig { returns(GetoptsState) }
|
|
86
|
+
def state
|
|
87
|
+
executor.state.getopts
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
sig { returns(ShellVariables) }
|
|
91
|
+
def variables
|
|
92
|
+
executor.state.variables
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
module Builtins
|
|
6
|
+
# `hash [-r] [name ...]` — the command-location cache. With names, look each up
|
|
7
|
+
# in PATH and remember it; a name containing a slash or naming a builtin /
|
|
8
|
+
# function / keyword is a silent no-op, an unresolvable name an error (status
|
|
9
|
+
# 1). `-r` forgets all. With no operands, list the remembered locations, one
|
|
10
|
+
# path per line, sorted by name.
|
|
11
|
+
#
|
|
12
|
+
# Divergence: rush does not auto-populate the cache as commands run (POSIX/dash
|
|
13
|
+
# remember a utility's location when it is used); only an explicit `hash name`
|
|
14
|
+
# records one. Observable only via `<cmd>; hash` and noted in the journal — the
|
|
15
|
+
# cache is otherwise bit-for-bit consistent with dash.
|
|
16
|
+
class Hash < Base
|
|
17
|
+
extend T::Sig
|
|
18
|
+
|
|
19
|
+
sig { returns(Status) }
|
|
20
|
+
def call
|
|
21
|
+
return reset if operands.first == '-r'
|
|
22
|
+
|
|
23
|
+
operands.empty? ? list : remember(operands)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
sig { returns(T::Hash[String, String]) }
|
|
29
|
+
def cache
|
|
30
|
+
executor.state.command_hash
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
sig { returns(Status) }
|
|
34
|
+
def reset
|
|
35
|
+
cache.clear
|
|
36
|
+
success
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
sig { returns(Status) }
|
|
40
|
+
def list
|
|
41
|
+
cache.sort.each { |_name, path| stdout.puts(path) }
|
|
42
|
+
success
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
sig { params(names: T::Array[String]).returns(Status) }
|
|
46
|
+
def remember(names)
|
|
47
|
+
missing = names.reject { |name| known?(name) }
|
|
48
|
+
missing.each { |name| stderr.puts("rush: hash: #{name}: not found") }
|
|
49
|
+
missing.empty? ? success : failure
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Whether the name is a known command; a resolvable PATH command is also
|
|
53
|
+
# cached (a slash path or a builtin / function is known but not cached).
|
|
54
|
+
sig { params(name: String).returns(T::Boolean) }
|
|
55
|
+
def known?(name)
|
|
56
|
+
return true if name.include?('/')
|
|
57
|
+
|
|
58
|
+
match = CommandLookup.new(executor).find(name)
|
|
59
|
+
match.cache_into(cache)
|
|
60
|
+
match.known?
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
module Builtins
|
|
6
|
+
# Shared base of fg and bg: resolve each %id operand (no operand means
|
|
7
|
+
# the current job, and an empty table is "No current job", like dash),
|
|
8
|
+
# refuse a job that was not created under job control — dash's per-job
|
|
9
|
+
# jobctl bit, independent of the monitor flag now in force (probed
|
|
10
|
+
# across set -m/+m flips) — and resume survivors with SIGCONT to the
|
|
11
|
+
# job's process group. dash loops multiple operands for both commands;
|
|
12
|
+
# the last one's status is the builtin's.
|
|
13
|
+
class JobResume < Base
|
|
14
|
+
extend T::Sig
|
|
15
|
+
|
|
16
|
+
sig { returns(Status) }
|
|
17
|
+
def call
|
|
18
|
+
specs = operands.empty? ? [nil] : operands
|
|
19
|
+
specs.map { |spec| resume_one(spec) }.fetch(-1)
|
|
20
|
+
rescue JobError => e
|
|
21
|
+
bad(e.message)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
# A job outside job control aborts the whole command (dash's sh_error:
|
|
27
|
+
# later operands never run), through the same status-2 error path as a
|
|
28
|
+
# bad %id.
|
|
29
|
+
sig { params(spec: T.nilable(String)).returns(Status) }
|
|
30
|
+
def resume_one(spec)
|
|
31
|
+
job = JobSpec.resolve(executor.jobs, spec || '%+')
|
|
32
|
+
raise JobError, refusal(spec) unless job.controlled?
|
|
33
|
+
|
|
34
|
+
resume(job)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# The subclass's whole move: bg lists and leaves the job running in
|
|
38
|
+
# the background; fg reattaches and waits.
|
|
39
|
+
sig { params(_job: JobTable::Job).returns(Status) }
|
|
40
|
+
def resume(_job)
|
|
41
|
+
# :nocov: overridden by fg/bg
|
|
42
|
+
raise NotImplementedError
|
|
43
|
+
# :nocov:
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
sig { params(spec: T.nilable(String)).returns(String) }
|
|
47
|
+
def refusal(spec)
|
|
48
|
+
spec ? "job #{spec} not created under job control" : 'job not created under job control'
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
sig { params(message: String).returns(Status) }
|
|
52
|
+
def bad(message)
|
|
53
|
+
stderr.puts("#{argv.fetch(0)}: #{message}")
|
|
54
|
+
failure(2)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# `fg [%id...]`: bring a job to the foreground — the command line goes
|
|
59
|
+
# to stdout (an empty placeholder until the command-text column lands
|
|
60
|
+
# with rush-mv8.6), SIGCONT resumes the group, the terminal follows it
|
|
61
|
+
# (JobControl#foreground), and the shell waits as for any foreground
|
|
62
|
+
# job: the job's status becomes $?, a ^Z parks it Stopped again under
|
|
63
|
+
# the same number, and a finished job leaves the table (dash frees the
|
|
64
|
+
# entry it fg-waited).
|
|
65
|
+
class Fg < JobResume
|
|
66
|
+
extend T::Sig
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
sig { params(job: JobTable::Job).returns(Status) }
|
|
71
|
+
def resume(job)
|
|
72
|
+
stdout.puts(job.text)
|
|
73
|
+
executor.job_control.foreground(job.members, text: job.text) do
|
|
74
|
+
job.continue(executor.system)
|
|
75
|
+
settle(job)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# A finished job answers from memory (dash's fg on a dead-but-
|
|
80
|
+
# remembered entry: getstatus, probed 137); a live one waits every
|
|
81
|
+
# member (JobTable#settle_members). Either way a signal death reports
|
|
82
|
+
# like any foreground wait's (dash: fg + kill prints Killed).
|
|
83
|
+
sig { params(job: JobTable::Job).returns(Status) }
|
|
84
|
+
def settle(job)
|
|
85
|
+
verdict = SignalReport.report(job.finished? ? job.status : executor.jobs.settle_members(job), stderr)
|
|
86
|
+
executor.jobs.forget(job) unless verdict.stopped?
|
|
87
|
+
verdict
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# `bg [%id...]`: resume a stopped job in the background — "[n] " and the
|
|
92
|
+
# (mv8.6) command line to stdout, SIGCONT, the entry runs on in the
|
|
93
|
+
# table and the terminal stays with the shell.
|
|
94
|
+
class Bg < JobResume
|
|
95
|
+
extend T::Sig
|
|
96
|
+
|
|
97
|
+
private
|
|
98
|
+
|
|
99
|
+
sig { params(job: JobTable::Job).returns(Status) }
|
|
100
|
+
def resume(job)
|
|
101
|
+
job.continue(executor.system)
|
|
102
|
+
stdout.puts("[#{job.number}] #{job.text}")
|
|
103
|
+
success
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
module Builtins
|
|
6
|
+
# `jobs [-l|-p] [%id...]`: list background jobs, newest first, + and -
|
|
7
|
+
# marking the current and previous job, states Running / Done / Done(n) /
|
|
8
|
+
# a signal description. -l inserts the pid after the mark; -p prints only
|
|
9
|
+
# pids. The command column (34) stays empty away from a tty, exactly like
|
|
10
|
+
# dash, which keeps no command text there. Displaying a finished job
|
|
11
|
+
# frees its entry (-p does not); a bad %id reports No such job with
|
|
12
|
+
# status 2 after the entries already rendered (dash-verified).
|
|
13
|
+
class Jobs < Base
|
|
14
|
+
extend T::Sig
|
|
15
|
+
|
|
16
|
+
# The flag picks a rendering style; anything else is an illegal option.
|
|
17
|
+
STYLES = T.let({ nil => :short_line, '-l' => :long_line, '-p' => :pid_line }.freeze,
|
|
18
|
+
T::Hash[T.nilable(String), Symbol])
|
|
19
|
+
|
|
20
|
+
sig { returns(Status) }
|
|
21
|
+
def call
|
|
22
|
+
executor.jobs.poll
|
|
23
|
+
flag, args = split_flag(operands)
|
|
24
|
+
STYLES.key?(flag) ? show(STYLES.fetch(flag), args) : bad("Illegal option #{flag}")
|
|
25
|
+
rescue JobError => e
|
|
26
|
+
bad(e.message)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
# tuple: the optional leading -flag and the remaining %id operands.
|
|
32
|
+
sig { params(args: T::Array[String]).returns([T.nilable(String), T::Array[String]]) }
|
|
33
|
+
def split_flag(args)
|
|
34
|
+
args.first&.start_with?('-') ? [args.fetch(0), args.drop(1)] : [nil, args]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
sig { params(style: Symbol, args: T::Array[String]).returns(Status) }
|
|
38
|
+
def show(style, args)
|
|
39
|
+
line = method(style)
|
|
40
|
+
each_selected(args) { |job| line.call(job) }
|
|
41
|
+
success
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Resolution stays interleaved with rendering so a bad %id aborts after
|
|
45
|
+
# the entries before it have already been printed, as dash does.
|
|
46
|
+
sig { params(args: T::Array[String], blk: T.proc.params(job: JobTable::Job).returns(T.untyped)).void }
|
|
47
|
+
def each_selected(args, &blk)
|
|
48
|
+
return executor.jobs.ordered.each(&blk) if args.empty?
|
|
49
|
+
|
|
50
|
+
args.each { |spec| yield(JobSpec.resolve(executor.jobs, spec)) }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
sig { params(job: JobTable::Job).void }
|
|
54
|
+
def pid_line(job)
|
|
55
|
+
stdout.puts(job.pid)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
sig { params(job: JobTable::Job).void }
|
|
59
|
+
def long_line(job)
|
|
60
|
+
report(job, "#{job.pid} ")
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
sig { params(job: JobTable::Job).void }
|
|
64
|
+
def short_line(job)
|
|
65
|
+
report(job, '')
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# dash's showjob line (JobReport). Displaying counts as reporting (no
|
|
69
|
+
# pre-prompt notification follows), a displayed finished job is freed —
|
|
70
|
+
# a Stopped one is not: it is still alive, and dash keeps listing it
|
|
71
|
+
# (probed) — and the bare-pid style never frees or reports.
|
|
72
|
+
sig { params(job: JobTable::Job, pid_field: String).void }
|
|
73
|
+
def report(job, pid_field)
|
|
74
|
+
stdout.puts(JobReport.line(executor.jobs, job, pid_field))
|
|
75
|
+
job.reported
|
|
76
|
+
executor.jobs.forget(job) if job.finished?
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
sig { params(message: String).returns(Status) }
|
|
80
|
+
def bad(message)
|
|
81
|
+
stderr.puts("jobs: #{message}")
|
|
82
|
+
failure(2)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
module Builtins
|
|
6
|
+
# `kill [-s sigspec | -sigspec | -signum] pid|%id...` sends a signal (TERM
|
|
7
|
+
# by default) to each process; `kill -0 pid` only checks that it exists.
|
|
8
|
+
# Signal names follow Rush::Signals (case-insensitive, no "SIG" prefix);
|
|
9
|
+
# numbers go straight to the OS so the unnamed ones (16, RT, ...) still
|
|
10
|
+
# work. A %id targets the job's process group (-pid, as dash) — without
|
|
11
|
+
# the terminal half of job control no such group exists, so delivery
|
|
12
|
+
# fails "no such process", status 1, and the job survives (dash parity).
|
|
13
|
+
# `kill -l N` prints the signal name for a number or wait status (128 +
|
|
14
|
+
# signal), and a bare `kill -l` lists the known names. A bad spec or an
|
|
15
|
+
# unresolved %id exits 2; a delivery that fails exits 1.
|
|
16
|
+
class Kill < Base
|
|
17
|
+
extend T::Sig
|
|
18
|
+
|
|
19
|
+
sig { returns(Status) }
|
|
20
|
+
def call
|
|
21
|
+
return usage if operands.empty?
|
|
22
|
+
return list(operands.drop(1)) if operands.first == '-l'
|
|
23
|
+
|
|
24
|
+
spec, pids = parse
|
|
25
|
+
send_signal(spec, pids)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
# tuple, not Array: parse is destructured into send_signal's two arguments.
|
|
31
|
+
sig { returns([String, T::Array[String]]) }
|
|
32
|
+
# mutant:disable -- for incomplete `-s`, the returned sigspec is ignored:
|
|
33
|
+
# `send_signal` reports usage before resolving any signal when no pid exists.
|
|
34
|
+
def parse
|
|
35
|
+
first = operands.fetch(0)
|
|
36
|
+
return [operands.fetch(1, ''), operands.drop(2)] if first == '-s'
|
|
37
|
+
return [first.delete_prefix('-'), operands.drop(1)] if flag?(first)
|
|
38
|
+
|
|
39
|
+
['TERM', operands]
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
sig { params(arg: String).returns(T::Boolean) }
|
|
43
|
+
def flag?(arg)
|
|
44
|
+
arg.start_with?('-') && !arg.eql?('-')
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
sig { params(spec: String, pids: T::Array[String]).returns(Status) }
|
|
48
|
+
def send_signal(spec, pids)
|
|
49
|
+
return usage if pids.empty?
|
|
50
|
+
|
|
51
|
+
signal = resolve(spec)
|
|
52
|
+
signal ? deliver(signal, pids) : bad("#{spec}: invalid signal specification")
|
|
53
|
+
rescue JobError => e
|
|
54
|
+
bad(e.message)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# A numeric spec goes straight to the OS (Integer); a name decodes to its
|
|
58
|
+
# canonical String, or nil when the spec is unknown.
|
|
59
|
+
sig { params(spec: String).returns(T.nilable(T.any(Integer, String))) }
|
|
60
|
+
def resolve(spec)
|
|
61
|
+
return Integer(spec) if spec.match?(/\A\d+\z/)
|
|
62
|
+
|
|
63
|
+
Signals.decode(spec)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
sig { params(signal: T.any(Integer, String), pids: T::Array[String]).returns(Status) }
|
|
67
|
+
def deliver(signal, pids)
|
|
68
|
+
failed = pids.reject { |pid| send_to(signal, pid) }
|
|
69
|
+
failed.empty? ? success : failure
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Returns a truthy OS delivery count on success, nil when delivery failed.
|
|
73
|
+
sig { params(signal: T.any(Integer, String), pid: String).returns(T.nilable(Integer)) }
|
|
74
|
+
def send_to(signal, pid)
|
|
75
|
+
target = os_target(pid)
|
|
76
|
+
executor.system.kill(signal, target)
|
|
77
|
+
rescue SystemCallError, ArgumentError, TypeError
|
|
78
|
+
oops("#{pid}: no such process")
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# A %id names the job's process group: negative pid, like dash. An
|
|
82
|
+
# unresolved %id raises JobError past the delivery loop (status 2).
|
|
83
|
+
sig { params(pid: String).returns(Integer) }
|
|
84
|
+
def os_target(pid)
|
|
85
|
+
return -JobSpec.resolve(executor.jobs, pid).pid if pid.start_with?('%')
|
|
86
|
+
|
|
87
|
+
Integer(pid)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
sig { params(args: T::Array[String]).returns(Status) }
|
|
91
|
+
def list(args)
|
|
92
|
+
args.empty? ? list_all : list_one(args.fetch(0))
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
sig { returns(Status) }
|
|
96
|
+
def list_all
|
|
97
|
+
Signals::NUMBERS.each { |num, name| stdout.puts(name) if num.nonzero? }
|
|
98
|
+
success
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
sig { params(arg: String).returns(Status) }
|
|
102
|
+
def list_one(arg)
|
|
103
|
+
name = listed_signal_name(arg)
|
|
104
|
+
name ? ok(name) : invalid_signal(arg)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
sig { params(arg: String).returns(T.nilable(String)) }
|
|
108
|
+
def listed_signal_name(arg)
|
|
109
|
+
return unless arg.match?(/\A\d+\z/)
|
|
110
|
+
|
|
111
|
+
num = adjust(Integer(arg))
|
|
112
|
+
return unless num.positive?
|
|
113
|
+
|
|
114
|
+
Signals::NUMBERS.fetch(num, nil)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
sig { params(num: Integer).returns(Integer) }
|
|
118
|
+
# mutant:disable -- for `kill -l 128`, `128` and `0` are both invalid;
|
|
119
|
+
# changing the wait-status boundary is observationally equivalent there.
|
|
120
|
+
def adjust(num)
|
|
121
|
+
num >= 129 ? num - 128 : num
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
sig { params(name: String).returns(Status) }
|
|
125
|
+
def ok(name)
|
|
126
|
+
stdout.puts(name)
|
|
127
|
+
success
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
sig { params(spec: T.nilable(String)).returns(Status) }
|
|
131
|
+
def invalid_signal(spec)
|
|
132
|
+
bad("#{spec}: invalid signal specification")
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
sig { returns(Status) }
|
|
136
|
+
def usage
|
|
137
|
+
bad('usage: kill [-s sigspec | -signum] pid ...')
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
sig { params(message: String).returns(Status) }
|
|
141
|
+
def bad(message)
|
|
142
|
+
stderr.puts("kill: #{message}")
|
|
143
|
+
failure(2)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
sig { params(message: String).returns(NilClass) }
|
|
147
|
+
def oops(message)
|
|
148
|
+
stderr.puts("kill: #{message}")
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
module Builtins
|
|
6
|
+
# `local name[=value] ...` — inside a function, mark each name as local so its
|
|
7
|
+
# prior value is restored when the function returns. A bare name keeps its
|
|
8
|
+
# current value (dash semantics); name=value also assigns. Used outside a
|
|
9
|
+
# function it is an error with status 2.
|
|
10
|
+
class Local < Base
|
|
11
|
+
extend T::Sig
|
|
12
|
+
|
|
13
|
+
sig { returns(Status) }
|
|
14
|
+
def call
|
|
15
|
+
return not_in_function unless state.variables.in_function?
|
|
16
|
+
|
|
17
|
+
operands.each { |operand| declare(operand) }
|
|
18
|
+
success
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
sig { returns(ShellState) }
|
|
24
|
+
def state
|
|
25
|
+
executor.state
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
sig { params(operand: String).void }
|
|
29
|
+
def declare(operand)
|
|
30
|
+
state.variables.declare_local_operand(operand)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
sig { returns(Status) }
|
|
34
|
+
def not_in_function
|
|
35
|
+
stderr.puts('rush: local: not in a function')
|
|
36
|
+
failure(2)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
module Builtins
|
|
6
|
+
# Shared engine of `break` and `continue` (POSIX 2.9.5). Both are successful
|
|
7
|
+
# builtins, so they set $? to 0 before unwinding: the status is seen after
|
|
8
|
+
# the loop, and by the next iteration's body. With no enclosing loop the
|
|
9
|
+
# builtin is a no-op; a level past the actual nesting is clamped, so
|
|
10
|
+
# `break 5` in two loops exits both. The level (>= 1) is validated even
|
|
11
|
+
# with no loop, so `break abc` still aborts. Subclasses name the signal
|
|
12
|
+
# that carries the level up to the enclosing loop runner.
|
|
13
|
+
class LoopJump < Base
|
|
14
|
+
extend T::Sig
|
|
15
|
+
|
|
16
|
+
sig { returns(Status) }
|
|
17
|
+
def call
|
|
18
|
+
level = validated
|
|
19
|
+
executor.state.record_status(success)
|
|
20
|
+
raise signal, clamped(level) if executor.state.loops.any?
|
|
21
|
+
|
|
22
|
+
success
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
# The LoopControl subclass this builtin unwinds with.
|
|
28
|
+
sig { returns(T.class_of(LoopControl)) }
|
|
29
|
+
def signal
|
|
30
|
+
raise NotImplementedError
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
sig { returns(Integer) }
|
|
34
|
+
def validated
|
|
35
|
+
first = operands.first
|
|
36
|
+
first ? numeric_operand(first, min: 1) : 1
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
sig { params(level: Integer).returns(Integer) }
|
|
40
|
+
def clamped(level)
|
|
41
|
+
[level, executor.state.loops.depth].min
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|