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,39 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
module Builtins
|
|
6
|
+
# `printf format [arg ...]` — write the arguments under the control of the
|
|
7
|
+
# format, which is reused until the arguments are exhausted. Writes no
|
|
8
|
+
# trailing newline of its own. A present non-numeric argument to a numeric
|
|
9
|
+
# conversion is reported and treated as 0 (exit status 1).
|
|
10
|
+
class Printf < Base
|
|
11
|
+
extend T::Sig
|
|
12
|
+
|
|
13
|
+
sig { returns(Status) }
|
|
14
|
+
def call
|
|
15
|
+
return usage if operands.empty?
|
|
16
|
+
|
|
17
|
+
text, ok = PrintfFormatter.new(operands.drop(1)).render(operands.first)
|
|
18
|
+
stdout.write(text)
|
|
19
|
+
report(ok)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
sig { params(valid: T::Boolean).returns(Status) }
|
|
25
|
+
def report(valid)
|
|
26
|
+
return success if valid
|
|
27
|
+
|
|
28
|
+
stderr.puts('rush: printf: expected numeric value')
|
|
29
|
+
failure
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
sig { returns(Status) }
|
|
33
|
+
def usage
|
|
34
|
+
stderr.puts('rush: printf: usage: printf format [arguments]')
|
|
35
|
+
failure(2)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# typed: false
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'strscan'
|
|
5
|
+
|
|
6
|
+
module Rush
|
|
7
|
+
module Builtins
|
|
8
|
+
# Renders a printf template against its arguments: literal text (with
|
|
9
|
+
# backslash escapes), %% and the %[flags][width][.prec]conv conversions,
|
|
10
|
+
# cycling the template while arguments remain. Flags/width/precision defer to
|
|
11
|
+
# Ruby's format; numeric conversions coerce their argument (a present
|
|
12
|
+
# non-number is reported via the ok flag and treated as 0, a missing one as
|
|
13
|
+
# 0 silently). %b and octal escapes arrive in a later slice. Returns [text, ok].
|
|
14
|
+
class PrintfFormatter
|
|
15
|
+
NUMERIC = %w[d i o u x X].freeze
|
|
16
|
+
|
|
17
|
+
# Scans and walks one pass of a printf template: literal runs and resolved
|
|
18
|
+
# backslash escapes pass straight through, while each %conversion is handed
|
|
19
|
+
# back to the formatter to render against the next argument (double
|
|
20
|
+
# dispatch), so the scanning lives here and the formatting stays there.
|
|
21
|
+
class Template < StringScanner
|
|
22
|
+
ESCAPES = { '\\' => '\\', 'a' => "\a", 'b' => "\b", 'f' => "\f",
|
|
23
|
+
'n' => "\n", 'r' => "\r", 't' => "\t", 'v' => "\v" }.freeze
|
|
24
|
+
ESCAPE_TABLE = EscapeTable.new(ESCAPES).freeze
|
|
25
|
+
SPEC = /\A([-+ #0]*\d*(?:\.\d+)?)([diouxXcs%])/
|
|
26
|
+
|
|
27
|
+
def emit(formatter)
|
|
28
|
+
out = +''
|
|
29
|
+
while (char = getch)
|
|
30
|
+
out << piece(formatter, char)
|
|
31
|
+
end
|
|
32
|
+
out
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def piece(formatter, char)
|
|
38
|
+
return conversion(formatter) if char == '%'
|
|
39
|
+
return escape if char == '\\'
|
|
40
|
+
|
|
41
|
+
char
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def conversion(formatter)
|
|
45
|
+
return '%' unless scan(SPEC)
|
|
46
|
+
|
|
47
|
+
flags, conv = captures
|
|
48
|
+
formatter.convert(T.must(flags), T.must(conv))
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def escape
|
|
52
|
+
ESCAPE_TABLE.escape(getch)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def initialize(args)
|
|
57
|
+
@args = args
|
|
58
|
+
@cursor = 0
|
|
59
|
+
@ok = true
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def render(template)
|
|
63
|
+
start = @cursor
|
|
64
|
+
text = Template.new(template).emit(self)
|
|
65
|
+
return [text, @ok] if @cursor == start || @cursor >= @args.size
|
|
66
|
+
|
|
67
|
+
rest, = render(template)
|
|
68
|
+
[text + rest, @ok]
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Render one %conversion (called back from Template): %% is a literal %, a
|
|
72
|
+
# numeric/char/string conversion consumes and formats the next argument.
|
|
73
|
+
def convert(flags, conv)
|
|
74
|
+
return '%' if conv == '%'
|
|
75
|
+
|
|
76
|
+
arg = take
|
|
77
|
+
return numeric(flags, conv, arg) if NUMERIC.include?(conv)
|
|
78
|
+
return format("%#{flags}s", first_char(arg)) if conv == 'c'
|
|
79
|
+
|
|
80
|
+
format("%#{flags}s", arg)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
private
|
|
84
|
+
|
|
85
|
+
def numeric(flags, conv, arg)
|
|
86
|
+
format("%#{flags}#{conv}", to_int(arg))
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def to_int(arg)
|
|
90
|
+
return 0 if arg.empty?
|
|
91
|
+
|
|
92
|
+
Integer(arg, exception: false) || invalid
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def invalid
|
|
96
|
+
@ok = false
|
|
97
|
+
0
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def take
|
|
101
|
+
arg = @args.fetch(@cursor, '')
|
|
102
|
+
@cursor += 1
|
|
103
|
+
arg
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def first_char(arg)
|
|
107
|
+
arg.each_char.first
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
module Builtins
|
|
6
|
+
# `pwd` — print the shell's logical working directory.
|
|
7
|
+
class Pwd < Base
|
|
8
|
+
extend T::Sig
|
|
9
|
+
|
|
10
|
+
sig { returns(Status) }
|
|
11
|
+
def call
|
|
12
|
+
stdout.puts(executor.state.variables.pwd)
|
|
13
|
+
success
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
module Builtins
|
|
6
|
+
# `read [-r] var ...` — read a logical line from stdin, split it on IFS and
|
|
7
|
+
# assign the fields to the named variables (the last variable takes the
|
|
8
|
+
# remainder). Repeated or clustered `-r` flags are accepted and `--` ends
|
|
9
|
+
# option parsing; any other option or an invalid variable name is a usage
|
|
10
|
+
# error. Without -r a backslash preserves the next character (shielding it
|
|
11
|
+
# from field splitting) and a trailing backslash continues onto the next
|
|
12
|
+
# physical line; with -r the line is taken verbatim. Returns non-zero when
|
|
13
|
+
# end of file arrives before a newline (assigning what was read).
|
|
14
|
+
class Read < Base
|
|
15
|
+
extend T::Sig
|
|
16
|
+
|
|
17
|
+
NAME = /\A[A-Za-z_]\w*\z/
|
|
18
|
+
|
|
19
|
+
sig { returns(Status) }
|
|
20
|
+
def call
|
|
21
|
+
parsed = parse_operands
|
|
22
|
+
return usage_error unless valid?(parsed)
|
|
23
|
+
|
|
24
|
+
raw, names = T.must(parsed)
|
|
25
|
+
read_and_assign(raw, names)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
sig { returns(T.nilable([T::Boolean, T::Array[String]])) }
|
|
31
|
+
def parse_operands
|
|
32
|
+
args = operands.dup
|
|
33
|
+
mode = consume_options(args)
|
|
34
|
+
return if mode == :invalid
|
|
35
|
+
|
|
36
|
+
[mode == :raw, args]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
sig { params(args: T::Array[String], mode: Symbol).returns(Symbol) }
|
|
40
|
+
def consume_options(args, mode: :cooked)
|
|
41
|
+
return mode unless option?(option = args.first)
|
|
42
|
+
|
|
43
|
+
args.shift
|
|
44
|
+
return mode if option == '--'
|
|
45
|
+
return :invalid unless option.to_s.delete_prefix('-').chars.all?('r')
|
|
46
|
+
|
|
47
|
+
consume_options(args, mode: :raw)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
sig { params(arg: T.nilable(String)).returns(T::Boolean) }
|
|
51
|
+
def option?(arg)
|
|
52
|
+
!!arg&.start_with?('-') && arg != '-'
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
sig { params(parsed: T.nilable([T::Boolean, T::Array[String]])).returns(T::Boolean) }
|
|
56
|
+
def valid?(parsed)
|
|
57
|
+
!!parsed && parsed.last.any? && parsed.last.all? { |name| name.match?(NAME) }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
sig { params(raw: T::Boolean, names: T::Array[String]).returns(Status) }
|
|
61
|
+
def read_and_assign(raw, names)
|
|
62
|
+
result = ReadInput.new(stdin, raw, pending: -> { executor.trap_runner.pending? }).call
|
|
63
|
+
return failure unless result
|
|
64
|
+
|
|
65
|
+
chars, complete = result
|
|
66
|
+
assign(chars, names)
|
|
67
|
+
complete ? success : failure
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
sig { params(chars: T::Array[Expansion::ReadChar], names: T::Array[String]).void }
|
|
71
|
+
def assign(chars, names)
|
|
72
|
+
fields = Expansion::ReadSplitter.new(ifs, names.size).split(chars)
|
|
73
|
+
names.each_with_index { |name, index| executor.state.variables.assign(name, fields.fetch(index)) }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
sig { returns(T.nilable(String)) }
|
|
77
|
+
def ifs
|
|
78
|
+
executor.state.variables.get('IFS')
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
sig { returns(T.untyped) }
|
|
82
|
+
def stdin
|
|
83
|
+
io.get(0)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
sig { returns(Status) }
|
|
87
|
+
def usage_error
|
|
88
|
+
stderr.puts('rush: read: arg count')
|
|
89
|
+
failure(2)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'io/wait'
|
|
5
|
+
|
|
6
|
+
module Rush
|
|
7
|
+
module Builtins
|
|
8
|
+
# Reads one logical line for the `read` builtin from the given stream.
|
|
9
|
+
# Without -r a backslash escapes the next character (annotating it as
|
|
10
|
+
# protected from field splitting) and a trailing unescaped backslash joins
|
|
11
|
+
# the next physical line. `call` returns the annotated characters plus
|
|
12
|
+
# whether the line was complete — terminated by a newline before end of
|
|
13
|
+
# file, dash's read exit-status rule.
|
|
14
|
+
class ReadInput
|
|
15
|
+
extend T::Sig
|
|
16
|
+
|
|
17
|
+
# Adds caught-signal polling only to real IO; StringIO-backed reads stay
|
|
18
|
+
# direct and deterministic.
|
|
19
|
+
class LineReader
|
|
20
|
+
extend T::Sig
|
|
21
|
+
|
|
22
|
+
POLL_INTERVAL = 0.01
|
|
23
|
+
|
|
24
|
+
sig { params(stream: T.untyped, pending: T.proc.returns(T::Boolean)).void }
|
|
25
|
+
def initialize(stream, pending)
|
|
26
|
+
@stream = stream
|
|
27
|
+
@pending = pending
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
sig { returns(T.nilable(String)) }
|
|
31
|
+
def gets
|
|
32
|
+
stream = @stream
|
|
33
|
+
return stream.gets unless stream.is_a?(IO)
|
|
34
|
+
|
|
35
|
+
wait_for_line(stream)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
sig { params(stream: IO).returns(T.nilable(String)) }
|
|
41
|
+
def wait_for_line(stream)
|
|
42
|
+
line = +''
|
|
43
|
+
catch(:physical_line) { loop { read_step(stream, line) } }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
sig { params(stream: IO, line: String).void }
|
|
47
|
+
def read_step(stream, line)
|
|
48
|
+
throw(:interrupted, line) if @pending.call
|
|
49
|
+
char = next_char(stream)
|
|
50
|
+
throw(:physical_line, line.empty? ? nil : line) unless char
|
|
51
|
+
append_char(line, char) if char.is_a?(String)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
sig { params(line: String, char: String).void }
|
|
55
|
+
def append_char(line, char)
|
|
56
|
+
line << char
|
|
57
|
+
throw(:physical_line, line) if char == "\n"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
sig { params(stream: IO).returns(T.untyped) }
|
|
61
|
+
def next_char(stream)
|
|
62
|
+
return :wait_readable unless stream.wait_readable(POLL_INTERVAL)
|
|
63
|
+
|
|
64
|
+
read_char(stream)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
sig { params(stream: IO).returns(T.untyped) }
|
|
68
|
+
def read_char(stream)
|
|
69
|
+
stream.read_nonblock(1)
|
|
70
|
+
rescue IO::WaitReadable
|
|
71
|
+
:wait_readable
|
|
72
|
+
rescue EOFError
|
|
73
|
+
nil
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# A line whose backslash-pairs all match, leaving one lone trailing
|
|
78
|
+
# backslash: a line-continuation request.
|
|
79
|
+
CONTINUED = /\A(?:[^\\]|\\.)*\\\z/m
|
|
80
|
+
|
|
81
|
+
# One output character: an optional escaping backslash and the character.
|
|
82
|
+
PAIR = /(\\)?(.)/m
|
|
83
|
+
|
|
84
|
+
sig { params(stdin: T.untyped, raw: T::Boolean, pending: T.proc.returns(T::Boolean)).void }
|
|
85
|
+
def initialize(stdin, raw, pending: -> { false })
|
|
86
|
+
@reader = LineReader.new(stdin, pending)
|
|
87
|
+
@raw = raw
|
|
88
|
+
@chars = T.let([], T::Array[Expansion::ReadChar])
|
|
89
|
+
@complete = T.let(false, T::Boolean)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
sig { returns(T.nilable([T::Array[Expansion::ReadChar], T::Boolean])) }
|
|
93
|
+
def call
|
|
94
|
+
partial = catch(:interrupted) do
|
|
95
|
+
gather
|
|
96
|
+
return [@chars, @complete]
|
|
97
|
+
end
|
|
98
|
+
partial_result(partial)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
private
|
|
102
|
+
|
|
103
|
+
sig { params(partial: String).returns(T.nilable([T::Array[Expansion::ReadChar], T::Boolean])) }
|
|
104
|
+
def partial_result(partial)
|
|
105
|
+
decode(partial)
|
|
106
|
+
[@chars, false] unless @chars.empty?
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
sig { void }
|
|
110
|
+
def gather
|
|
111
|
+
line = @reader.gets
|
|
112
|
+
return @complete = false unless line
|
|
113
|
+
|
|
114
|
+
@complete = line.end_with?("\n")
|
|
115
|
+
gather if decode(line.delete_suffix("\n"))
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Appends the line's annotated characters; true means a continuation is
|
|
119
|
+
# pending and the next physical line belongs to this logical one.
|
|
120
|
+
sig { params(text: String).returns(T::Boolean) }
|
|
121
|
+
def decode(text)
|
|
122
|
+
return cook(text) unless @raw
|
|
123
|
+
|
|
124
|
+
verbatim(text)
|
|
125
|
+
false
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
sig { params(text: String).void }
|
|
129
|
+
def verbatim(text)
|
|
130
|
+
text.each_char { |char| @chars << [char, false] }
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# A continuation leaves a zero-width escaped joint (['', true]) where the
|
|
134
|
+
# lines meet: dash's region boundary, which ends a whitespace-delimiter
|
|
135
|
+
# run in the splitter without contributing a character.
|
|
136
|
+
sig { params(text: String).returns(T::Boolean) }
|
|
137
|
+
def cook(text)
|
|
138
|
+
pending = text.match?(CONTINUED)
|
|
139
|
+
body = pending ? T.must(text[0..-2]) : text
|
|
140
|
+
body.scan(PAIR) { |escape, char| @chars << [T.must(char), escape == '\\'] }
|
|
141
|
+
(@chars << ['', true]) if pending
|
|
142
|
+
pending
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
module Builtins
|
|
6
|
+
# `readonly name[=value] ...` — mark each name read only, so a later
|
|
7
|
+
# assignment or unset aborts (ReadonlyError); see Declare for the shared
|
|
8
|
+
# engine. (The `-p` listing form arrives with the variable-printing slice.)
|
|
9
|
+
class Readonly < Declare
|
|
10
|
+
extend T::Sig
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
sig { params(operand: String).void }
|
|
15
|
+
def declare(operand)
|
|
16
|
+
executor.state.variables.readonly_operand(operand)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
module Builtins
|
|
6
|
+
# O(1) name -> builtin class lookup, populated by Builtins.default_registry.
|
|
7
|
+
class Registry
|
|
8
|
+
extend T::Sig
|
|
9
|
+
|
|
10
|
+
sig { void }
|
|
11
|
+
def initialize
|
|
12
|
+
@builtins = {}
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
sig { params(name: String, klass: T.class_of(Base)).void }
|
|
16
|
+
def register(name, klass)
|
|
17
|
+
@builtins[name] = klass
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
sig { params(name: String).returns(T.class_of(Base)) }
|
|
21
|
+
def fetch(name)
|
|
22
|
+
@builtins.fetch(name)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
sig { params(name: T.nilable(String)).returns(T.nilable(T.class_of(Base))) }
|
|
26
|
+
def lookup(name)
|
|
27
|
+
@builtins[name]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
sig { params(name: T.nilable(String)).returns(T::Boolean) }
|
|
31
|
+
def key?(name)
|
|
32
|
+
@builtins.key?(name)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
module Builtins
|
|
6
|
+
# `return [n]` — return from the current function with status n (default: the
|
|
7
|
+
# last command's status). A non-numeric n is a special-builtin error.
|
|
8
|
+
class Return < Base
|
|
9
|
+
extend T::Sig
|
|
10
|
+
|
|
11
|
+
sig { returns(T.noreturn) }
|
|
12
|
+
def call
|
|
13
|
+
raise ReturnSignal, code
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
sig { returns(Integer) }
|
|
19
|
+
def code
|
|
20
|
+
operands.first ? numeric_operand(operands.fetch(0)) : executor.state.last_status.exitstatus
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
module Builtins
|
|
6
|
+
# `set [-/+ options] [--] [arg ...]` — toggle shell options (-x/+x and the
|
|
7
|
+
# `-o name`/`+o name` long form) and, when operands follow (or after `--`),
|
|
8
|
+
# replace the positional parameters. With no operands the parameters are left
|
|
9
|
+
# unchanged; an unknown option is ignored. A bare trailing `-o` writes the
|
|
10
|
+
# current settings table, a bare `+o` the re-input form (POSIX XCU set).
|
|
11
|
+
# Options: -a/allexport, -C/noclobber, -e/errexit, -n/noexec, -u/nounset,
|
|
12
|
+
# -x/xtrace, -f/noglob, -v/verbose, pipefail.
|
|
13
|
+
class Set < Base
|
|
14
|
+
extend T::Sig
|
|
15
|
+
|
|
16
|
+
OPTIONS = Options::LETTERS
|
|
17
|
+
LONG = Options::LONG
|
|
18
|
+
|
|
19
|
+
sig { returns(Status) }
|
|
20
|
+
def call
|
|
21
|
+
rest = strip_options(operands)
|
|
22
|
+
executor.state.positional.replace(rest) if rest
|
|
23
|
+
success
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
sig { params(args: T::Array[String]).returns(T.nilable(T::Array[String])) }
|
|
29
|
+
def strip_options(args)
|
|
30
|
+
return if args.empty?
|
|
31
|
+
|
|
32
|
+
positionals(args, consume_options(args))
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
sig { params(args: T::Array[String]).returns(Integer) }
|
|
36
|
+
def consume_options(args)
|
|
37
|
+
index = 0
|
|
38
|
+
while (cluster = OptionCluster.parse(args[index]))
|
|
39
|
+
index += advance(cluster, args, index)
|
|
40
|
+
end
|
|
41
|
+
index
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
sig { params(cluster: OptionCluster, args: T::Array[String], index: Integer).returns(Integer) }
|
|
45
|
+
def advance(cluster, args, index)
|
|
46
|
+
return apply_long(cluster.sign, args[index + 1]) if cluster.letters.join == 'o'
|
|
47
|
+
|
|
48
|
+
apply(cluster)
|
|
49
|
+
1
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
sig { params(args: T::Array[String], index: Integer).returns(T.nilable(T::Array[String])) }
|
|
53
|
+
def positionals(args, index)
|
|
54
|
+
return if index == args.size
|
|
55
|
+
|
|
56
|
+
args[index] == '--' ? args.drop(index + 1) : args.drop(index)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
sig { params(cluster: OptionCluster).void }
|
|
60
|
+
def apply(cluster)
|
|
61
|
+
cluster.each_letter { |char, sign| toggle(OPTIONS.fetch(char, nil), sign) }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# With a name, toggle that option; bare `set -o` prints the option
|
|
65
|
+
# table (format follows dash), bare `set +o` the lines that re-create
|
|
66
|
+
# the settings when re-input (POSIX XCU set).
|
|
67
|
+
sig { params(sign: T.nilable(String), name: T.nilable(String)).returns(Integer) }
|
|
68
|
+
def apply_long(sign, name)
|
|
69
|
+
return toggle_long(sign, name) if name
|
|
70
|
+
|
|
71
|
+
options = executor.state.options
|
|
72
|
+
print_lines(sign == '-' ? ['Current option settings', *options.settings_rows] : options.reinput_lines)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
sig { params(sign: T.nilable(String), name: String).returns(Integer) }
|
|
76
|
+
def toggle_long(sign, name)
|
|
77
|
+
toggle(LONG.fetch(name, nil), sign)
|
|
78
|
+
2
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Prints a listing and consumes just the bare -o/+o flag itself.
|
|
82
|
+
sig { params(lines: T::Array[String]).returns(Integer) }
|
|
83
|
+
def print_lines(lines)
|
|
84
|
+
lines.each { |line| stdout.puts(line) }
|
|
85
|
+
1
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
sig { params(option: T.nilable(Symbol), sign: T.nilable(String)).void }
|
|
89
|
+
def toggle(option, sign)
|
|
90
|
+
return unless option
|
|
91
|
+
|
|
92
|
+
enabled = sign == '-'
|
|
93
|
+
# Monitor carries side effects (SIGTSTP disposition, tty check) and can
|
|
94
|
+
# refuse, so it routes through the job-control policy, not bare state.
|
|
95
|
+
return executor.state.set_option(option, enabled) unless option == :monitor
|
|
96
|
+
|
|
97
|
+
enabled ? executor.job_control.enable(stderr) : executor.job_control.disable
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
module Builtins
|
|
6
|
+
# `shift [n]` — discard the first n positional parameters (default 1) and
|
|
7
|
+
# renumber the rest. As a special builtin, two errors abort a non-interactive
|
|
8
|
+
# shell with status 2 (BuiltinError, firing the EXIT trap), matching dash: a
|
|
9
|
+
# non-numeric / negative operand ("Illegal number"), and asking to shift more
|
|
10
|
+
# than $# ("can't shift that many"). Extra operands past the first are ignored.
|
|
11
|
+
class Shift < Base
|
|
12
|
+
extend T::Sig
|
|
13
|
+
|
|
14
|
+
sig { returns(Status) }
|
|
15
|
+
def call
|
|
16
|
+
raise BuiltinError, "shift: can't shift that many" if count > state.positional.size
|
|
17
|
+
|
|
18
|
+
state.positional.shift(count)
|
|
19
|
+
success
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
sig { returns(ShellState) }
|
|
25
|
+
def state
|
|
26
|
+
executor.state
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
sig { returns(Integer) }
|
|
30
|
+
def count
|
|
31
|
+
operands.empty? ? 1 : numeric_operand(T.must(operands.first))
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
module Builtins
|
|
6
|
+
# `test EXPR` / `[ EXPR ]` — evaluate a conditional expression, returning
|
|
7
|
+
# success (0) when true and failure (1) when false. A malformed expression
|
|
8
|
+
# is reported on stderr with exit status 2. Invoked as `[`, the expression
|
|
9
|
+
# must be terminated by a closing `]`.
|
|
10
|
+
class Test < Base
|
|
11
|
+
extend T::Sig
|
|
12
|
+
|
|
13
|
+
sig { returns(Status) }
|
|
14
|
+
def call
|
|
15
|
+
evaluate(bracketed? ? without_close(operands) : operands)
|
|
16
|
+
rescue TestError => e
|
|
17
|
+
report(e.message)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
sig { returns(T::Boolean) }
|
|
23
|
+
def bracketed?
|
|
24
|
+
argv.first == '['
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
sig { params(ops: T::Array[String]).returns(T::Array[String]) }
|
|
28
|
+
def without_close(ops)
|
|
29
|
+
*body, close = ops
|
|
30
|
+
raise TestError, "missing `]'" unless close == ']'
|
|
31
|
+
|
|
32
|
+
body
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
sig { params(ops: T::Array[String]).returns(Status) }
|
|
36
|
+
def evaluate(ops)
|
|
37
|
+
context = TestContext.new(executor.system, FdOperand.new(io))
|
|
38
|
+
TestExpr.new(ops, context).true? ? success : failure
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
sig { params(message: String).returns(Status) }
|
|
42
|
+
def report(message)
|
|
43
|
+
stderr.puts("rush: #{argv.first}: #{message}")
|
|
44
|
+
failure(2)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
module Builtins
|
|
6
|
+
# The impure inputs to one test/[ evaluation: the filesystem port plus the
|
|
7
|
+
# resolver that projects shell-level fd bindings onto unary operands.
|
|
8
|
+
class TestContext
|
|
9
|
+
extend T::Sig
|
|
10
|
+
|
|
11
|
+
sig { params(files: SystemCalls, fd_operand: FdOperand).void }
|
|
12
|
+
def initialize(files, fd_operand)
|
|
13
|
+
@files = files
|
|
14
|
+
@fd_operand = fd_operand
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
sig { params(operator: String, operand: String).returns(T::Boolean) }
|
|
18
|
+
def unary(operator, operand)
|
|
19
|
+
resolved = @fd_operand.resolve(operator, operand)
|
|
20
|
+
TestOperators.unary(operator).call(@files, resolved)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|