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,74 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
module Builtins
|
|
6
|
+
# Pure evaluator for `test`/`[` expressions. POSIX specifies the result
|
|
7
|
+
# by argument count (XCU test), so #evaluate applies that table in dash's
|
|
8
|
+
# order: at three arguments a binary primary outranks any other reading;
|
|
9
|
+
# a leading `!` at three or four arguments negates the rest (re-checked,
|
|
10
|
+
# so `! a = b` reads as the negated compare); a full `( ... )` wrapper at
|
|
11
|
+
# three or four arguments drops to its contents. Everything else — zero
|
|
12
|
+
# to two arguments and the shapes POSIX leaves unspecified — is handed to
|
|
13
|
+
# TestGrammar, the transcription of dash's recursive descent. A malformed
|
|
14
|
+
# expression raises TestError, which the builtin maps to exit 2.
|
|
15
|
+
#
|
|
16
|
+
# One deliberate divergence, on POSIX's side: dash zeroes (rather than
|
|
17
|
+
# toggles) its negation parity when it peels `!` twice, so `[ ! ! ! x ]`
|
|
18
|
+
# is true and `[ ! ! -n x ]` false there; POSIX defines the four-argument
|
|
19
|
+
# `!` form as the negated three-argument test, so rush negates honestly.
|
|
20
|
+
# Pinned in the unit specs.
|
|
21
|
+
class TestExpr
|
|
22
|
+
def initialize(args, context)
|
|
23
|
+
@args = args
|
|
24
|
+
@context = context
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def true?
|
|
28
|
+
evaluate(@args)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
# A binary primary at exactly three arguments outranks every other
|
|
34
|
+
# reading; only then do the `!` and `( ... )` rows apply.
|
|
35
|
+
def evaluate(args)
|
|
36
|
+
return binary(args) if shortcut?(args)
|
|
37
|
+
|
|
38
|
+
peel(args)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def shortcut?(args)
|
|
42
|
+
args.size == 3 && TestOperators.binary?(args[1])
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def peel(args)
|
|
46
|
+
return !evaluate(args.drop(1)) if negation?(args)
|
|
47
|
+
return grammar(args[1...-1].to_a) if wrapped?(args)
|
|
48
|
+
|
|
49
|
+
grammar(args)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# `!` peels only at three or four arguments (POSIX's 3- and 4-argument
|
|
53
|
+
# rows); at other lengths the grammar owns `!` with tighter binding.
|
|
54
|
+
def negation?(args)
|
|
55
|
+
(3..4).cover?(args.size) && args.first == '!'
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# A full `( ... )` wrapper likewise drops to its contents only at three
|
|
59
|
+
# or four arguments; longer groupings are parsed as grammar primaries.
|
|
60
|
+
def wrapped?(args)
|
|
61
|
+
(3..4).cover?(args.size) && args.first == '(' && args.last == ')'
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def binary(args)
|
|
65
|
+
args => [lhs, op, rhs]
|
|
66
|
+
TestOperators.apply_binary(op, lhs, rhs)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def grammar(args)
|
|
70
|
+
TestGrammar.new(args, @context).truth
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
module Builtins
|
|
6
|
+
# The historical `test`/`[` grammar, transcribed from dash's recursive
|
|
7
|
+
# descent (oexpr/aexpr/nexpr/primary). POSIX leaves test beyond four
|
|
8
|
+
# arguments unspecified and marks the -a/-o connectives obsolescent, but
|
|
9
|
+
# countless real scripts use them, so rush reproduces dash here:
|
|
10
|
+
#
|
|
11
|
+
# or ::= and { "-o" and } (both arms always evaluated)
|
|
12
|
+
# and ::= not { "-a" not }
|
|
13
|
+
# not ::= "!" not | primary
|
|
14
|
+
# primary ::= "(" or ")" | unary-op word | word binary-op word | word
|
|
15
|
+
#
|
|
16
|
+
# The cursor mirrors dash's t_wp pointer discipline: it rests on the last
|
|
17
|
+
# consumed word, and a word left over after the parse is the "unexpected
|
|
18
|
+
# operator" syntax error (TestError, exit 2). A missing operand after a
|
|
19
|
+
# connective parses as the missing expression, which is false, so
|
|
20
|
+
# `[ x -o ]` is true and `[ x -a ]` is false, exactly as in dash.
|
|
21
|
+
class TestGrammar
|
|
22
|
+
def initialize(args, context)
|
|
23
|
+
@args = args
|
|
24
|
+
@context = context
|
|
25
|
+
@tokens = TestTokens.new(args)
|
|
26
|
+
@pos = 0
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def truth
|
|
30
|
+
result = or_expr(kind_at(@pos))
|
|
31
|
+
trailing = @args[@pos + 1]
|
|
32
|
+
raise TestError, "#{trailing}: unexpected operator" if @args[@pos] && trailing
|
|
33
|
+
|
|
34
|
+
result
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def kind_at(index)
|
|
40
|
+
@tokens.kind_at(index)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def or_expr(kind)
|
|
44
|
+
result = and_expr(kind)
|
|
45
|
+
result = and_expr(kind_at(@pos += 2)) || result while connective == :bor
|
|
46
|
+
result
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def and_expr(kind)
|
|
50
|
+
result = not_expr(kind)
|
|
51
|
+
result = not_expr(kind_at(@pos += 2)) && result while connective == :band
|
|
52
|
+
result
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# The kind of the word after the cursor, or :eoi once the current word
|
|
56
|
+
# is exhausted — dash's `if (!*t_wp) break; t_lex(t_wp + 1)`.
|
|
57
|
+
def connective
|
|
58
|
+
@args.fetch(@pos) { return :eoi }
|
|
59
|
+
kind_at(@pos + 1)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def not_expr(kind)
|
|
63
|
+
return primary(kind) unless kind == :bunop
|
|
64
|
+
|
|
65
|
+
negand = kind_at(@pos + 1)
|
|
66
|
+
@pos += 1 unless negand == :eoi
|
|
67
|
+
!not_expr(negand)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# The missing expression is false, `(` opens a grouping, a unary
|
|
71
|
+
# primary applies, and any other kind reads as a word primary.
|
|
72
|
+
def primary(kind)
|
|
73
|
+
case kind
|
|
74
|
+
in :eoi then false
|
|
75
|
+
in :lparen then group
|
|
76
|
+
else kind == :unop ? unary : word
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def group
|
|
81
|
+
inner = kind_at(@pos += 1)
|
|
82
|
+
return false if inner == :rparen
|
|
83
|
+
|
|
84
|
+
result = or_expr(inner)
|
|
85
|
+
raise TestError, 'closing paren expected' unless kind_at(@pos += 1) == :rparen
|
|
86
|
+
|
|
87
|
+
result
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# The operand always exists: a unary primary with nothing after it is
|
|
91
|
+
# demoted to a plain operand by TestTokens, never classified :unop.
|
|
92
|
+
def unary
|
|
93
|
+
op = current
|
|
94
|
+
@context.unary(op, @args.fetch(@pos += 1))
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# A word primary: a binary comparison when a binary primary follows,
|
|
98
|
+
# otherwise the plain non-empty test of the word itself.
|
|
99
|
+
def word
|
|
100
|
+
return binary if kind_at(@pos + 1) == :binop
|
|
101
|
+
|
|
102
|
+
!current.empty?
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def binary
|
|
106
|
+
lhs = current
|
|
107
|
+
op = @args.fetch(@pos += 1)
|
|
108
|
+
rhs = @args.fetch(@pos += 1) { raise TestError, "#{op}: argument expected" }
|
|
109
|
+
TestOperators.apply_binary(op, lhs, rhs)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def current
|
|
113
|
+
@args.fetch(@pos)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
module Builtins
|
|
6
|
+
# The operator vocabulary of `test`/`[`, shared by the argument-count
|
|
7
|
+
# front end (TestExpr) and the recursive-descent grammar (TestGrammar).
|
|
8
|
+
# The tables map to lambdas, not method symbols, so each primary carries
|
|
9
|
+
# its operand types past both checkers (the #: types each lambda for
|
|
10
|
+
# Steep, the T.let the table for Sorbet). File primaries ask the
|
|
11
|
+
# SystemCalls port; -t parses its operand as a file-descriptor number
|
|
12
|
+
# first, rejecting non-numbers like dash's "Illegal number" (exit 2).
|
|
13
|
+
module TestOperators
|
|
14
|
+
UNARY = T.let({
|
|
15
|
+
'-n' => ->(_files, val) { !val.empty? }, #: ^(SystemCalls, String) -> bool
|
|
16
|
+
'-z' => ->(_files, val) { val.empty? }, #: ^(SystemCalls, String) -> bool
|
|
17
|
+
'-t' => ->(files, val) { files.tty_fd?(fd_number(val)) }, #: ^(SystemCalls, String) -> bool
|
|
18
|
+
'-e' => ->(files, val) { files.exist?(val) }, #: ^(SystemCalls, String) -> bool
|
|
19
|
+
'-f' => ->(files, val) { files.file?(val) }, #: ^(SystemCalls, String) -> bool
|
|
20
|
+
'-d' => ->(files, val) { files.directory?(val) }, #: ^(SystemCalls, String) -> bool
|
|
21
|
+
'-r' => ->(files, val) { files.readable?(val) }, #: ^(SystemCalls, String) -> bool
|
|
22
|
+
'-w' => ->(files, val) { files.writable?(val) }, #: ^(SystemCalls, String) -> bool
|
|
23
|
+
'-x' => ->(files, val) { files.executable?(val) }, #: ^(SystemCalls, String) -> bool
|
|
24
|
+
'-s' => ->(files, val) { files.file_nonempty?(val) }, #: ^(SystemCalls, String) -> bool
|
|
25
|
+
'-h' => ->(files, val) { files.symlink?(val) }, #: ^(SystemCalls, String) -> bool
|
|
26
|
+
'-L' => ->(files, val) { files.symlink?(val) }, #: ^(SystemCalls, String) -> bool
|
|
27
|
+
'-p' => ->(files, val) { files.pipe?(val) }, #: ^(SystemCalls, String) -> bool
|
|
28
|
+
'-b' => ->(files, val) { files.blockdev?(val) }, #: ^(SystemCalls, String) -> bool
|
|
29
|
+
'-c' => ->(files, val) { files.chardev?(val) }, #: ^(SystemCalls, String) -> bool
|
|
30
|
+
'-S' => ->(files, val) { files.socket?(val) }, #: ^(SystemCalls, String) -> bool
|
|
31
|
+
'-g' => ->(files, val) { files.setgid?(val) }, #: ^(SystemCalls, String) -> bool
|
|
32
|
+
'-u' => ->(files, val) { files.setuid?(val) } #: ^(SystemCalls, String) -> bool
|
|
33
|
+
}.freeze, T::Hash[String, T.proc.params(files: SystemCalls, val: String).returns(T::Boolean)])
|
|
34
|
+
STRING = T.let({
|
|
35
|
+
'=' => ->(lhs, rhs) { lhs == rhs }, #: ^(String, String) -> bool
|
|
36
|
+
'!=' => ->(lhs, rhs) { lhs != rhs } #: ^(String, String) -> bool
|
|
37
|
+
}.freeze, T::Hash[String, T.proc.params(lhs: String, rhs: String).returns(T::Boolean)])
|
|
38
|
+
INTEGER = T.let({
|
|
39
|
+
'-eq' => ->(lhs, rhs) { lhs == rhs }, #: ^(Integer, Integer) -> bool
|
|
40
|
+
'-ne' => ->(lhs, rhs) { lhs != rhs }, #: ^(Integer, Integer) -> bool
|
|
41
|
+
'-gt' => ->(lhs, rhs) { lhs > rhs }, #: ^(Integer, Integer) -> bool
|
|
42
|
+
'-ge' => ->(lhs, rhs) { lhs >= rhs }, #: ^(Integer, Integer) -> bool
|
|
43
|
+
'-lt' => ->(lhs, rhs) { lhs < rhs }, #: ^(Integer, Integer) -> bool
|
|
44
|
+
'-le' => ->(lhs, rhs) { lhs <= rhs } #: ^(Integer, Integer) -> bool
|
|
45
|
+
}.freeze, T::Hash[String, T.proc.params(lhs: Integer, rhs: Integer).returns(T::Boolean)])
|
|
46
|
+
|
|
47
|
+
# A string that may name an integer for the numeric primaries: #value is its
|
|
48
|
+
# integer when it is a valid (optionally signed, blank-padded) decimal, else
|
|
49
|
+
# nil. Underscores and 0x are rejected, matching dash's strtol-strictness.
|
|
50
|
+
class MaybeInteger
|
|
51
|
+
PATTERN = /\A\s*[+-]?\d+\s*\z/
|
|
52
|
+
|
|
53
|
+
def initialize(text)
|
|
54
|
+
@text = text
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def valid?
|
|
58
|
+
@text.match?(PATTERN)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def value
|
|
62
|
+
Integer(@text, 10) if valid?
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def self.unary?(word)
|
|
67
|
+
UNARY.key?(word)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def self.binary?(word)
|
|
71
|
+
STRING.key?(word) || INTEGER.key?(word)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def self.unary(op)
|
|
75
|
+
UNARY.fetch(op)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def self.apply_binary(op, lhs, rhs)
|
|
79
|
+
string = STRING[op]
|
|
80
|
+
return string.call(lhs, rhs) if string
|
|
81
|
+
|
|
82
|
+
INTEGER.fetch(op).call(to_int(lhs), to_int(rhs))
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def self.to_int(text)
|
|
86
|
+
MaybeInteger.new(text).value || raise(TestError, "#{text}: integer expected")
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def self.fd_number(text)
|
|
90
|
+
MaybeInteger.new(text).value || raise(TestError, "Illegal number: #{text}")
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
module Builtins
|
|
6
|
+
# Positional word classification for `test`/`[`, mirroring dash's t_lex:
|
|
7
|
+
# a word is an operator only in operator position. A unary primary is
|
|
8
|
+
# demoted to a plain operand when it is the last word (`[ -n ]` is the
|
|
9
|
+
# non-empty test of "-n") or when a binary primary follows with more
|
|
10
|
+
# beyond (`[ -t = -t ]` compares strings); a `(` closing the argument
|
|
11
|
+
# list is likewise a word. Everything else keeps its table kind: unary
|
|
12
|
+
# and binary primaries, `!`, the XSI-obsolescent -a/-o connectives, and
|
|
13
|
+
# the parentheses.
|
|
14
|
+
class TestTokens
|
|
15
|
+
extend T::Sig
|
|
16
|
+
|
|
17
|
+
KINDS = T.let({ '!' => :bunop, '-a' => :band, '-o' => :bor,
|
|
18
|
+
'(' => :lparen, ')' => :rparen }.freeze, T::Hash[String, Symbol])
|
|
19
|
+
|
|
20
|
+
# The t_lex demotion rules, keyed by table kind (a registry, so the
|
|
21
|
+
# classifier itself never branches on the kind it is computing).
|
|
22
|
+
DEMOTIONS = T.let({
|
|
23
|
+
unop: ->(args, index) { operand_position?(args, index) }, #: ^(Array[String], Integer) -> bool
|
|
24
|
+
lparen: ->(args, index) { args[index + 1].nil? } #: ^(Array[String], Integer) -> bool
|
|
25
|
+
}.freeze, T::Hash[Symbol, T.proc.params(args: T::Array[String], index: Integer).returns(T::Boolean)])
|
|
26
|
+
|
|
27
|
+
NEVER = T.let(->(_args, _index) { false },
|
|
28
|
+
T.proc.params(args: T::Array[String], index: Integer).returns(T::Boolean))
|
|
29
|
+
|
|
30
|
+
sig { params(args: T::Array[String]).void }
|
|
31
|
+
def initialize(args)
|
|
32
|
+
@args = args
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
sig { params(index: Integer).returns(Symbol) }
|
|
36
|
+
def kind_at(index)
|
|
37
|
+
positional(@args.fetch(index) { return :eoi }, index)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# dash's isoperand: a unary primary reads as an operand when nothing
|
|
41
|
+
# follows, or when the next word is a binary primary and more follows.
|
|
42
|
+
sig { params(args: T::Array[String], index: Integer).returns(T::Boolean) }
|
|
43
|
+
def self.operand_position?(args, index)
|
|
44
|
+
following = args.fetch(index + 1) { return true }
|
|
45
|
+
args.fetch(index + 2) { return false }
|
|
46
|
+
TestOperators.binary?(following)
|
|
47
|
+
end
|
|
48
|
+
private_class_method :operand_position?
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
sig { params(word: String, index: Integer).returns(Symbol) }
|
|
53
|
+
def positional(word, index)
|
|
54
|
+
kind = KINDS[word] || primary_kind(word)
|
|
55
|
+
return :operand if DEMOTIONS.fetch(kind, NEVER).call(@args, index)
|
|
56
|
+
|
|
57
|
+
kind
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
sig { params(word: String).returns(Symbol) }
|
|
61
|
+
def primary_kind(word)
|
|
62
|
+
return :unop if TestOperators.unary?(word)
|
|
63
|
+
return :binop if TestOperators.binary?(word)
|
|
64
|
+
|
|
65
|
+
:operand
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
module Builtins
|
|
6
|
+
# `times` — write the accumulated CPU times: line 1 the shell's own user and
|
|
7
|
+
# system time, line 2 the user and system time of its children, each formatted
|
|
8
|
+
# `<min>m<sec>s <min>m<sec>s` with six-decimal seconds (POSIX, matching dash).
|
|
9
|
+
# The values are non-deterministic, so behaviour is verified by format.
|
|
10
|
+
class Times < Base
|
|
11
|
+
extend T::Sig
|
|
12
|
+
|
|
13
|
+
sig { returns(Status) }
|
|
14
|
+
def call
|
|
15
|
+
tms = executor.system.times
|
|
16
|
+
write_times(tms)
|
|
17
|
+
success
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
sig { params(tms: Process::Tms).void }
|
|
23
|
+
def write_times(tms)
|
|
24
|
+
stdout.puts("#{clock(tms.utime)} #{clock(tms.stime)}")
|
|
25
|
+
stdout.puts("#{clock(tms.cutime)} #{clock(tms.cstime)}")
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
sig { params(seconds: Float).returns(String) }
|
|
29
|
+
def clock(seconds)
|
|
30
|
+
minutes, secs = seconds.divmod(60)
|
|
31
|
+
format('%<min>dm%<sec>fs', min: minutes, sec: secs)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
module Builtins
|
|
6
|
+
# `trap [action] signal...` sets, ignores ("" action) or resets ("-" action)
|
|
7
|
+
# the handler for each signal; EXIT (or 0) runs when the shell exits. With no
|
|
8
|
+
# operands it lists the active traps, one `trap -- 'action' NAME` line each in
|
|
9
|
+
# signal-number order. The action word is consumed only when a signal follows
|
|
10
|
+
# it, so `trap INT` resets INT (matching dash). A spec that names no signal is
|
|
11
|
+
# reported as "trap: SPEC: bad trap" and stops processing with status 1.
|
|
12
|
+
class Trap < Base
|
|
13
|
+
extend T::Sig
|
|
14
|
+
|
|
15
|
+
sig { returns(Status) }
|
|
16
|
+
def call
|
|
17
|
+
return list if operands.empty?
|
|
18
|
+
|
|
19
|
+
action, signals = split
|
|
20
|
+
apply(action, signals)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
# tuple, not Array: split feeds `apply(*split)`, a fixed-arity call.
|
|
26
|
+
sig { returns([T.nilable(String), T::Array[String]]) }
|
|
27
|
+
def split
|
|
28
|
+
return [nil, operands] if operands.size < 2
|
|
29
|
+
|
|
30
|
+
[operands.first, operands.drop(1)]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Apply left to right, stopping at (but keeping the work before) the first
|
|
34
|
+
# spec that names no signal — dash's behaviour for `trap x INT BADD TERM`.
|
|
35
|
+
sig { params(action: T.nilable(String), signals: T::Array[String]).returns(Status) }
|
|
36
|
+
def apply(action, signals)
|
|
37
|
+
bad = signals.find { |spec| !place(action, spec) }
|
|
38
|
+
bad ? bad_trap(bad) : success
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
sig { params(action: T.nilable(String), spec: String).returns(T.nilable(String)) }
|
|
42
|
+
def place(action, spec)
|
|
43
|
+
name = Signals.decode(spec)
|
|
44
|
+
change(name, action) if name
|
|
45
|
+
name
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
sig { params(name: String, action: T.nilable(String)).void }
|
|
49
|
+
def change(name, action)
|
|
50
|
+
if action && action != '-'
|
|
51
|
+
executor.trap_runner.set(name, action)
|
|
52
|
+
else
|
|
53
|
+
executor.trap_runner.reset(name)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
sig { params(spec: String).returns(Status) }
|
|
58
|
+
def bad_trap(spec)
|
|
59
|
+
stderr.puts("trap: #{spec}: bad trap")
|
|
60
|
+
failure(1)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
sig { returns(Status) }
|
|
64
|
+
def list
|
|
65
|
+
traps.listing.each { |name, action| stdout.puts(line(name, action)) }
|
|
66
|
+
success
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
sig { params(name: String, action: String).returns(String) }
|
|
70
|
+
def line(name, action)
|
|
71
|
+
"trap -- #{quote(action)} #{name}"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
sig { params(action: String).returns(String) }
|
|
75
|
+
def quote(action)
|
|
76
|
+
"'#{action.gsub("'", %q('"'"'))}'"
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
sig { returns(TrapTable) }
|
|
80
|
+
def traps
|
|
81
|
+
executor.state.traps
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
module Builtins
|
|
6
|
+
# `type name ...` — report how each name would be used: a shell keyword,
|
|
7
|
+
# function, special or regular builtin, or the executable found in PATH.
|
|
8
|
+
# Exit status 127 if any name is unknown (the message still goes to stdout).
|
|
9
|
+
class Type < Base
|
|
10
|
+
extend T::Sig
|
|
11
|
+
|
|
12
|
+
sig { returns(Status) }
|
|
13
|
+
def call
|
|
14
|
+
unknown = operands.reject { |name| report(name) }
|
|
15
|
+
unknown.empty? ? success : failure(127)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
sig { params(name: String).returns(T.nilable(String)) }
|
|
21
|
+
def report(name)
|
|
22
|
+
line = CommandLookup.new(executor).describe(name)
|
|
23
|
+
stdout.puts(line || "#{name}: not found")
|
|
24
|
+
line
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|