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
22struct TaskSubcommand {
24 pub(crate) task: String,
26 pub(crate) args_context: ArgsContext,
28}
29
30#[derive(Deserialize, Serialize)]
32pub(crate) enum Version {
33 #[serde(rename = "1")]
34 V1,
35}
36
37#[derive(Debug, PartialEq, Eq)]
39enum ArgsError {
40 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
64fn colorize_task_name(val: &str) -> ColoredString {
66 val.bright_cyan()
67}
68
69fn colorize_mom_file_path(val: &str) -> ColoredString {
71 val.bright_blue()
72}
73
74struct Mom {
75 mom_files: MomFilesContainer,
76}
77
78impl Mom {
79 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 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 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 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 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
192impl TaskSubcommand {
194 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
208pub 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(¤t_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}