mom_task/
cli.rs

1#[cfg(test)]
2#[path = "cli_test.rs"]
3mod cli_test;
4
5use clap::ArgAction;
6use colored::{ColoredString, Colorize};
7use serde::{Deserialize, Serialize};
8use std::error::Error;
9use std::path::PathBuf;
10use std::sync::{Arc, Mutex};
11use std::{env, fmt};
12
13use crate::args::ArgsContext;
14use crate::mom_file_paths::{GlobalMomFilePath, MomFilePaths, PathIterator, SingleMomFilePath};
15use crate::mom_files::MomFile;
16use crate::mom_files_container::MomFilesContainer;
17use crate::print_utils::MomOutput;
18use crate::types::DynErrResult;
19
20const HELP: &str = "For documentation check https://github.com/adrianmrit/mom.";
21
22/// Holds the data for running the given task.
23struct TaskSubcommand {
24    /// Task to run, if given
25    pub(crate) task: String,
26    /// Args to run the command with
27    pub(crate) args_context: ArgsContext,
28}
29
30/// Enum of available mom file versions
31#[derive(Deserialize, Serialize)]
32pub(crate) enum Version {
33    #[serde(rename = "1")]
34    V1,
35}
36
37/// Argument errors
38#[derive(Debug, PartialEq, Eq)]
39enum ArgsError {
40    /// Raised when no task to run is given
41    MissingTaskArg,
42}
43
44impl fmt::Display for ArgsError {
45    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
46        match *self {
47            ArgsError::MissingTaskArg => write!(f, "No task was given."),
48        }
49    }
50}
51
52impl Error for ArgsError {
53    fn description(&self) -> &str {
54        match *self {
55            ArgsError::MissingTaskArg => "no task given",
56        }
57    }
58
59    fn cause(&self) -> Option<&dyn Error> {
60        None
61    }
62}
63
64/// Sets the color when printing the task name
65fn colorize_task_name(val: &str) -> ColoredString {
66    val.bright_cyan()
67}
68
69/// Sets the color when printing the mom file path
70fn colorize_mom_file_path(val: &str) -> ColoredString {
71    val.bright_blue()
72}
73
74struct Mom {
75    mom_files: MomFilesContainer,
76}
77
78impl Mom {
79    /// Creates a new instance of `Mom`
80    fn new() -> Self {
81        Self {
82            mom_files: MomFilesContainer::new(),
83        }
84    }
85
86    fn get_mom_file_lock(&mut self, path: PathBuf) -> DynErrResult<Arc<Mutex<MomFile>>> {
87        let mom_file_ptr = match self.mom_files.read_mom_file(path.clone()) {
88            Ok(val) => val,
89            Err(e) => {
90                let e = format!("{}:\n{}", &path.to_string_lossy().red(), e);
91                return Err(e.into());
92            }
93        };
94        Ok(mom_file_ptr)
95    }
96
97    /// prints mom file paths and their tasks
98    fn print_tasks_list(&mut self, paths: PathIterator) -> DynErrResult<()> {
99        let mut found = false;
100        for path in paths {
101            found = true;
102            let mom_file_ptr = self.get_mom_file_lock(path.clone())?;
103            let mom_file_lock = mom_file_ptr.lock().unwrap();
104
105            println!("{}:", colorize_mom_file_path(&path.to_string_lossy()));
106
107            let mut task_names = mom_file_lock.get_public_task_names();
108            task_names.sort();
109            if task_names.is_empty() {
110                println!("  {}", "No tasks found.".red());
111            } else {
112                for task in task_names {
113                    println!(" - {}", colorize_task_name(task));
114                }
115            }
116        }
117        if !found {
118            println!("No mom files found.");
119        }
120        Ok(())
121    }
122
123    /// Prints help for the given task
124    fn print_task_info(&mut self, paths: PathIterator, task: &str) -> DynErrResult<()> {
125        for path in paths {
126            let mom_file_ptr = self.get_mom_file_lock(path.clone())?;
127            let mom_file_lock = mom_file_ptr.lock().unwrap();
128
129            let task = mom_file_lock.clone_task(task);
130
131            match task {
132                Some(task) => {
133                    println!("{}:", colorize_mom_file_path(&path.to_string_lossy()));
134                    print!(" - {}", colorize_task_name(task.get_name()));
135                    if task.is_private() {
136                        print!(" {}", "(private)".red());
137                    }
138                    println!();
139                    let prefix = "     ";
140                    match task.get_help().trim() {
141                        "" => println!("{}{}", prefix, "No help to display".yellow()),
142                        help => {
143                            //                 " -   "  Two spaces after the dash
144                            let help_lines: Vec<&str> = help.lines().collect();
145                            println!(
146                                "{}{}",
147                                prefix,
148                                help_lines.join(&format!("\n{}", prefix)).green()
149                            )
150                        }
151                    }
152                    return Ok(());
153                }
154                None => continue,
155            }
156        }
157        Err(format!("Task {} not found", task).into())
158    }
159
160    /// Runs the given task
161    fn run_task(
162        &mut self,
163        paths: PathIterator,
164        task: &str,
165        args: &ArgsContext,
166        dry_run: bool,
167    ) -> DynErrResult<()> {
168        for path in paths {
169            let mom_file_ptr = self.get_mom_file_lock(path.clone())?;
170            let mom_file_lock = mom_file_ptr.lock().unwrap();
171
172            let task = mom_file_lock.clone_public_task(task);
173
174            match task {
175                Some(task) => {
176                    println!("{}", &path.to_string_lossy().mom_info());
177                    return match task.run(args, &mom_file_lock, dry_run) {
178                        Ok(val) => Ok(val),
179                        Err(e) => {
180                            let e = format!("{}:\n{}", &path.to_string_lossy().red(), e);
181                            Err(e.into())
182                        }
183                    };
184                }
185                None => continue,
186            }
187        }
188        Err(format!("Task {} not found", task).into())
189    }
190}
191
192// TODO: Handle
193impl TaskSubcommand {
194    /// Returns a new TaskSubcommand
195    pub(crate) fn new(args: &clap::ArgMatches) -> Result<TaskSubcommand, ArgsError> {
196        let (task_name, task_args) = match args.subcommand() {
197            None => return Err(ArgsError::MissingTaskArg),
198            Some(command) => command,
199        };
200
201        Ok(TaskSubcommand {
202            task: String::from(task_name),
203            args_context: ArgsContext::from(task_args.clone()),
204        })
205    }
206}
207
208/// Executes the program. If errors are encountered during the execution these
209/// are returned immediately. The wrapping method needs to take care of formatting
210/// and displaying these errors appropriately.
211pub fn exec() -> DynErrResult<()> {
212    let app = clap::Command::new(clap::crate_name!())
213        .version(clap::crate_version!())
214        .about(clap::crate_description!())
215        .author(clap::crate_authors!())
216        .after_help(HELP)
217        .allow_external_subcommands(true)
218        .arg(
219            clap::Arg::new("list")
220                .short('l')
221                .long("list")
222                .help("Lists configuration files that can be reached from the current directory")
223                .action(ArgAction::SetTrue),
224        )
225        .arg(
226            clap::Arg::new("list-tasks")
227                .short('t')
228                .long("list-tasks")
229                .help("Lists tasks")
230                .conflicts_with_all(["task-info"])
231                .action(ArgAction::SetTrue),
232        )
233        .arg(
234            clap::Arg::new("task-info")
235                .short('i')
236                .long("task-info")
237                .action(ArgAction::Set)
238                .help("Displays information about the given task")
239                .value_name("TASK"),
240        )
241        .arg(
242            clap::Arg::new("dry")
243                .long("dry")
244                .action(ArgAction::SetTrue)
245                .help("Runs the task in dry mode, i.e. without executing any commands"),
246        )
247        .arg(
248            clap::Arg::new("file")
249                .short('f')
250                .long("file")
251                .action(ArgAction::Set)
252                .help("Search for tasks in the given file")
253                .value_name("FILE"),
254        )
255        .arg(
256            clap::Arg::new("global")
257                .short('g')
258                .long("global")
259                .help("Search for tasks in ~/mom/mom.global.{yml,yaml}")
260                .conflicts_with_all(["file"])
261                .action(ArgAction::SetTrue),
262        );
263    let matches = app.get_matches();
264
265    let current_dir = env::current_dir()?;
266    let mut mom = Mom::new();
267
268    let mom_file_paths: PathIterator = match matches.get_one::<String>("file") {
269        None => match matches.get_one::<bool>("global").cloned().unwrap_or(false) {
270            true => GlobalMomFilePath::new(),
271            false => MomFilePaths::new(&current_dir),
272        },
273        Some(file_path) => SingleMomFilePath::new(file_path),
274    };
275
276    let dry_run = matches.get_one::<bool>("dry").cloned().unwrap_or(false);
277
278    if matches
279        .get_one::<bool>("list-tasks")
280        .cloned()
281        .unwrap_or(false)
282    {
283        mom.print_tasks_list(mom_file_paths)?;
284        return Ok(());
285    };
286
287    if let Some(task_name) = matches.get_one::<String>("task-info") {
288        mom.print_task_info(mom_file_paths, task_name)?;
289        return Ok(());
290    };
291
292    if matches.get_one::<bool>("list").cloned().unwrap_or(false) {
293        for path in mom_file_paths {
294            println!("{}", colorize_mom_file_path(&path.to_string_lossy()));
295        }
296        return Ok(());
297    }
298
299    let task_command = TaskSubcommand::new(&matches)?;
300
301    mom.run_task(
302        mom_file_paths,
303        &task_command.task,
304        &task_command.args_context,
305        dry_run,
306    )
307}