Skip to content

Commit

Permalink
new: changed to support sub command
Browse files Browse the repository at this point in the history
  • Loading branch information
sttk committed Aug 10, 2024
1 parent 9492709 commit 4b9801a
Show file tree
Hide file tree
Showing 8 changed files with 896 additions and 13 deletions.
17 changes: 16 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,7 @@ pub struct Cmd<'a> {
cfgs: Vec<OptCfg>,

_leaked_strs: Vec<&'a str>,
_num_of_args: usize,
}

impl<'a> Drop for Cmd<'a> {
Expand All @@ -339,7 +340,7 @@ impl fmt::Debug for Cmd<'_> {
}
}

impl<'a> Cmd<'a> {
impl<'b, 'a> Cmd<'a> {
/// Creates a `Cmd` instance with command line arguments obtained from [std::env::args_os].
///
/// Since [std::env::args_os] returns a vector of [OsString] and they can contain invalid
Expand Down Expand Up @@ -413,12 +414,15 @@ impl<'a> Cmd<'a> {
cmd_name_start = 0;
}

let _num_of_args = _leaked_strs.len();

Ok(Cmd {
name: &_leaked_strs[0][cmd_name_start..],
args: Vec::new(),
opts: HashMap::new(),
cfgs: Vec::new(),
_leaked_strs,
_num_of_args,
})
}

Expand Down Expand Up @@ -449,15 +453,26 @@ impl<'a> Cmd<'a> {
cmd_name_start = 0;
};

let _num_of_args = _leaked_strs.len();

Cmd {
name: &_leaked_strs[0][cmd_name_start..],
args: Vec::new(),
opts: HashMap::new(),
cfgs: Vec::new(),
_leaked_strs,
_num_of_args,
}
}

fn sub_cmd(&'a self, from_index: usize) -> Cmd<'b> {
Cmd::with_strings(
self._leaked_strs[from_index..(self._num_of_args)]
.into_iter()
.map(|s| s.to_string()),
)
}

/// Returns the command name.
///
/// This name is base name extracted from the command path string slice, which is the first
Expand Down
6 changes: 3 additions & 3 deletions src/parse/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,13 @@ where

'L0: for (i_arg, arg) in args.iter().enumerate() {
if is_non_opt {
collect_args(arg);
if until_1st_arg {
if let Some(err) = first_err {
return Err(err);
}
return Ok(Some(i_arg));
}
collect_args(arg);
} else if !prev_opt_taking_args.is_empty() {
match collect_opts(prev_opt_taking_args, Some(arg)) {
Err(err) => {
Expand Down Expand Up @@ -107,13 +107,13 @@ where
}
} else if arg.starts_with("-") {
if arg.len() == 1 {
collect_args(arg);
if until_1st_arg {
if let Some(err) = first_err {
return Err(err);
}
return Ok(Some(i_arg));
}
collect_args(arg);
continue 'L0;
}

Expand Down Expand Up @@ -176,13 +176,13 @@ where
}
}
} else {
collect_args(arg);
if until_1st_arg {
if let Some(err) = first_err {
return Err(err);
}
return Ok(Some(i_arg));
}
collect_args(arg);
}
}

Expand Down
245 changes: 242 additions & 3 deletions src/parse/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use super::parse_args;
use crate::errors::InvalidOption;
use crate::Cmd;

