cli/
cli_parser.rs

1//! # cli_parser
2//!
3//! Defines the cli args and parsers them.
4//!
5
6#[cfg(test)]
7#[path = "cli_parser_test.rs"]
8mod cli_parser_test;
9
10use crate::completion::generate_completions;
11
12use crate::cli::{
13    AUTHOR, DEFAULT_LOG_LEVEL, DEFAULT_OUTPUT_FORMAT, DEFAULT_TASK_NAME, DESCRIPTION, VERSION,
14};
15use crate::profile;
16use crate::types::{CliArgs, GlobalConfig};
17use cliparser::types::{
18    Argument, ArgumentHelp, ArgumentOccurrence, ArgumentValueType, CliParsed, CliSpec,
19    CliSpecMetaInfo, PositionalArgument,
20};
21
22use crate::error::CargoMakeError;
23
24fn get_args(
25    cli_parsed: &CliParsed,
26    global_config: &GlobalConfig,
27    command_name: &str,
28    sub_command: bool,
29) -> CliArgs {
30    let mut cli_args = CliArgs::new();
31
32    cli_args.command = if sub_command {
33        let mut binary = "cargo ".to_string();
34        binary.push_str(command_name);
35        binary
36    } else {
37        command_name.to_string()
38    };
39
40    cli_args.env = to_owned_vec(cli_parsed.argument_values.get("env"));
41
42    cli_args.build_file = match cli_parsed.get_first_value("makefile") {
43        Some(value) => Some(value),
44        None => None,
45    };
46
47    cli_args.completion = match cli_parsed.get_first_value("completion") {
48        Some(value) => Some(value.to_string()),
49        None => None,
50    };
51
52    cli_args.cwd = cli_parsed.get_first_value("cwd");
53
54    let default_log_level = match global_config.log_level {
55        Some(ref value) => value.to_string(),
56        None => DEFAULT_LOG_LEVEL.to_string(),
57    };
58    cli_args.log_level = if cli_parsed.arguments.contains("verbose") {
59        "verbose".to_string()
60    } else if cli_parsed.arguments.contains("quiet") {
61        "error".to_string()
62    } else if cli_parsed.arguments.contains("silent") {
63        "off".to_string()
64    } else {
65        cli_parsed
66            .get_first_value("loglevel")
67            .unwrap_or(default_log_level)
68            .to_string()
69    };
70
71    let default_disable_color = match global_config.disable_color {
72        Some(value) => value,
73        None => false,
74    };
75    cli_args.disable_color = cli_parsed.arguments.contains("no-color")
76        || envmnt::is("CARGO_MAKE_DISABLE_COLOR")
77        || default_disable_color;
78
79    cli_args.print_time_summary = cli_parsed.arguments.contains("time-summary")
80        || envmnt::is("CARGO_MAKE_PRINT_TIME_SUMMARY");
81
82    cli_args.env_file = match cli_parsed.get_first_value("envfile") {
83        Some(value) => Some(value.to_string()),
84        None => None,
85    };
86
87    cli_args.output_format = cli_parsed
88        .get_first_value("output-format")
89        .unwrap_or(DEFAULT_OUTPUT_FORMAT.to_string())
90        .to_string();
91
92    cli_args.list_category_steps = match cli_parsed.get_first_value("list-category-steps") {
93        Some(value) => Some(value.to_string()),
94        None => None,
95    };
96
97    cli_args.output_file = match cli_parsed.get_first_value("output-file") {
98        Some(value) => Some(value.to_string()),
99        None => None,
100    };
101
102    let profile_name = cli_parsed
103        .get_first_value("profile")
104        .unwrap_or_else(profile::default_profile);
105    cli_args.profile = Some(profile_name.to_string());
106
107    cli_args.disable_check_for_updates = cli_parsed.arguments.contains("disable-check-for-updates");
108    cli_args.experimental = cli_parsed.arguments.contains("experimental");
109    cli_args.print_only = cli_parsed.arguments.contains("print-steps");
110    cli_args.disable_workspace = cli_parsed.arguments.contains("no-workspace");
111    cli_args.disable_on_error = cli_parsed.arguments.contains("no-on-error");
112    cli_args.allow_private = cli_parsed.arguments.contains("allow-private");
113    cli_args.skip_init_end_tasks = cli_parsed.arguments.contains("skip-init-end-tasks");
114    cli_args.list_all_steps = cli_parsed.arguments.contains("list-steps");
115    cli_args.diff_execution_plan = cli_parsed.arguments.contains("diff-steps");
116    cli_args.hide_uninteresting = cli_parsed.arguments.contains("hide-uninteresting");
117
118    cli_args.skip_tasks_pattern = match cli_parsed.get_first_value("skip-tasks-pattern") {
119        Some(value) => Some(value.to_string()),
120        None => None,
121    };
122
123    let default_task_name = match global_config.default_task_name {
124        Some(ref value) => value.to_string(),
125        None => DEFAULT_TASK_NAME.to_string(),
126    };
127    let task = cli_parsed
128        .get_first_value("task")
129        .unwrap_or(default_task_name);
130    let task_cmd = to_owned_vec(cli_parsed.argument_values.get("TASK_CMD")).unwrap_or(vec![]);
131    let task_cmd_slice = task_cmd.as_slice();
132    let (task, arguments) = match task_cmd_slice {
133        &[] => (task, None),
134        &[ref task_name, ref task_args @ ..] => {
135            let args_strings = task_args.iter().map(|item| item.to_string()).collect();
136            (task_name.to_string(), Some(args_strings))
137        }
138    };
139    cli_args.task = task;
140    cli_args.arguments = arguments;
141
142    cli_args
143}
144
145pub fn create_cli(global_config: &GlobalConfig, mut spec: CliSpec, default_meta: bool) -> CliSpec {
146    let default_task_name = match global_config.default_task_name {
147        Some(ref value) => value.as_str(),
148        None => &DEFAULT_TASK_NAME,
149    };
150    let default_log_level = match global_config.log_level {
151        Some(ref value) => value.as_str(),
152        None => &DEFAULT_LOG_LEVEL,
153    };
154
155    if default_meta {
156        spec = spec
157            .set_meta_info(Some(CliSpecMetaInfo {
158                author: Some(AUTHOR.to_string()),
159                version: Some(VERSION.to_string()),
160                description: Some(DESCRIPTION.to_string()),
161                project: Some("cargo-make".to_string()),
162                help_post_text: Some(
163                    "See more info at: https://github.com/sagiegurari/cargo-make".to_string(),
164                ),
165            }))
166            .add_command("makers")
167            .add_subcommand(vec!["cargo", "make"])
168            .add_subcommand(vec!["cargo-make", "make"]); // done by cargo
169    }
170    add_arguments(spec, default_task_name, default_log_level)
171}
172
173fn add_arguments(spec: CliSpec, default_task_name: &str, default_log_level: &str) -> CliSpec {
174    spec
175        .add_argument(Argument {
176            name: "help".to_string(),
177            key: vec!["--help".to_string(), "-h".to_string()],
178            argument_occurrence: ArgumentOccurrence::Single,
179            value_type: ArgumentValueType::None,
180            default_value: None,
181            help: Some(ArgumentHelp::Text("Print help information".to_string())),
182        })
183        .add_argument(Argument {
184            name: "version".to_string(),
185            key: vec!["--version".to_string(), "-V".to_string()],
186            argument_occurrence: ArgumentOccurrence::Single,
187            value_type: ArgumentValueType::None,
188            default_value: None,
189            help: Some(ArgumentHelp::Text("Print version information".to_string())),
190        })
191        .add_argument(Argument {
192            name: "makefile".to_string(),
193            key: vec!["--makefile".to_string()],
194            argument_occurrence: ArgumentOccurrence::Single,
195            value_type: ArgumentValueType::Single,
196            default_value: None,
197            help: Some(ArgumentHelp::TextAndParam(
198                "The optional toml file containing the tasks definitions".to_string(),
199                "FILE".to_string(),
200            )),
201        })
202        .add_argument(Argument {
203            name: "task".to_string(),
204            key: vec!["--task".to_string(), "-t".to_string()],
205            argument_occurrence: ArgumentOccurrence::Single,
206            value_type: ArgumentValueType::Single,
207            default_value: Some(default_task_name.to_string()),
208            help: Some(ArgumentHelp::TextAndParam(
209                "The task name to execute (can omit the flag if the task name is the last argument)".to_string(),
210                "TASK".to_string(),
211            )),
212        })
213        .add_argument(Argument {
214            name: "profile".to_string(),
215            key: vec!["--profile".to_string(), "-p".to_string()],
216            argument_occurrence: ArgumentOccurrence::Single,
217            value_type: ArgumentValueType::Single,
218            default_value: Some(profile::default_profile()),
219            help: Some(ArgumentHelp::TextAndParam(
220                "The profile name (will be converted to lower case)".to_string(),
221                "PROFILE".to_string(),
222            )),
223        })
224        .add_argument(Argument {
225            name: "completion".to_string(),
226            key: vec!["--completion".to_string()],
227            argument_occurrence: ArgumentOccurrence::Single,
228            value_type: ArgumentValueType::Single,
229            default_value: None,
230            help: Some(ArgumentHelp::TextAndParam(
231                "Will enable completion for the defined tasks for a given shell".to_string(),
232                "COMPLETION".to_string(),
233            )),
234        })
235        .add_argument(Argument {
236            name: "cwd".to_string(),
237            key: vec!["--cwd".to_string()],
238            argument_occurrence: ArgumentOccurrence::Single,
239            value_type: ArgumentValueType::Single,
240            default_value: None,
241            help: Some(ArgumentHelp::TextAndParam(
242                "Will set the current working directory. The search for the makefile will be from this directory if defined.".to_string(),
243                "DIRECTORY".to_string(),
244            )),
245        })
246        .add_argument(Argument {
247            name: "no-workspace".to_string(),
248            key: vec!["--no-workspace".to_string()],
249            argument_occurrence: ArgumentOccurrence::Single,
250            value_type: ArgumentValueType::None,
251            default_value: None,
252            help: Some(ArgumentHelp::Text(
253                "Disable workspace support (tasks are triggered on workspace and not on members)".to_string(),
254            )),
255        })
256        .add_argument(Argument {
257            name: "no-on-error".to_string(),
258            key: vec!["--no-on-error".to_string()],
259            argument_occurrence: ArgumentOccurrence::Single,
260            value_type: ArgumentValueType::None,
261            default_value: None,
262            help: Some(ArgumentHelp::Text(
263                "Disable on error flow even if defined in config sections".to_string(),
264            )),
265        })
266        .add_argument(Argument {
267            name: "allow-private".to_string(),
268            key: vec!["--allow-private".to_string()],
269            argument_occurrence: ArgumentOccurrence::Single,
270            value_type: ArgumentValueType::None,
271            default_value: None,
272            help: Some(ArgumentHelp::Text(
273                "Allow invocation of private tasks".to_string(),
274            )),
275        })
276        .add_argument(Argument {
277            name: "skip-init-end-tasks".to_string(),
278            key: vec!["--skip-init-end-tasks".to_string()],
279            argument_occurrence: ArgumentOccurrence::Single,
280            value_type: ArgumentValueType::None,
281            default_value: None,
282            help: Some(ArgumentHelp::Text(
283                "If set, init and end tasks are skipped".to_string(),
284            )),
285        })
286        .add_argument(Argument {
287            name: "skip-tasks-pattern".to_string(),
288            key: vec!["--skip-tasks".to_string()],
289            argument_occurrence: ArgumentOccurrence::Single,
290            value_type: ArgumentValueType::Single,
291            default_value: None,
292            help: Some(ArgumentHelp::TextAndParam(
293                "Skip all tasks that match the provided regex (example: pre.*|post.*)".to_string(),
294                "SKIP_TASK_PATTERNS".to_string(),
295            )),
296        })
297        .add_argument(Argument {
298            name: "envfile".to_string(),
299            key: vec!["--env-file".to_string()],
300            argument_occurrence: ArgumentOccurrence::Single,
301            value_type: ArgumentValueType::Single,
302            default_value: None,
303            help: Some(ArgumentHelp::TextAndParam(
304                "Set environment variables from provided file".to_string(),
305                "FILE".to_string(),
306            )),
307        })
308        .add_argument(Argument {
309            name: "env".to_string(),
310            key: vec!["--env".to_string(), "-e".to_string()],
311            argument_occurrence: ArgumentOccurrence::Multiple,
312            value_type: ArgumentValueType::Single,
313            default_value: None,
314            help: Some(ArgumentHelp::TextAndParam(
315                "Set environment variables".to_string(),
316                "ENV".to_string(),
317            )),
318        })
319        .add_argument(Argument {
320            name: "loglevel".to_string(),
321            key: vec!["--loglevel".to_string(), "-l".to_string()],
322            argument_occurrence: ArgumentOccurrence::Single,
323            value_type: ArgumentValueType::Single,
324            default_value: Some(default_log_level.to_string()),
325            help: Some(ArgumentHelp::TextAndParam(
326                "The log level (verbose, info, error, off)".to_string(),
327                "LOG LEVEL".to_string(),
328            )),
329        })
330        .add_argument(Argument {
331            name: "verbose".to_string(),
332            key: vec!["--verbose".to_string(), "-v".to_string()],
333            argument_occurrence: ArgumentOccurrence::Single,
334            value_type: ArgumentValueType::None,
335            default_value: None,
336            help: Some(ArgumentHelp::Text(
337                "Sets the log level to verbose (shorthand for --loglevel verbose)".to_string(),
338            )),
339        })
340        .add_argument(Argument {
341            name: "quiet".to_string(),
342            key: vec!["--quiet".to_string()],
343            argument_occurrence: ArgumentOccurrence::Single,
344            value_type: ArgumentValueType::None,
345            default_value: None,
346            help: Some(ArgumentHelp::Text(
347                "Sets the log level to error (shorthand for --loglevel error)".to_string(),
348            )),
349        })
350        .add_argument(Argument {
351            name: "silent".to_string(),
352            key: vec!["--silent".to_string()],
353            argument_occurrence: ArgumentOccurrence::Single,
354            value_type: ArgumentValueType::None,
355            default_value: None,
356            help: Some(ArgumentHelp::Text(
357                "Sets the log level to off (shorthand for --loglevel off)".to_string(),
358            )),
359        })
360        .add_argument(Argument {
361            name: "no-color".to_string(),
362            key: vec!["--no-color".to_string()],
363            argument_occurrence: ArgumentOccurrence::Single,
364            value_type: ArgumentValueType::None,
365            default_value: None,
366            help: Some(ArgumentHelp::Text(
367                "Disables colorful output".to_string(),
368            )),
369        })
370        .add_argument(Argument {
371            name: "time-summary".to_string(),
372            key: vec!["--time-summary".to_string()],
373            argument_occurrence: ArgumentOccurrence::Single,
374            value_type: ArgumentValueType::None,
375            default_value: None,
376            help: Some(ArgumentHelp::Text(
377                "Print task level time summary at end of flow".to_string(),
378            )),
379        })
380        .add_argument(Argument {
381            name: "experimental".to_string(),
382            key: vec!["--experimental".to_string()],
383            argument_occurrence: ArgumentOccurrence::Single,
384            value_type: ArgumentValueType::None,
385            default_value: None,
386            help: Some(ArgumentHelp::Text(
387                "Allows access unsupported experimental predefined tasks.".to_string(),
388            )),
389        })
390        .add_argument(Argument {
391            name: "disable-check-for-updates".to_string(),
392            key: vec!["--disable-check-for-updates".to_string()],
393            argument_occurrence: ArgumentOccurrence::Single,
394            value_type: ArgumentValueType::None,
395            default_value: None,
396            help: Some(ArgumentHelp::Text(
397                "Disables the update check during startup".to_string(),
398            )),
399        })
400        .add_argument(Argument {
401            name: "output-format".to_string(),
402            key: vec!["--output-format".to_string()],
403            argument_occurrence: ArgumentOccurrence::Single,
404            value_type: ArgumentValueType::Single,
405            default_value: None,
406            help: Some(ArgumentHelp::TextAndParam(
407                "The print/list steps format (some operations do not support all formats) (default, short-description, markdown, markdown-single-page, markdown-sub-section, autocomplete)".to_string(),
408                "OUTPUT FORMAT".to_string(),
409            )),
410        })
411        .add_argument(Argument {
412            name: "output-file".to_string(),
413            key: vec!["--output-file".to_string()],
414            argument_occurrence: ArgumentOccurrence::Single,
415            value_type: ArgumentValueType::Single,
416            default_value: None,
417            help: Some(ArgumentHelp::TextAndParam(
418                "The list steps output file name".to_string(),
419                "OUTPUT_FILE".to_string(),
420            )),
421        })
422        .add_argument(Argument {
423            name: "hide-uninteresting".to_string(),
424            key: vec!["--hide-uninteresting".to_string()],
425            argument_occurrence: ArgumentOccurrence::Single,
426            value_type: ArgumentValueType::None,
427            default_value: None,
428            help: Some(ArgumentHelp::Text(
429                "Hide any minor tasks such as pre/post hooks.".to_string(),
430            )),
431        })
432        .add_argument(Argument {
433            name: "print-steps".to_string(),
434            key: vec!["--print-steps".to_string()],
435            argument_occurrence: ArgumentOccurrence::Single,
436            value_type: ArgumentValueType::None,
437            default_value: None,
438            help: Some(ArgumentHelp::Text(
439                "Only prints the steps of the build in the order they will be invoked but without invoking them".to_string(),
440            )),
441        })
442        .add_argument(Argument {
443            name: "list-steps".to_string(),
444            key: vec!["--list-all-steps".to_string()],
445            argument_occurrence: ArgumentOccurrence::Single,
446            value_type: ArgumentValueType::None,
447            default_value: None,
448            help: Some(ArgumentHelp::Text(
449                "Lists all known steps".to_string(),
450            )),
451        })
452        .add_argument(Argument {
453            name: "list-category-steps".to_string(),
454            key: vec!["--list-category-steps".to_string()],
455            argument_occurrence: ArgumentOccurrence::Single,
456            value_type: ArgumentValueType::Single,
457            default_value: None,
458            help: Some(ArgumentHelp::TextAndParam(
459                "List steps for a given category".to_string(),
460                "CATEGORY".to_string(),
461            )),
462        })
463        .add_argument(Argument {
464            name: "diff-steps".to_string(),
465            key: vec!["--diff-steps".to_string()],
466            argument_occurrence: ArgumentOccurrence::Single,
467            value_type: ArgumentValueType::None,
468            default_value: None,
469            help: Some(ArgumentHelp::Text(
470                "Runs diff between custom flow and prebuilt flow (requires git)".to_string(),
471            )),
472        })
473        .set_positional_argument(Some(PositionalArgument {
474            name: "TASK_CMD".to_string(),
475            help: Some(ArgumentHelp::Text(
476                "The task to execute, potentially including arguments which can be accessed in the task itself.".to_string(),
477            )),
478        }))
479}
480
481pub fn parse_args(
482    global_config: &GlobalConfig,
483    command_name: &str,
484    sub_command: bool,
485    args: Option<Vec<&str>>,
486    spec: CliSpec,
487) -> Result<CliArgs, CargoMakeError> {
488    let cli_parsed = match args {
489        Some(args_vec) => cliparser::parse(&args_vec, &spec),
490        None => cliparser::parse_process(&spec),
491    }?;
492
493    if cli_parsed.arguments.contains("help") {
494        // generate help text
495        let help_text = cliparser::help(&spec);
496        println!("{}", help_text);
497        Err(CargoMakeError::ExitCode(std::process::ExitCode::SUCCESS))
498    } else if cli_parsed.arguments.contains("version") {
499        // generate version text
500        let version_text = cliparser::version(&spec);
501        println!("{}", version_text);
502        Err(CargoMakeError::ExitCode(std::process::ExitCode::SUCCESS))
503    } else if let Some(shell) = cli_parsed.get_first_value("completion") {
504        // Call the function to generate completions
505        if let Err(e) = generate_completions(&shell) {
506            error!("Error generating completions: {}", e);
507            return Err(CargoMakeError::ExitCode(std::process::ExitCode::FAILURE));
508        }
509        Err(crate::error::CargoMakeError::ExitCode(
510            std::process::ExitCode::SUCCESS,
511        ))
512    } else {
513        Ok(get_args(
514            &cli_parsed,
515            &global_config,
516            command_name,
517            sub_command,
518        ))
519    }
520}
521
522pub fn parse(
523    global_config: &GlobalConfig,
524    command_name: &str,
525    sub_command: bool,
526) -> Result<CliArgs, CargoMakeError> {
527    parse_args(
528        global_config,
529        command_name,
530        sub_command,
531        None,
532        create_cli(&global_config, CliSpec::new(), true),
533    )
534}
535
536fn to_owned_vec(vec_option: Option<&Vec<String>>) -> Option<Vec<String>> {
537    match vec_option {
538        Some(vec) => Some(vec.to_owned()),
539        None => None,
540    }
541}