1use clap::Args;
2use gammalooprs::settings::RuntimeSettings;
3use schemars::JsonSchema;
4use serde::{Deserialize, Serialize};
5
6use color_eyre::{eyre::eyre, Result};
7
8use crate::{
9 command_parser::{split_command_line, split_command_list},
10 state::{CommandHistory, RunHistory, State},
11 CLISettings,
12};
13
14use super::{CommandExecution, Commands};
15
16pub const MAX_RUN_DEPTH: usize = 100;
17
18#[derive(Debug, Clone)]
19pub enum PreparedCommand {
20 Plain(CommandHistory),
21 Run {
22 command: CommandHistory,
23 plan: PreparedRun,
24 },
25}
26
27#[derive(Debug, Clone)]
28pub struct PreparedCommandsBlock {
29 pub name: String,
30 pub commands: Vec<PreparedCommand>,
31}
32
33#[derive(Debug, Clone, Default)]
34pub struct PreparedRun {
35 pub blocks: Vec<PreparedCommandsBlock>,
36 pub commands: Vec<PreparedCommand>,
37}
38
39impl PreparedRun {
40 pub fn is_empty(&self) -> bool {
41 self.blocks.is_empty() && self.commands.is_empty()
42 }
43}
44
45#[derive(Debug, Args, Serialize, Deserialize, Clone, JsonSchema, PartialEq)]
46pub struct Run {
47 #[arg(value_name = "BLOCK_NAME")]
49 pub(crate) block_names: Vec<String>,
50
51 #[arg(short = 'c', long)]
53 pub(crate) commands: Option<String>,
54}
55
56impl Run {
57 pub fn selected_block_names(&self) -> &[String] {
58 self.block_names.as_slice()
59 }
60
61 pub fn is_noop(&self) -> bool {
62 self.block_names.is_empty()
63 && self
64 .commands
65 .as_ref()
66 .map(|commands| commands.trim().is_empty())
67 .unwrap_or(true)
68 }
69
70 pub fn canonical_raw_string(&self) -> String {
71 let mut parts = vec!["run".to_string()];
72 parts.extend(self.block_names.iter().map(|name| shell_quote(name)));
73 if let Some(commands) = self.commands.as_ref() {
74 if !commands.trim().is_empty() {
75 parts.push("-c".to_string());
76 parts.push(shell_quote(commands));
77 }
78 }
79 parts.join(" ")
80 }
81
82 pub fn parse_inline_commands(&self) -> Result<Vec<CommandHistory>> {
83 let Some(commands) = self.commands.as_deref() else {
84 return Ok(Vec::new());
85 };
86
87 split_command_list(commands)
88 .map_err(|_| {
89 eyre!("Could not parse run -c command list: unmatched quotes or trailing escape")
90 })?
91 .into_iter()
92 .enumerate()
93 .map(|(index, command)| {
94 CommandHistory::from_raw_string(&command).map_err(|err| {
95 eyre!(
96 "Failed to parse run -c command #{} '{}': {}",
97 index + 1,
98 command,
99 err
100 )
101 })
102 })
103 .collect()
104 }
105
106 pub fn prepare(&self, run_history: &RunHistory, depth: usize) -> Result<PreparedRun> {
107 if depth > MAX_RUN_DEPTH {
108 return Err(eyre!(
109 "Maximum nested run depth of {} reached while preparing {}",
110 MAX_RUN_DEPTH,
111 self.canonical_raw_string()
112 ));
113 }
114
115 let selected_blocks = run_history.select_command_blocks(self.selected_block_names())?;
116 let commands = prepare_command_histories_with_context(
117 &self.parse_inline_commands()?,
118 run_history,
119 depth + 1,
120 "run -c",
121 )?;
122 let blocks = selected_blocks
123 .into_iter()
124 .map(|block| {
125 let block_context = format!("command block '{}'", block.name);
126 Ok(PreparedCommandsBlock {
127 name: block.name,
128 commands: prepare_command_histories_with_context(
129 &block.commands,
130 run_history,
131 depth + 1,
132 &block_context,
133 )?,
134 })
135 })
136 .collect::<Result<Vec<_>>>()?;
137
138 Ok(PreparedRun { blocks, commands })
139 }
140
141 pub fn run(
142 &self,
143 state: &mut State,
144 global_settings: &mut CLISettings,
145 default_runtime_settings: &mut RuntimeSettings,
146 run_history: &mut RunHistory,
147 ) -> Result<CommandExecution> {
148 let mut session_state = crate::session::CliSessionState::default();
149 let mut session = crate::session::CliSession::new(
150 state,
151 run_history,
152 global_settings,
153 default_runtime_settings,
154 &mut session_state,
155 );
156 session.execute_command(CommandHistory::new_with_raw(
157 Commands::Run(self.clone()),
158 self.canonical_raw_string(),
159 ))
160 }
161}
162
163pub fn prepare_command_histories(
164 commands: &[CommandHistory],
165 run_history: &RunHistory,
166 depth: usize,
167) -> Result<Vec<PreparedCommand>> {
168 prepare_command_histories_with_context(commands, run_history, depth, "commands")
169}
170
171pub fn prepare_command_histories_with_context(
172 commands: &[CommandHistory],
173 run_history: &RunHistory,
174 depth: usize,
175 context: &str,
176) -> Result<Vec<PreparedCommand>> {
177 commands
178 .iter()
179 .cloned()
180 .enumerate()
181 .map(|(index, command)| {
182 PreparedCommand::prepare(command, run_history, depth).map_err(|err| {
183 eyre!(
184 "Failed to validate {} command #{}: {}",
185 context,
186 index + 1,
187 err
188 )
189 })
190 })
191 .collect()
192}
193
194impl PreparedCommand {
195 pub fn prepare(
196 command: CommandHistory,
197 run_history: &RunHistory,
198 depth: usize,
199 ) -> Result<Self> {
200 match command.command.clone() {
201 Commands::Run(run) => Ok(Self::Run {
202 command,
203 plan: run.prepare(run_history, depth)?,
204 }),
205 _ => Ok(Self::Plain(command)),
206 }
207 }
208}
209
210fn shell_quote(value: &str) -> String {
211 if value.is_empty() {
212 return "''".to_string();
213 }
214
215 if split_command_line(value)
216 .map(|split| split.len() == 1 && split[0] == value)
217 .unwrap_or(false)
218 && !value.contains(';')
219 {
220 return value.to_string();
221 }
222
223 format!("'{}'", value.replace('\'', "'\"'\"'"))
224}