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 = Command::new(program);
57 command.args(flags(*interpreter)).arg(text);
58 Ok((command, interpreter_name(program)))
59 }
60 }
61 }
62
63 /// The sentence the tool's description ends with: which interpreter
64 /// a shell line runs under, in the spelling the result records, or
65 /// nothing when the arm is not usable, whose refusal says why at the
66 /// call. A model writes a different language for each interpreter.
67 pub(super) fn statement(&self) -> String {
68 match self {
69 Shell::Absent | Shell::Missing { .. } => String::new(),
70 Shell::Found { program, .. } => {
71 format!(" A shell line runs under {}.", interpreter_name(program))
72 }
73 }
74 }
75}
76
77/// The arguments that put one line in front of each interpreter.
78///
79/// PowerShell is told to load no profile and ask nothing, because a
80/// profile is one person's machine and a prompt would hold the command
81/// until its window closed.
82fn flags(interpreter: Interpreter) -> &'static [&'static str] {
83 match interpreter {
84 Interpreter::System if cfg!(windows) => &["/C"],
85 Interpreter::System => &["-c"],
86 Interpreter::Pwsh => &["-NoLogo", "-NoProfile", "-NonInteractive", "-Command"],
87 }
88}
89
90/// The refusal for an interpreter a building asked for and this machine
91/// does not have.
92fn missing(asked: Interpreter) -> AxError {
93 match asked {
94 Interpreter::System => AxError::failure(
95 AxCode::ToolUnavailable,
96 "run shell",
97 "this machine's own shell is missing or does not start",
98 )
99 .with_recovery(
100 "use the program arm with an explicit executable; `sprawling doctor` names what is \
101 wrong with the shell",
102 ),
103 Interpreter::Pwsh => AxError::failure(
104 AxCode::ToolUnavailable,
105 "run shell",
106 "this building runs shell lines under PowerShell 7 (pwsh), and this machine has no \
107 pwsh 7 on its search path",
108 )
109 .with_recovery(
110 "install PowerShell 7, or set `[sandbox] interpreter = \"system\"` in this building's \
111 CONFIG.toml",
112 ),
113 }
114}
115
116/// The name a result records an interpreter under: the program's file
117/// name without its extension, in lower case (`cmd`, `pwsh`, `sh`,
118/// `bash`, `zsh`).
119///
120/// The program's name rather than the configured word, because the
121/// tally asks which program ran, and `system` is a different program on
122/// every platform.
123pub(super) fn interpreter_name(program: &Path) -> String {
124 program
125 .file_stem()
126 .unwrap_or(program.as_os_str())
127 .to_string_lossy()
128 .to_lowercase()
129}