impl<'a> Cmd<'a> {
impl<'b, 'a> Cmd<'a> {
/// Parses command line arguments without configurations.
///
/// This method divides command line arguments into options and command arguments based on
Expand All @@ -28,6 +28,7 @@ impl<'a> Cmd<'a> {
/// use cliargs::errors::InvalidOption;
///
/// let mut cmd = Cmd::with_strings(vec![ /* ... */ ]);
///
/// match cmd.parse() {
/// Ok(_) => { /* ... */ },
/// Err(InvalidOption::OptionContainsInvalidChar { option }) => {
Expand All @@ -51,9 +52,9 @@ impl<'a> Cmd<'a> {

let take_opt_args = |_arg: &str| false;

if !self._leaked_strs.is_empty() {
if self._num_of_args > 0 {
match parse_args(
&self._leaked_strs[1..],
&self._leaked_strs[1..(self._num_of_args)],
collect_args,
collect_opts,
take_opt_args,
Expand All @@ -66,6 +67,75 @@ impl<'a> Cmd<'a> {

Ok(())
}

/// Parses command line arguments without configurations but stops parsing when encountering
/// first command argument.
///
/// This method creates and returns a new [Cmd] instance that holds the command line arguments
/// starting from the first command argument.
///
/// This method divides command line arguments into options and command arguments based on
/// simple rules that are almost the same as POSIX & GNU:
/// arguments staring with `-` or `--` are treated as options, and others are treated as command
/// arguments.
/// If an `=` is found within an option, the part before the `=` is treated as the option name,
/// and the part after the `=` is treated as the option argument.
/// Options starting with `--` are long options and option starting with `-` are short options.
/// Multiple short options can be concatenated into a single command line argument.
/// If an argument is exactly `--`, all subsequent arguments are treated as command arguments.
///
/// Since the results of parsing are stored into this `Cmd` instance, this method returns a
/// [Result] which contains an unit value (`()`) if succeeding, or a `errors::InvalidOption`
/// if failing.
///
/// ```rust
/// use cliargs::Cmd;
/// use cliargs::errors::InvalidOption;
///
/// let mut cmd = Cmd::with_strings(vec![ /* ... */ ]);
///
/// match cmd.parse_until_sub_cmd() {
/// Ok(Some(mut sub_cmd)) => {
/// let sub_cmd_name = sub_cmd.name();
/// match sub_cmd.parse() {
/// Ok(_) => { /* ... */ },
/// Err(err) => panic!("Invalid option: {}", err.option()),
/// }
/// },
/// Ok(None) => { /* ... */ },
/// Err(InvalidOption::OptionContainsInvalidChar { option }) => {
/// panic!("Option contains invalid character: {option}");
/// },
/// Err(err) => panic!("Invalid option: {}", err.option()),
/// }
/// ```
pub fn parse_until_sub_cmd(&mut self) -> Result<Option<Cmd<'b>>, InvalidOption> {
let collect_args = |_arg| {};

let collect_opts = |name, option| {
let vec = self.opts.entry(name).or_insert_with(|| Vec::new());
if let Some(arg) = option {
vec.push(arg);
}
Ok(())
};

let take_opt_args = |_arg: &str| false;

if self._num_of_args > 0 {
if let Some(idx) = parse_args(
&self._leaked_strs[1..(self._num_of_args)],
collect_args,
collect_opts,
take_opt_args,
true,
)? {
return Ok(Some(self.sub_cmd(idx + 1))); // +1, because parse_args parses from 1.
}
}

Ok(None)
}
}

#[cfg(test)]
Expand Down Expand Up @@ -572,3 +642,172 @@ mod tests_of_cmd {
}
}
}

#[cfg(test)]
mod tests_of_parse_until_sub_cmd {
use super::*;

#[test]
fn test_if_command_line_arguments_contains_no_command_argument_and_option() {
let ui_args = vec!["/path/to/app".to_string()];
let mut cmd = Cmd::with_strings(ui_args);

match cmd.parse_until_sub_cmd() {
Ok(None) => {}
Ok(Some(_)) => assert!(false),
Err(_) => assert!(false),
}

assert_eq!(cmd.name(), "app");
assert_eq!(cmd.args(), &[] as &[&str]);
}

#[test]
fn test_if_command_line_arguments_contains_only_command_arguments() {
let ui_args = vec![
"/path/to/app".to_string(),
"foo".to_string(),
"bar".to_string(),
];
let mut cmd = Cmd::with_strings(ui_args);

match cmd.parse_until_sub_cmd() {
Ok(Some(mut sub_cmd)) => {
assert_eq!(sub_cmd.name(), "foo");
assert_eq!(sub_cmd.args(), &[] as &[&str]);

match sub_cmd.parse() {
Ok(_) => {}
Err(_) => assert!(false),
}

assert_eq!(sub_cmd.name(), "foo");
assert_eq!(sub_cmd.args(), &["bar"]);
}
Ok(None) => assert!(false),
Err(_) => assert!(false),
}

assert_eq!(cmd.name(), "app");
assert_eq!(cmd.args(), &[] as &[&str]);

//

let f = || {
let ui_args = vec![
"/path/to/app".to_string(),
"foo".to_string(),
"bar".to_string(),
];
let mut cmd = Cmd::with_strings(ui_args);

if let Some(mut sub_cmd) = cmd.parse_until_sub_cmd()? {
assert_eq!(sub_cmd.name(), "foo");
assert_eq!(sub_cmd.args(), &[] as &[&str]);

match sub_cmd.parse() {
Ok(_) => {}
Err(_) => assert!(false),
}

assert_eq!(sub_cmd.name(), "foo");
assert_eq!(sub_cmd.args(), &["bar"]);

assert_eq!(cmd.name(), "app");
assert_eq!(cmd.args(), &[] as &[&str]);
} else {
assert_eq!(cmd.name(), "app");
assert_eq!(cmd.args(), &[] as &[&str]);
}

Ok::<(), InvalidOption>(())
};
let _ = f();
}

#[test]
fn test_if_command_line_arguments_contains_only_command_options() {
let ui_args = vec![
"/path/to/app".to_string(),
"--foo".to_string(),
"-b".to_string(),
];
let mut cmd = Cmd::with_strings(ui_args);

match cmd.parse_until_sub_cmd() {
Ok(None) => {}
Ok(Some(_)) => assert!(false),
Err(_) => assert!(false),
}

assert_eq!(cmd.name(), "app");
assert_eq!(cmd.args(), &[] as &[&str]);
assert_eq!(cmd.has_opt("foo"), true);
assert_eq!(cmd.has_opt("b"), true);
assert_eq!(cmd.opt_arg("foo"), None);
assert_eq!(cmd.opt_arg("b"), None);
}

#[test]
fn test_if_command_line_arguments_contains_both_command_arguments_and_options() {
let ui_args = vec![
"/path/to/app".to_string(),
"--foo=123".to_string(),
"bar".to_string(),
"--baz".to_string(),
"-q=ABC".to_string(),
"quux".to_string(),
];
let mut cmd = Cmd::with_strings(ui_args);

if let Some(mut sub_cmd) = cmd.parse_until_sub_cmd().unwrap() {
assert_eq!(sub_cmd.name(), "bar");
assert_eq!(sub_cmd.args(), &[] as &[&str]);
assert_eq!(cmd.has_opt("baz"), false);
assert_eq!(cmd.opt_arg("baz"), None);
assert_eq!(cmd.has_opt("q"), false);
assert_eq!(cmd.opt_arg("q"), None);

match sub_cmd.parse() {
Ok(_) => {}
Err(_) => assert!(false),
}

assert_eq!(sub_cmd.name(), "bar");
assert_eq!(sub_cmd.args(), &["quux"]);
assert_eq!(sub_cmd.has_opt("baz"), true);
assert_eq!(sub_cmd.opt_arg("baz"), None);
assert_eq!(sub_cmd.has_opt("q"), true);
assert_eq!(sub_cmd.opt_arg("q"), Some("ABC"));
}

assert_eq!(cmd.name(), "app");
assert_eq!(cmd.args(), &[] as &[&str]);
assert_eq!(cmd.has_opt("foo"), true);
assert_eq!(cmd.opt_arg("foo"), Some("123"));
}

#[test]
fn test_if_fail_to_parse() {
let ui_args = vec![
"/path/to/app".to_string(),
"--f#o".to_string(),
"bar".to_string(),
];
let mut cmd = Cmd::with_strings(ui_args);

match cmd.parse_until_sub_cmd() {
Ok(None) => assert!(false),
Ok(Some(_)) => assert!(false),
Err(InvalidOption::OptionContainsInvalidChar { option }) => {
assert_eq!(option, "f#o");
}
Err(_) => assert!(false),
}

assert_eq!(cmd.name(), "app");
assert_eq!(cmd.args(), &[] as &[&str]);
assert_eq!(cmd.has_opt("f#o"), false);
assert_eq!(cmd.opt_arg("f#o"), None);
}
}
Loading

0 comments on commit 4b9801a

Please sign in to comment.