Skip to main content

gammaloop_api/commands/
run.rs

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    /// Command block names to execute (in order)
48    #[arg(value_name = "BLOCK_NAME")]
49    pub(crate) block_names: Vec<String>,
50
51    /// Semicolon-separated inline commands executed after the named blocks.
52    #[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}