1use std::process::ExitCode;
2
3use crate::cli::{Cli, ShellCommand};
4use crate::config::{LoadOptions, load_config};
5use crate::error::SboxError;
6use crate::resolve::{ResolutionTarget, resolve_execution_plan};
7
8pub fn execute(cli: &Cli, command: &ShellCommand) -> Result<ExitCode, SboxError> {
9 let loaded = load_config(&LoadOptions {
10 workspace: cli.workspace.clone(),
11 config: cli.config.clone(),
12 })?;
13
14 let initial_shell = resolve_initial_shell(command)?;
15 let initial_plan = resolve_execution_plan(
16 cli,
17 &loaded,
18 ResolutionTarget::Shell,
19 std::slice::from_ref(&initial_shell),
20 )?;
21 let shell = resolve_shell(cli, command, &loaded.config, &initial_plan.profile_name)?;
22 let plan = if shell == initial_shell {
23 initial_plan
24 } else {
25 resolve_execution_plan(cli, &loaded, ResolutionTarget::Shell, &[shell])?
26 };
27 crate::exec::validate_execution_safety(
28 &plan,
29 crate::exec::strict_security_enabled(cli, &loaded.config),
30 )?;
31
32 match plan.mode {
33 crate::config::model::ExecutionMode::Host => crate::exec::execute_host(&plan),
34 crate::config::model::ExecutionMode::Sandbox => match plan.backend {
35 crate::config::BackendKind::Podman => {
36 crate::backend::podman::execute_interactive(&plan)
37 }
38 crate::config::BackendKind::Docker => {
39 crate::backend::docker::execute_interactive(&plan)
40 }
41 },
42 }
43}
44
45fn resolve_shell(
46 cli: &Cli,
47 command: &ShellCommand,
48 config: &crate::config::model::Config,
49 active_profile: &str,
50) -> Result<String, SboxError> {
51 if let Some(shell) = &command.shell {
52 return Ok(shell.clone());
53 }
54
55 if let Some(profile) = config.profiles.get(active_profile)
56 && let Some(shell) = &profile.shell
57 {
58 return Ok(shell.clone());
59 }
60
61 if let Some(profile_name) = &cli.profile
62 && let Some(profile) = config.profiles.get(profile_name)
63 && let Some(shell) = &profile.shell
64 {
65 return Ok(shell.clone());
66 }
67
68 if let Some(shell) = std::env::var_os("SHELL") {
69 let shell = shell.to_string_lossy().trim().to_string();
70 if !shell.is_empty() {
71 return Ok(shell);
72 }
73 }
74
75 Ok(default_shell().to_string())
76}
77
78fn resolve_initial_shell(command: &ShellCommand) -> Result<String, SboxError> {
79 if let Some(shell) = &command.shell {
80 return Ok(shell.clone());
81 }
82
83 if let Some(shell) = std::env::var_os("SHELL") {
84 let shell = shell.to_string_lossy().trim().to_string();
85 if !shell.is_empty() {
86 return Ok(shell);
87 }
88 }
89
90 Ok(default_shell().to_string())
91}
92
93fn default_shell() -> &'static str {
95 #[cfg(windows)]
96 {
97 "cmd.exe"
98 }
99 #[cfg(not(windows))]
100 {
101 "/bin/sh"
102 }
103}
104
105#[cfg(test)]
106mod tests {
107 use indexmap::IndexMap;
108
109 use super::resolve_shell;
110 use crate::cli::{Cli, Commands, ShellCommand};
111 use crate::config::model::{Config, ExecutionMode, ProfileConfig};
112
113 fn base_cli() -> Cli {
114 Cli {
115 config: None,
116 workspace: None,
117 backend: None,
118 image: None,
119 profile: None,
120 mode: None,
121 strict_security: false,
122 verbose: 0,
123 quiet: false,
124 command: Commands::Shell(ShellCommand::default()),
125 }
126 }
127
128 fn base_config() -> Config {
129 let mut profiles = IndexMap::new();
130 profiles.insert(
131 "default".to_string(),
132 ProfileConfig {
133 mode: ExecutionMode::Sandbox,
134 image: None,
135 network: Some("off".to_string()),
136 writable: Some(true),
137 require_pinned_image: None,
138 require_lockfile: None,
139 role: None,
140 lockfile_files: Vec::new(),
141 pre_run: Vec::new(),
142 network_allow: Vec::new(),
143 ports: Vec::new(),
144 capabilities: None,
145 no_new_privileges: Some(true),
146 read_only_rootfs: None,
147 reuse_container: None,
148 shell: Some("/bin/bash".to_string()),
149 writable_paths: None,
150 },
151 );
152
153 Config {
154 version: 1,
155 runtime: None,
156 workspace: None,
157 identity: None,
158 image: None,
159 environment: None,
160 mounts: Vec::new(),
161 caches: Vec::new(),
162 secrets: Vec::new(),
163 profiles,
164 dispatch: IndexMap::new(),
165
166 package_manager: None,
167 }
168 }
169
170 #[test]
171 fn shell_flag_overrides_profile_shell() {
172 let cli = base_cli();
173 let config = base_config();
174 let command = ShellCommand {
175 shell: Some("/bin/zsh".to_string()),
176 };
177
178 let shell =
179 resolve_shell(&cli, &command, &config, "default").expect("shell should resolve");
180 assert_eq!(shell, "/bin/zsh");
181 }
182
183 #[test]
184 fn profile_shell_is_used_when_flag_is_absent() {
185 let cli = base_cli();
186 let config = base_config();
187
188 let shell = resolve_shell(&cli, &ShellCommand::default(), &config, "default")
189 .expect("shell should resolve");
190 assert_eq!(shell, "/bin/bash");
191 }
192}