Command line parser

I'm developing a program that uses a "internal" command line interface.
I need a parser, i already saw "clap" but it is not ok for me.
I need something that can parse something like:
COMMAND subcommand -PARAMETER=value, value2 -PARAMETER2

and fill a struct like:

struct Cmd
{
main_cmd : String,
subcommand : String,
parameters : Vec
}

struct Parameter
{
par : String,
opts : Vec
}

There is something useful for me, or i have to build myself?

Thank you

I built a simple macro for myself, but limited it to just 1) a fixed set of positional arguments, 2) optional arguments. Output is just strings, not converted to any other kind of type. If you need some serious struct, you get into the area of procedural macros, which are order of magnitude more complicated.


#[macro_export]
macro_rules! autocli {
	// usage examples:
	// autocli!(input_path, output_path, "input: *.osm.gz, output: *.csv")
	// autocli!(number, "input argument: a random number from 1 to 5")
	// autocli!(argument1, argument2, [option1, my_option2] "input argument: a random number from 1 to 5")

	($($arg_name:ident),+, $([$($opt_name:ident),+],)? $help_text:literal) => {
		let mut args: Vec<std::ffi::OsString> = if cfg!(test) {
			use std::str::FromStr;
			let v: String = std::env::var("TEZIREK_ARGS").unwrap_or("a,b,c".to_string()).into();
			v.split(",").map(|s| std::ffi::OsString::from_str(s).unwrap()).collect()
		} else {
			std::env::args_os().collect()
		};
		let mut opts: std::collections::HashSet<String> = std::collections::HashSet::new();
		args.reverse();
		args.pop();
		args.retain(|a| {
			let b = a.to_str().unwrap();
			if b.starts_with("--") {
				opts.insert(b[2..].to_owned());
				false
			} else { true }});

		let usage = "".to_owned()
			$( + " " + stringify!($arg_name).to_ascii_uppercase().as_str())+
			$( $( + " [--" + stringify!($opt_name).to_ascii_lowercase().replace("_", "-").as_str() + "]")+ )?;
		let help_text = $help_text;

		$(
			let Some($arg_name) = args.pop() else { // if there aren't enough CLI args, we quit and print the help msg
				println!("{}\nRequired args: {}\n", help_text, usage);
				std::process::exit(64);
			};
			let binding = $arg_name.clone();
			let $arg_name = binding.to_str().unwrap();
		)+

		$(
			$(
				let $opt_name = opts.contains(&stringify!($opt_name).to_ascii_lowercase().replace("_", "-"));
			)+
		)?

		if args.len() > 0 {
			println!("{}\nRequired args: {}\n", help_text, usage);
			std::process::exit(64);
		}
	};
}

usage:

autocli!(osm_file, graph_file, errors_file, "OSM_FILE: *.osm.gz;\nGRAPH_FILE: *.bin;\nERRORS_FILE: *.txt (list of broken relations that will be ignored)");
1 Like

If I remember correctly, clap supports subcommands and multiple parsers. You can look at the start of the line (COMMAND in your case) and then select the correct clap parser to invoke to process the rest of the line.

2 Likes

Welcome to the forum!

Just a nit for future posts: Please enclose your code in triple backquotes and indent it as described in this pinned post: Forum Code Formatting and Syntax Highlighting.

Just so that I know I understand the question:

Is the idea that PARAMETER and PARAMETER2 can be any string, and not just from a predefined list of accepted values?

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.