runtime/tools/exec/shell.rs
1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4// Copyright (c) 2026 2youg1 and the sprawling contributors
5
6//! Which interpreter a shell line runs under, and the command line each
7//! one takes (`crates/runtime/Spec.lean` ยง8-13-2 D30).
8//!
9//! The configuration names an interpreter and the machine says whether
10//! it has it; this file only holds the answer the two arrive at. A
11//! building that asked for PowerShell 7 on a machine without it is
12//! refused by name rather than handed the platform's shell, because a
13//! line written for one interpreter is a different language under the
14//! other, and a clear refusal beats a failure nobody can read.
15
16use std::path::{Path, PathBuf};
17use std::process::Command;
18
19use kernel::{AxCode, AxError, Interpreter};
20
21/// The shell arm as this run's machine and configuration leave it.
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub enum Shell {
24 /// No layer of this run's configuration offers the shell arm.
25 Absent,
26 /// A layer offers it under `asked`, and this machine has no usable
27 /// program for that interpreter.
28 Missing { asked: Interpreter },
29 /// The program a shell line is handed to, and which interpreter it is.
30 Found {
31 program: PathBuf,
32 interpreter: Interpreter,
33 },
34}
35
36impl Shell {
37 /// The command that runs `text`, and the name the result records the
38 /// interpreter under.
39 ///
40 /// # Errors
41 /// `ToolUnavailable` when the arm is not offered or its interpreter
42 /// is missing, each with the recovery that would work.
43 pub(super) fn command(&self, text: &str) -> Result<(Command, String), AxError> {
44 match self {
45 Shell::Absent => Err(AxError::failure(
46 AxCode::ToolUnavailable,
47 "run shell",
48 "no shell interpreter was found",
49 )
50 .with_recovery("use the program arm with an explicit executable")),
51 Shell::Missing { asked } => Err(missing(*asked)),
52 Shell::Found {
53 program,
54 interpreter,
55 } => {
56 let mut command = child::command(program);
57 command.args(flags(*interpreter)).arg(text);
58 Ok((command, interpreter_name(program)))
59 }
60 }
61 }
62
63 pub(super) fn container_command(&self, text: &str) -> Result<(Command, String), AxError> {
64 match self {
65 Shell::Absent => self.command(text),
66 Shell::Missing { asked }
67 | Shell::Found {
68 interpreter: asked, ..
69 } => {
70 let (program, arguments) = match asked {
71 Interpreter::System => ("/bin/sh", &["-c"][..]),
72 Interpreter::Pwsh => ("pwsh", flags(Interpreter::Pwsh)),
73 };
74 let mut command = child::command(program);
75 command.args(arguments).arg(text);
76 Ok((command, interpreter_name(Path::new(program))))
77 }
78 }
79 }
80
81 /// The sentence the tool's description ends with: which interpreter
82 /// a shell line runs under, in the spelling the result records, or
83 /// nothing when the arm is not usable, whose refusal says why at the
84 /// call. A model writes a different language for each interpreter.
85 pub(super) fn statement(&self) -> String {
86 match self {
87 Shell::Absent | Shell::Missing { .. } => String::new(),
88 Shell::Found { program, .. } => {
89 format!(" A shell line runs under {}.", interpreter_name(program))
90 }
91 }
92 }
93}
94
95/// The arguments that put one line in front of each interpreter.
96///
97/// PowerShell is told to load no profile and ask nothing, because a
98/// profile is one person's machine and a prompt would hold the command
99/// until its window closed.
100fn flags(interpreter: Interpreter) -> &'static [&'static str] {
101 match interpreter {
102 Interpreter::System if cfg!(windows) => &["/C"],
103 Interpreter::System => &["-c"],
104 Interpreter::Pwsh => &["-NoLogo", "-NoProfile", "-NonInteractive", "-Command"],
105 }
106}
107
108/// The refusal for an interpreter a building asked for and this machine
109/// does not have.
110fn missing(asked: Interpreter) -> AxError {
111 match asked {
112 Interpreter::System => AxError::failure(
113 AxCode::ToolUnavailable,
114 "run shell",
115 "this machine's own shell is missing or does not start",
116 )
117 .with_recovery(
118 "use the program arm with an explicit executable; `sprawling doctor` names what is \
119 wrong with the shell",
120 ),
121 Interpreter::Pwsh => AxError::failure(
122 AxCode::ToolUnavailable,
123 "run shell",
124 "this building runs shell lines under PowerShell 7 (pwsh), and this machine has no \
125 pwsh 7 on its search path",
126 )
127 .with_recovery(
128 "install PowerShell 7, or set `[sandbox] interpreter = \"system\"` in this building's \
129 CONFIG.toml",
130 ),
131 }
132}
133
134/// The name a result records an interpreter under: the program's file
135/// name without its extension, in lower case (`cmd`, `pwsh`, `sh`,
136/// `bash`, `zsh`).
137///
138/// The program's name rather than the configured word, because the
139/// tally asks which program ran, and `system` is a different program on
140/// every platform.
141pub(super) fn interpreter_name(program: &Path) -> String {
142 program
143 .file_stem()
144 .unwrap_or(program.as_os_str())
145 .to_string_lossy()
146 .to_lowercase()
147}