Skip to main content

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}