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,127 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
module Builtins
|
|
6
|
+
# `cd [dir]` — change the working directory, maintaining a *logical* PWD
|
|
7
|
+
# (not Dir.pwd, which resolves symlinks) plus OLDPWD. Defaults to $HOME.
|
|
8
|
+
class Cd < Base
|
|
9
|
+
extend T::Sig
|
|
10
|
+
|
|
11
|
+
# A CDPATH resolution result: the path to chdir to, and whether POSIX
|
|
12
|
+
# requires printing the resolved logical directory after a successful cd.
|
|
13
|
+
Target = Data.define(:path, :echo)
|
|
14
|
+
|
|
15
|
+
sig { returns(Status) }
|
|
16
|
+
def call
|
|
17
|
+
target = operands.first || executor.state.variables.get('HOME')
|
|
18
|
+
return report('HOME not set') unless target
|
|
19
|
+
|
|
20
|
+
target == '-' ? change_back : change_to(target)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
sig { params(dir: String).returns(Status) }
|
|
26
|
+
def change_to(dir)
|
|
27
|
+
target = resolve_cdpath(dir)
|
|
28
|
+
perform_chdir(target.path)
|
|
29
|
+
success_after_cdpath(target)
|
|
30
|
+
rescue Errno::ENOENT, Errno::ENOTDIR
|
|
31
|
+
report("#{dir}: No such file or directory")
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
sig { returns(Status) }
|
|
35
|
+
def change_back
|
|
36
|
+
dir = oldpwd
|
|
37
|
+
perform_chdir(dir)
|
|
38
|
+
success_with_echo
|
|
39
|
+
rescue Errno::ENOENT, Errno::ENOTDIR
|
|
40
|
+
report("#{dir}: No such file or directory")
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
sig { returns(String) }
|
|
44
|
+
def oldpwd
|
|
45
|
+
executor.state.variables.get('OLDPWD') || executor.state.variables.current_pwd
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
sig { params(dir: String).returns(Target) }
|
|
49
|
+
def resolve_cdpath(dir)
|
|
50
|
+
return Target.new(path: dir, echo: false) unless search_cdpath?(dir)
|
|
51
|
+
|
|
52
|
+
cdpath_targets(dir).find { |target| executor.system.directory?(target.path) } ||
|
|
53
|
+
Target.new(path: dir, echo: false)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
sig { params(dir: String).returns(T::Boolean) }
|
|
57
|
+
def search_cdpath?(dir)
|
|
58
|
+
cdpath? && relative_cdpath_operand?(dir)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
sig { params(dir: String).returns(T::Boolean) }
|
|
62
|
+
def relative_cdpath_operand?(dir)
|
|
63
|
+
!dir.start_with?('/') && !%w[. ..].include?(dir.split('/', 2).first)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
sig { returns(T::Boolean) }
|
|
67
|
+
def cdpath?
|
|
68
|
+
!!executor.state.variables.get('CDPATH')
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
sig { params(dir: String).returns(T::Array[Target]) }
|
|
72
|
+
def cdpath_targets(dir)
|
|
73
|
+
cdpath_entries.map { |prefix| cdpath_target(prefix, dir) }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
sig { returns(T::Array[String]) }
|
|
77
|
+
def cdpath_entries
|
|
78
|
+
T.must(executor.state.variables.get('CDPATH')).split(':', -1)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
sig { params(prefix: String, dir: String).returns(Target) }
|
|
82
|
+
def cdpath_target(prefix, dir)
|
|
83
|
+
Target.new(path: cdpath_path(prefix, dir), echo: cdpath_echo?(prefix))
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
sig { params(prefix: String, dir: String).returns(String) }
|
|
87
|
+
def cdpath_path(prefix, dir)
|
|
88
|
+
prefix.empty? ? dir : "#{prefix}/#{dir}"
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
sig { params(prefix: String).returns(T::Boolean) }
|
|
92
|
+
def cdpath_echo?(prefix)
|
|
93
|
+
!prefix.empty?
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
sig { params(dir: String).void }
|
|
97
|
+
def perform_chdir(dir)
|
|
98
|
+
executor.system.chdir(dir)
|
|
99
|
+
update_pwd(dir)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
sig { returns(Status) }
|
|
103
|
+
def success_with_echo
|
|
104
|
+
stdout.puts(executor.state.variables.current_pwd)
|
|
105
|
+
success
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
sig { params(target: Target).returns(Status) }
|
|
109
|
+
def success_after_cdpath(target)
|
|
110
|
+
target.echo ? success_with_echo : success
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
sig { params(dir: String).void }
|
|
114
|
+
def update_pwd(dir)
|
|
115
|
+
variables = executor.state.variables
|
|
116
|
+
variables.move_to(executor.system.expand_path(dir, variables.current_pwd))
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# A failed cd exits 2, matching the dash oracle (POSIX only asks for >0).
|
|
120
|
+
sig { params(message: String).returns(Status) }
|
|
121
|
+
def report(message)
|
|
122
|
+
stderr.puts("rush: cd: #{message}")
|
|
123
|
+
failure(2)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
module Builtins
|
|
6
|
+
# `:` — the null command; expands its arguments and always succeeds.
|
|
7
|
+
class Colon < Base
|
|
8
|
+
extend T::Sig
|
|
9
|
+
|
|
10
|
+
sig { returns(Status) }
|
|
11
|
+
def call
|
|
12
|
+
success
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
module Builtins
|
|
6
|
+
# `command [-p] name [arg ...]` runs name as a command, skipping shell
|
|
7
|
+
# functions (so a function can call the builtin it shadows); -p searches
|
|
8
|
+
# the system default PATH (confstr _CS_PATH) for an external instead of
|
|
9
|
+
# $PATH, without changing the child's environment. `command -v name`
|
|
10
|
+
# prints how name resolves — the name itself for a keyword / function /
|
|
11
|
+
# builtin, the path for an external — and -V the long `type` form; both
|
|
12
|
+
# exit 127 for an unknown name, 0 with no operand, and -V outranks -v.
|
|
13
|
+
class Command < Base
|
|
14
|
+
extend T::Sig
|
|
15
|
+
|
|
16
|
+
sig { returns(Status) }
|
|
17
|
+
def call
|
|
18
|
+
opts = CommandOptions.new(operands)
|
|
19
|
+
bad = opts.illegal
|
|
20
|
+
bad ? illegal_option(bad) : dispatch(opts)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
sig { params(opts: CommandOptions).returns(Status) }
|
|
26
|
+
def dispatch(opts)
|
|
27
|
+
return no_name(opts) unless opts.name
|
|
28
|
+
return verbose(opts) if opts.verbose?
|
|
29
|
+
return verify(opts) if opts.verify?
|
|
30
|
+
|
|
31
|
+
run(opts)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# dash exits 0 for a bare `command`, `command -v` and `command -V` alike
|
|
35
|
+
# (probed); the bare run form is the colon builtin's no-op.
|
|
36
|
+
sig { params(opts: CommandOptions).returns(Status) }
|
|
37
|
+
def no_name(opts)
|
|
38
|
+
return success if opts.verbose? || opts.verify?
|
|
39
|
+
|
|
40
|
+
Colon.new(executor, [], io).call
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
sig { params(opts: CommandOptions).returns(Status) }
|
|
44
|
+
def verify(opts)
|
|
45
|
+
print_terse(lookup(opts).find(opts.name))
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
sig { params(match: CommandLookup::Match).returns(Status) }
|
|
49
|
+
def print_terse(match)
|
|
50
|
+
return failure(127) unless match.known?
|
|
51
|
+
|
|
52
|
+
stdout.puts(match.terse)
|
|
53
|
+
success
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
sig { params(opts: CommandOptions).returns(Status) }
|
|
57
|
+
def verbose(opts)
|
|
58
|
+
print_description(T.must(opts.name), lookup(opts).find(opts.name))
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
sig { params(name: String, match: CommandLookup::Match).returns(Status) }
|
|
62
|
+
def print_description(name, match)
|
|
63
|
+
return not_described(name) unless match.known?
|
|
64
|
+
|
|
65
|
+
stdout.puts(match.describe)
|
|
66
|
+
success
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
sig { params(name: String).returns(Status) }
|
|
70
|
+
def not_described(name)
|
|
71
|
+
stdout.puts("#{name}: not found")
|
|
72
|
+
failure(127)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
sig { params(opts: CommandOptions).returns(CommandLookup) }
|
|
76
|
+
def lookup(opts)
|
|
77
|
+
opts.default_path? ? default_lookup : CommandLookup.new(executor)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
sig { returns(CommandLookup) }
|
|
81
|
+
def default_lookup
|
|
82
|
+
CommandLookup.new(executor, path: executor.system.default_path)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
sig { params(opts: CommandOptions).returns(Status) }
|
|
86
|
+
def run(opts)
|
|
87
|
+
name = T.must(opts.name)
|
|
88
|
+
builtin = executor.builtins.lookup(name)
|
|
89
|
+
builtin ? run_builtin(builtin, opts) : run_external(name, opts)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
sig { params(builtin: T.class_of(Base), opts: CommandOptions).returns(Status) }
|
|
93
|
+
def run_builtin(builtin, opts)
|
|
94
|
+
builtin.new(executor, opts.operands, io, command_environment).call
|
|
95
|
+
rescue Error => e
|
|
96
|
+
raise unless ErrorPolicy.decision(:command, e) == :demote2
|
|
97
|
+
|
|
98
|
+
demoted_error(e)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
sig { params(error: StandardError).returns(Status) }
|
|
102
|
+
def demoted_error(error)
|
|
103
|
+
stderr.puts("rush: #{error.message}")
|
|
104
|
+
failure(2)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
sig { params(name: String, opts: CommandOptions).returns(Status) }
|
|
108
|
+
def run_external(name, opts)
|
|
109
|
+
external = External.new(executor, opts.operands, io, command_environment)
|
|
110
|
+
return external.call unless opts.default_path?
|
|
111
|
+
|
|
112
|
+
run_on_default_path(name, external)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# The default-PATH resolution happens here in the shell (dash does the
|
|
116
|
+
# same): the resolved file is executed while argv[0] stays the name as
|
|
117
|
+
# typed, and $PATH is neither consulted nor mutated.
|
|
118
|
+
sig { params(name: String, external: External).returns(Status) }
|
|
119
|
+
def run_on_default_path(name, external)
|
|
120
|
+
path = default_lookup.executable_path(name)
|
|
121
|
+
path ? external.call_file(path) : not_found(name)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
sig { params(name: String).returns(Status) }
|
|
125
|
+
def not_found(name)
|
|
126
|
+
stderr.puts("rush: #{name}: not found")
|
|
127
|
+
failure(127)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
sig { params(letter: String).returns(Status) }
|
|
131
|
+
def illegal_option(letter)
|
|
132
|
+
stderr.puts("rush: command: Illegal option #{letter}")
|
|
133
|
+
failure(2)
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
module Builtins
|
|
6
|
+
# The parsed option prefix of the `command` builtin's argv: leading
|
|
7
|
+
# clusters of -p / -v / -V letters (repeats accumulate; -V outranks -v,
|
|
8
|
+
# as in dash), an optional `--` terminator, then the operands. A letter
|
|
9
|
+
# outside the set is recorded as `illegal` (e.g. '-z'), which `command`
|
|
10
|
+
# reports with status 2; `+x` or a lone `-` is an operand, not options.
|
|
11
|
+
class CommandOptions
|
|
12
|
+
extend T::Sig
|
|
13
|
+
|
|
14
|
+
LETTERS = T.let({ 'p' => :default_path, 'v' => :verify, 'V' => :verbose }.freeze,
|
|
15
|
+
T::Hash[String, Symbol])
|
|
16
|
+
|
|
17
|
+
# A dash followed by letters, except the bare `--` terminator (a lone
|
|
18
|
+
# dash or a +cluster is an operand — dash-probed).
|
|
19
|
+
CLUSTER = /\A-(?!-\z)./
|
|
20
|
+
|
|
21
|
+
sig { returns(T::Array[String]) }
|
|
22
|
+
attr_reader :operands
|
|
23
|
+
|
|
24
|
+
sig { returns(T.nilable(String)) }
|
|
25
|
+
attr_reader :illegal
|
|
26
|
+
|
|
27
|
+
sig { params(args: T::Array[String]).void }
|
|
28
|
+
def initialize(args)
|
|
29
|
+
@args = args
|
|
30
|
+
@flags = T.let([], T::Array[Symbol])
|
|
31
|
+
@illegal = T.let(nil, T.nilable(String))
|
|
32
|
+
@operands = T.let(consume, T::Array[String])
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
sig { returns(T.nilable(String)) }
|
|
36
|
+
def name
|
|
37
|
+
operands.first
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
sig { returns(T::Boolean) }
|
|
41
|
+
def default_path?
|
|
42
|
+
@flags.include?(:default_path)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
sig { returns(T::Boolean) }
|
|
46
|
+
def verify?
|
|
47
|
+
@flags.include?(:verify)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
sig { returns(T::Boolean) }
|
|
51
|
+
def verbose?
|
|
52
|
+
@flags.include?(:verbose)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
sig { returns(T::Array[String]) }
|
|
58
|
+
def consume
|
|
59
|
+
index = count_clusters
|
|
60
|
+
@args.drop(arg_at(index) == '--' ? index + 1 : index)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
sig { returns(Integer) }
|
|
64
|
+
def count_clusters
|
|
65
|
+
index = 0
|
|
66
|
+
index += 1 while take(index)
|
|
67
|
+
index
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
sig { params(index: Integer).returns(T::Boolean) }
|
|
71
|
+
def take(index)
|
|
72
|
+
return false unless cluster?(index)
|
|
73
|
+
|
|
74
|
+
arg_at(index).chars.drop(1).all? { |letter| record(letter) }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
sig { params(index: Integer).returns(T::Boolean) }
|
|
78
|
+
def cluster?(index)
|
|
79
|
+
arg_at(index).match?(CLUSTER)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
sig { params(index: Integer).returns(String) }
|
|
83
|
+
def arg_at(index)
|
|
84
|
+
@args.fetch(index, '')
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# The letter's flag (feeding take's all?-driven cluster scan), nil once
|
|
88
|
+
# the letter proves illegal.
|
|
89
|
+
sig { params(letter: String).returns(T.nilable(Symbol)) }
|
|
90
|
+
def record(letter)
|
|
91
|
+
flag = LETTERS.fetch(letter) { record_illegal(letter) }
|
|
92
|
+
@flags << flag if flag
|
|
93
|
+
flag
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
sig { params(letter: String).returns(NilClass) }
|
|
97
|
+
def record_illegal(letter)
|
|
98
|
+
@illegal = "-#{letter}"
|
|
99
|
+
nil
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
module Builtins
|
|
6
|
+
# `continue [n]` — resume the n-th enclosing loop's next iteration
|
|
7
|
+
# (default 1); the shared POSIX 2.9.5 semantics live in LoopJump.
|
|
8
|
+
class Continue < LoopJump
|
|
9
|
+
extend T::Sig
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
sig { returns(T.class_of(LoopControl)) }
|
|
14
|
+
def signal
|
|
15
|
+
ContinueSignal
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
module Builtins
|
|
6
|
+
# Shared engine of `export` and `readonly`: each operand is `name[=value]` —
|
|
7
|
+
# the value is assigned first when one is given, then the subclass's marking
|
|
8
|
+
# applies on the variable table. Both always report success.
|
|
9
|
+
class Declare < Base
|
|
10
|
+
extend T::Sig
|
|
11
|
+
|
|
12
|
+
sig { returns(Status) }
|
|
13
|
+
def call
|
|
14
|
+
operands.each { |operand| declare(operand) }
|
|
15
|
+
success
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
# Apply this builtin's marking to one name[=value] operand.
|
|
21
|
+
sig { params(_operand: String).void }
|
|
22
|
+
def declare(_operand)
|
|
23
|
+
raise NotImplementedError
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
# The shell's builtin commands and the default name -> implementation table.
|
|
6
|
+
module Builtins
|
|
7
|
+
extend T::Sig
|
|
8
|
+
|
|
9
|
+
# The builtins available in Slice 1. Later slices extend this table.
|
|
10
|
+
DEFAULTS = {
|
|
11
|
+
':' => Colon, 'true' => True, 'false' => False,
|
|
12
|
+
'echo' => Echo, 'exit' => Exit, 'pwd' => Pwd, 'cd' => Cd,
|
|
13
|
+
'break' => Break, 'continue' => Continue, 'return' => Return,
|
|
14
|
+
'test' => Test, '[' => Test, 'set' => Set, 'shift' => Shift,
|
|
15
|
+
'export' => Export, 'unset' => Unset, 'eval' => Eval, 'getopts' => Getopts, 'read' => Read,
|
|
16
|
+
'printf' => Printf, '.' => Dot, 'readonly' => Readonly, 'exec' => Exec,
|
|
17
|
+
'local' => Local, 'type' => Type, 'command' => Command, 'trap' => Trap,
|
|
18
|
+
'kill' => Kill, 'alias' => Alias, 'unalias' => Unalias,
|
|
19
|
+
'hash' => Hash, 'times' => Times, 'umask' => Umask, 'ulimit' => Ulimit, 'wait' => Wait,
|
|
20
|
+
'jobs' => Jobs, 'fg' => Fg, 'bg' => Bg
|
|
21
|
+
}.freeze
|
|
22
|
+
|
|
23
|
+
sig { returns(Registry) }
|
|
24
|
+
def self.default_registry
|
|
25
|
+
Registry.new.tap { |registry| DEFAULTS.each { |name, klass| registry.register(name, klass) } }
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
module Builtins
|
|
6
|
+
# `. filename` — read the file and run it in the current shell, so its
|
|
7
|
+
# functions and variables persist. The file is read command by command
|
|
8
|
+
# (SourceRunner), so an alias or function it defines shapes its own later
|
|
9
|
+
# lines. Redirections apply to the file's commands (executor.with_io);
|
|
10
|
+
# exit/break/continue propagate to the caller, but `return` is bounded to the
|
|
11
|
+
# dot script — it stops the file and becomes the `.` command's status (POSIX
|
|
12
|
+
# 2.14), unlike eval. A missing file or a syntax error is a special-builtin
|
|
13
|
+
# error that aborts a non-interactive shell with status 2 (BuiltinError); a
|
|
14
|
+
# missing operand is a plain usage error that does not abort. PATH search for
|
|
15
|
+
# an unqualified name arrives later.
|
|
16
|
+
class Dot < Base
|
|
17
|
+
extend T::Sig
|
|
18
|
+
|
|
19
|
+
sig { returns(Status) }
|
|
20
|
+
def call
|
|
21
|
+
return usage if operands.empty?
|
|
22
|
+
|
|
23
|
+
source(operands.first)
|
|
24
|
+
rescue Errno::ENOENT
|
|
25
|
+
raise BuiltinError, ".: #{operands.first}: No such file or directory"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
sig { params(path: T.untyped).returns(Status) }
|
|
31
|
+
def source(path)
|
|
32
|
+
text = executor.system.read_file(path)
|
|
33
|
+
executor.with_io(io) { run_text(text) }
|
|
34
|
+
rescue ParseError => e
|
|
35
|
+
raise BuiltinError, ".: #{e}"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
sig { params(text: String).returns(Status) }
|
|
39
|
+
def run_text(text)
|
|
40
|
+
SourceRunner.new(executor, text).run
|
|
41
|
+
rescue ReturnSignal => e
|
|
42
|
+
Status.new(e.code)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
sig { returns(Status) }
|
|
46
|
+
def usage
|
|
47
|
+
stderr.puts('rush: .: filename argument required')
|
|
48
|
+
failure(2)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
module Builtins
|
|
6
|
+
# `echo [-n] [arg...]` — joins arguments with spaces and adds a trailing
|
|
7
|
+
# newline; `-n` suppresses it. XSI escape processing (\n, \t, \c, ...) is
|
|
8
|
+
# added in Phase 2.
|
|
9
|
+
class Echo < Base
|
|
10
|
+
extend T::Sig
|
|
11
|
+
|
|
12
|
+
sig { returns(Status) }
|
|
13
|
+
def call
|
|
14
|
+
stdout.write(render)
|
|
15
|
+
success
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
sig { returns(String) }
|
|
21
|
+
def render
|
|
22
|
+
body = printed.join(' ')
|
|
23
|
+
newline? ? "#{body}\n" : body
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
sig { returns(T::Boolean) }
|
|
27
|
+
def newline?
|
|
28
|
+
!operands.first.eql?('-n')
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
sig { returns(T::Array[String]) }
|
|
32
|
+
def printed
|
|
33
|
+
newline? ? operands : operands.drop(1)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
module Builtins
|
|
6
|
+
# `eval [arg ...]` — join the arguments with spaces and run the result in
|
|
7
|
+
# the current shell, returning its status. The input is read command by
|
|
8
|
+
# command (SourceRunner), so an `alias` or function defined by one command
|
|
9
|
+
# shapes how the next is parsed. A redirection on eval applies to the parsed
|
|
10
|
+
# commands (executor.with_io), and exit/break/continue/return all propagate
|
|
11
|
+
# to the enclosing context. A syntax error is a special-builtin error: it
|
|
12
|
+
# aborts a non-interactive shell with status 2 (BuiltinError), after the
|
|
13
|
+
# complete commands before it have run.
|
|
14
|
+
class Eval < Base
|
|
15
|
+
extend T::Sig
|
|
16
|
+
|
|
17
|
+
sig { returns(Status) }
|
|
18
|
+
def call
|
|
19
|
+
executor.with_io(io) { SourceRunner.new(executor, operands.join(' ')).run }
|
|
20
|
+
rescue ParseError => e
|
|
21
|
+
raise BuiltinError, "eval: #{e}"
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
module Builtins
|
|
6
|
+
# `exec [command [arg ...]]` — with no command, make the redirections
|
|
7
|
+
# permanent for the rest of the shell; with a command, replace the shell
|
|
8
|
+
# process with it (applying the redirections first). A failed exec aborts the
|
|
9
|
+
# shell with 127 (not found) or 126 (not executable), matching dash.
|
|
10
|
+
class Exec < Base
|
|
11
|
+
extend T::Sig
|
|
12
|
+
|
|
13
|
+
sig { returns(Status) }
|
|
14
|
+
def call
|
|
15
|
+
operands.empty? ? redirect_shell : replace_process
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
sig { returns(Status) }
|
|
21
|
+
def redirect_shell
|
|
22
|
+
executor.replace_io(io)
|
|
23
|
+
success
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
sig { returns(T.noreturn) }
|
|
27
|
+
def replace_process
|
|
28
|
+
executor.system.exec(command_environment, operands, options)
|
|
29
|
+
rescue Errno::ENOENT
|
|
30
|
+
abort_exec('not found', 127)
|
|
31
|
+
rescue Errno::EACCES
|
|
32
|
+
abort_exec('Permission denied', 126)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
sig { params(reason: String, code: Integer).returns(T.noreturn) }
|
|
36
|
+
def abort_exec(reason, code)
|
|
37
|
+
report_exec_error(reason)
|
|
38
|
+
raise ExitSignal, code
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
sig { params(reason: String).void }
|
|
42
|
+
def report_exec_error(reason)
|
|
43
|
+
stderr.puts("rush: #{operands.first}: #{reason}")
|
|
44
|
+
rescue Errno::EBADF
|
|
45
|
+
nil
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
sig { returns(T::Hash[T.any(Integer, Symbol), T.untyped]) }
|
|
49
|
+
def options
|
|
50
|
+
io.to_spawn_options.merge(close_others: true)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
module Builtins
|
|
6
|
+
# `exit [n]` — unwind to the top level via ExitSignal, carrying the status n
|
|
7
|
+
# (the executor's exiting status when no operand is given: the last command's
|
|
8
|
+
# status normally, or the shell's terminating status inside the EXIT trap).
|
|
9
|
+
# A non-numeric n is a special-builtin error. With a stopped job in the
|
|
10
|
+
# table the first exit is refused — "You have stopped jobs.", status 0 —
|
|
11
|
+
# and an immediately repeated exit goes through (dash's stoppedjobs
|
|
12
|
+
# guard, probed batch and interactive alike; only the interactive prompt
|
|
13
|
+
# loop re-arms the warning).
|
|
14
|
+
class Exit < Base
|
|
15
|
+
extend T::Sig
|
|
16
|
+
|
|
17
|
+
sig { returns(Status) }
|
|
18
|
+
def call
|
|
19
|
+
return refuse_stopped if stopped_jobs?
|
|
20
|
+
|
|
21
|
+
raise ExitSignal, code
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
# Stopped jobs block the exit once (the Control's dash-job_warning
|
|
27
|
+
# window arms on the first refusal).
|
|
28
|
+
sig { returns(T::Boolean) }
|
|
29
|
+
def stopped_jobs?
|
|
30
|
+
executor.jobs.ordered.any?(&:stopped?) && executor.jobs.control.warn_exit?
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
sig { returns(Status) }
|
|
34
|
+
def refuse_stopped
|
|
35
|
+
stderr.puts('You have stopped jobs.')
|
|
36
|
+
success
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
sig { returns(Integer) }
|
|
40
|
+
def code
|
|
41
|
+
operands.empty? ? executor.trap_runner.exiting_status : numeric_operand(operands.fetch(0))
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
module Builtins
|
|
6
|
+
# `export name[=value] ...` — mark each name for inclusion in the environment
|
|
7
|
+
# of subsequently executed commands; see Declare for the shared engine.
|
|
8
|
+
# (The `-p` listing form arrives with the variable-printing slice.)
|
|
9
|
+
class Export < Declare
|
|
10
|
+
extend T::Sig
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
sig { params(operand: String).void }
|
|
15
|
+
def declare(operand)
|
|
16
|
+
executor.state.variables.export_operand(operand)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|