Switch to a command-line API closer to TaskWarrior
* Use a parser (rather than clap) to process the command line * Outline some generic support for filtering, reporting, modifying, etc. * Break argument parsing strictly from invocation, to allow independent testing
This commit is contained in:
232
cli/src/argparse/args.rs
Normal file
232
cli/src/argparse/args.rs
Normal file
@@ -0,0 +1,232 @@
|
||||
//! Parsers for argument lists -- arrays of strings
|
||||
use super::ArgList;
|
||||
use nom::bytes::complete::tag as nomtag;
|
||||
use nom::{
|
||||
branch::*,
|
||||
character::complete::*,
|
||||
combinator::*,
|
||||
error::{Error, ErrorKind},
|
||||
multi::*,
|
||||
sequence::*,
|
||||
Err, IResult,
|
||||
};
|
||||
|
||||
/// Recognizes any argument
|
||||
pub(super) fn any(input: &str) -> IResult<&str, &str> {
|
||||
rest(input)
|
||||
}
|
||||
|
||||
/// Recognizes a literal string
|
||||
pub(super) fn literal(literal: &'static str) -> impl Fn(&str) -> IResult<&str, &str> {
|
||||
move |input: &str| all_consuming(nomtag(literal))(input)
|
||||
}
|
||||
|
||||
/// Recognizes a comma-separated list of ID's (integers or UUID prefixes)
|
||||
pub(super) fn id_list(input: &str) -> IResult<&str, Vec<&str>> {
|
||||
fn hex_n(n: usize) -> impl Fn(&str) -> IResult<&str, &str> {
|
||||
move |input: &str| recognize(many_m_n(n, n, one_of(&b"0123456789abcdefABCDEF"[..])))(input)
|
||||
}
|
||||
all_consuming(separated_list1(
|
||||
char(','),
|
||||
alt((
|
||||
recognize(tuple((
|
||||
hex_n(8),
|
||||
char('-'),
|
||||
hex_n(4),
|
||||
char('-'),
|
||||
hex_n(4),
|
||||
char('-'),
|
||||
hex_n(4),
|
||||
char('-'),
|
||||
hex_n(12),
|
||||
))),
|
||||
recognize(tuple((
|
||||
hex_n(8),
|
||||
char('-'),
|
||||
hex_n(4),
|
||||
char('-'),
|
||||
hex_n(4),
|
||||
char('-'),
|
||||
hex_n(4),
|
||||
))),
|
||||
recognize(tuple((hex_n(8), char('-'), hex_n(4), char('-'), hex_n(4)))),
|
||||
recognize(tuple((hex_n(8), char('-'), hex_n(4)))),
|
||||
hex_n(8),
|
||||
digit1,
|
||||
)),
|
||||
))(input)
|
||||
}
|
||||
|
||||
/// Recognizes a tag prefixed with `+` and returns the tag value
|
||||
#[allow(dead_code)] // tags not implemented yet
|
||||
pub(super) fn plus_tag(input: &str) -> IResult<&str, &str> {
|
||||
fn to_tag(input: (char, &str)) -> Result<&str, ()> {
|
||||
Ok(input.1)
|
||||
}
|
||||
map_res(
|
||||
all_consuming(tuple((char('+'), recognize(pair(alpha1, alphanumeric0))))),
|
||||
to_tag,
|
||||
)(input)
|
||||
}
|
||||
|
||||
/// Recognizes a tag prefixed with `-` and returns the tag value
|
||||
#[allow(dead_code)] // tags not implemented yet
|
||||
pub(super) fn minus_tag(input: &str) -> IResult<&str, &str> {
|
||||
fn to_tag(input: (char, &str)) -> Result<&str, ()> {
|
||||
Ok(input.1)
|
||||
}
|
||||
map_res(
|
||||
all_consuming(tuple((char('-'), recognize(pair(alpha1, alphanumeric0))))),
|
||||
to_tag,
|
||||
)(input)
|
||||
}
|
||||
|
||||
/// Recognizes a tag prefixed with either `-` or `+`, returning true for + and false for -
|
||||
#[allow(dead_code)] // tags not implemented yet
|
||||
pub(super) fn tag(input: &str) -> IResult<&str, (bool, &str)> {
|
||||
fn to_plus(input: &str) -> Result<(bool, &str), ()> {
|
||||
Ok((true, input))
|
||||
}
|
||||
fn to_minus(input: &str) -> Result<(bool, &str), ()> {
|
||||
Ok((false, input))
|
||||
}
|
||||
alt((map_res(plus_tag, to_plus), map_res(minus_tag, to_minus)))(input)
|
||||
}
|
||||
|
||||
/// Consume a single argument from an argument list that matches the given string parser (one
|
||||
/// of the other functions in this module). The given parser must consume the entire input.
|
||||
pub(super) fn arg_matching<'a, O, F>(f: F) -> impl Fn(ArgList<'a>) -> IResult<ArgList, O>
|
||||
where
|
||||
F: Fn(&'a str) -> IResult<&'a str, O>,
|
||||
{
|
||||
move |input: ArgList<'a>| {
|
||||
if let Some(arg) = input.get(0) {
|
||||
return match f(arg) {
|
||||
Ok(("", rv)) => Ok((&input[1..], rv)),
|
||||
// single-arg parsers must consume the entire arg
|
||||
Ok((unconsumed, _)) => panic!("unconsumed argument input {}", unconsumed),
|
||||
// single-arg parsers are all complete parsers
|
||||
Err(Err::Incomplete(_)) => unreachable!(),
|
||||
// for error and failure, rewrite to an error at this position in the arugment list
|
||||
Err(Err::Error(Error { input: _, code })) => Err(Err::Error(Error { input, code })),
|
||||
Err(Err::Failure(Error { input: _, code })) => {
|
||||
Err(Err::Failure(Error { input, code }))
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Err(Err::Error(Error {
|
||||
input,
|
||||
// since we're using nom's built-in Error, our choices here are limited, but tihs
|
||||
// occurs when there's no argument where one is expected, so Eof seems appropriate
|
||||
code: ErrorKind::Eof,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_arg_matching() {
|
||||
assert_eq!(
|
||||
arg_matching(tag)(argv!["+foo", "bar"]).unwrap(),
|
||||
(argv!["bar"], (true, "foo"))
|
||||
);
|
||||
assert_eq!(
|
||||
arg_matching(tag)(argv!["-foo", "bar"]).unwrap(),
|
||||
(argv!["bar"], (false, "foo"))
|
||||
);
|
||||
assert!(arg_matching(tag)(argv!["foo", "bar"]).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_plus_tag() {
|
||||
assert_eq!(plus_tag("+abc").unwrap().1, "abc");
|
||||
assert_eq!(plus_tag("+abc123").unwrap().1, "abc123");
|
||||
assert!(plus_tag("-abc123").is_err());
|
||||
assert!(plus_tag("+abc123 ").is_err());
|
||||
assert!(plus_tag(" +abc123").is_err());
|
||||
assert!(plus_tag("+1abc").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_minus_tag() {
|
||||
assert_eq!(minus_tag("-abc").unwrap().1, "abc");
|
||||
assert_eq!(minus_tag("-abc123").unwrap().1, "abc123");
|
||||
assert!(minus_tag("+abc123").is_err());
|
||||
assert!(minus_tag("-abc123 ").is_err());
|
||||
assert!(minus_tag(" -abc123").is_err());
|
||||
assert!(minus_tag("-1abc").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tag() {
|
||||
assert_eq!(tag("-abc").unwrap().1, (false, "abc"));
|
||||
assert_eq!(tag("+abc123").unwrap().1, (true, "abc123"));
|
||||
assert!(tag("+abc123 --").is_err());
|
||||
assert!(tag("-abc123 ").is_err());
|
||||
assert!(tag(" -abc123").is_err());
|
||||
assert!(tag("-1abc").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_literal() {
|
||||
assert_eq!(literal("list")("list").unwrap().1, "list");
|
||||
assert!(literal("list")("listicle").is_err());
|
||||
assert!(literal("list")(" list ").is_err());
|
||||
assert!(literal("list")("LiSt").is_err());
|
||||
assert!(literal("list")("denylist").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_id_list_single() {
|
||||
assert_eq!(id_list("123").unwrap().1, vec!["123".to_owned()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_id_list_uuids() {
|
||||
assert_eq!(id_list("12341234").unwrap().1, vec!["12341234".to_owned()]);
|
||||
assert_eq!(id_list("1234abcd").unwrap().1, vec!["1234abcd".to_owned()]);
|
||||
assert_eq!(id_list("abcd1234").unwrap().1, vec!["abcd1234".to_owned()]);
|
||||
assert_eq!(
|
||||
id_list("abcd1234-1234").unwrap().1,
|
||||
vec!["abcd1234-1234".to_owned()]
|
||||
);
|
||||
assert_eq!(
|
||||
id_list("abcd1234-1234-2345").unwrap().1,
|
||||
vec!["abcd1234-1234-2345".to_owned()]
|
||||
);
|
||||
assert_eq!(
|
||||
id_list("abcd1234-1234-2345-3456").unwrap().1,
|
||||
vec!["abcd1234-1234-2345-3456".to_owned()]
|
||||
);
|
||||
assert_eq!(
|
||||
id_list("abcd1234-1234-2345-3456-0123456789ab").unwrap().1,
|
||||
vec!["abcd1234-1234-2345-3456-0123456789ab".to_owned()]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_id_list_invalid_partial_uuids() {
|
||||
assert!(id_list("abcd123").is_err());
|
||||
assert!(id_list("abcd12345").is_err());
|
||||
assert!(id_list("abcd1234-").is_err());
|
||||
assert!(id_list("abcd1234-123").is_err());
|
||||
assert!(id_list("abcd1234-1234-").is_err());
|
||||
assert!(id_list("abcd1234-12345-").is_err());
|
||||
assert!(id_list("abcd1234-1234-2345-3456-0123456789ab-").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_id_list_uuids_mixed() {
|
||||
assert_eq!(id_list("abcd1234,abcd1234-1234,abcd1234-1234-2345,abcd1234-1234-2345-3456,abcd1234-1234-2345-3456-0123456789ab").unwrap().1,
|
||||
vec!["abcd1234".to_owned(),
|
||||
"abcd1234-1234".to_owned(),
|
||||
"abcd1234-1234-2345".to_owned(),
|
||||
"abcd1234-1234-2345-3456".to_owned(),
|
||||
"abcd1234-1234-2345-3456-0123456789ab".to_owned(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
62
cli/src/argparse/command.rs
Normal file
62
cli/src/argparse/command.rs
Normal file
@@ -0,0 +1,62 @@
|
||||
use super::args::*;
|
||||
use super::{ArgList, Subcommand};
|
||||
use failure::{format_err, Fallible};
|
||||
use nom::{combinator::*, sequence::*, Err, IResult};
|
||||
|
||||
/// A command is the overall command that the CLI should execute.
|
||||
///
|
||||
/// It consists of some information common to all commands and a `Subcommand` identifying the
|
||||
/// particular kind of behavior desired.
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub(crate) struct Command {
|
||||
pub(crate) command_name: String,
|
||||
pub(crate) subcommand: Subcommand,
|
||||
}
|
||||
|
||||
impl Command {
|
||||
pub(super) fn parse(input: ArgList) -> IResult<ArgList, Command> {
|
||||
fn to_command(input: (&str, Subcommand)) -> Result<Command, ()> {
|
||||
let command = Command {
|
||||
command_name: input.0.to_owned(),
|
||||
subcommand: input.1,
|
||||
};
|
||||
Ok(command)
|
||||
}
|
||||
map_res(
|
||||
all_consuming(tuple((arg_matching(any), Subcommand::parse))),
|
||||
to_command,
|
||||
)(input)
|
||||
}
|
||||
|
||||
/// Parse a command from the given list of strings.
|
||||
pub fn from_argv(argv: &[&str]) -> Fallible<Command> {
|
||||
match Command::parse(argv) {
|
||||
Ok((&[], cmd)) => Ok(cmd),
|
||||
Ok((trailing, _)) => Err(format_err!(
|
||||
"command line has trailing arguments: {:?}",
|
||||
trailing
|
||||
)),
|
||||
Err(Err::Incomplete(_)) => unreachable!(),
|
||||
Err(Err::Error(e)) => Err(format_err!("command line not recognized: {:?}", e)),
|
||||
Err(Err::Failure(e)) => Err(format_err!("command line not recognized: {:?}", e)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
// NOTE: most testing of specific subcommands is handled in `subcommand.rs`.
|
||||
|
||||
#[test]
|
||||
fn test_version() {
|
||||
assert_eq!(
|
||||
Command::from_argv(argv!["task", "version"]).unwrap(),
|
||||
Command {
|
||||
subcommand: Subcommand::Version,
|
||||
command_name: "task".to_owned(),
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
105
cli/src/argparse/filter.rs
Normal file
105
cli/src/argparse/filter.rs
Normal file
@@ -0,0 +1,105 @@
|
||||
use super::args::{arg_matching, id_list};
|
||||
use super::ArgList;
|
||||
use nom::{combinator::*, multi::fold_many0, IResult};
|
||||
|
||||
/// A filter represents a selection of a particular set of tasks.
|
||||
#[derive(Debug, PartialEq, Default, Clone)]
|
||||
pub(crate) struct Filter {
|
||||
/// A list of numeric IDs or prefixes of UUIDs
|
||||
pub(crate) id_list: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
enum FilterArg {
|
||||
IdList(Vec<String>),
|
||||
}
|
||||
|
||||
impl Filter {
|
||||
pub(super) fn parse(input: ArgList) -> IResult<ArgList, Filter> {
|
||||
fn fold(mut acc: Filter, mod_arg: FilterArg) -> Filter {
|
||||
match mod_arg {
|
||||
FilterArg::IdList(mut id_list) => {
|
||||
if let Some(ref mut existing) = acc.id_list {
|
||||
// given multiple ID lists, concatenate them to represent
|
||||
// an "OR" between them.
|
||||
existing.append(&mut id_list);
|
||||
} else {
|
||||
acc.id_list = Some(id_list);
|
||||
}
|
||||
}
|
||||
}
|
||||
acc
|
||||
}
|
||||
fold_many0(
|
||||
Self::id_list,
|
||||
Filter {
|
||||
..Default::default()
|
||||
},
|
||||
fold,
|
||||
)(input)
|
||||
}
|
||||
|
||||
fn id_list(input: ArgList) -> IResult<ArgList, FilterArg> {
|
||||
fn to_filterarg(mut input: Vec<&str>) -> Result<FilterArg, ()> {
|
||||
Ok(FilterArg::IdList(
|
||||
input.drain(..).map(str::to_owned).collect(),
|
||||
))
|
||||
}
|
||||
map_res(arg_matching(id_list), to_filterarg)(input)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_empty() {
|
||||
let (input, filter) = Filter::parse(argv![]).unwrap();
|
||||
assert_eq!(input.len(), 0);
|
||||
assert_eq!(
|
||||
filter,
|
||||
Filter {
|
||||
..Default::default()
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_id_list_single() {
|
||||
let (input, filter) = Filter::parse(argv!["1"]).unwrap();
|
||||
assert_eq!(input.len(), 0);
|
||||
assert_eq!(
|
||||
filter,
|
||||
Filter {
|
||||
id_list: Some(vec!["1".to_owned()]),
|
||||
..Default::default()
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_id_list_commas() {
|
||||
let (input, filter) = Filter::parse(argv!["1,2,3"]).unwrap();
|
||||
assert_eq!(input.len(), 0);
|
||||
assert_eq!(
|
||||
filter,
|
||||
Filter {
|
||||
id_list: Some(vec!["1".to_owned(), "2".to_owned(), "3".to_owned()]),
|
||||
..Default::default()
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_id_list_uuids() {
|
||||
let (input, filter) = Filter::parse(argv!["1,abcd1234"]).unwrap();
|
||||
assert_eq!(input.len(), 0);
|
||||
assert_eq!(
|
||||
filter,
|
||||
Filter {
|
||||
id_list: Some(vec!["1".to_owned(), "abcd1234".to_owned()]),
|
||||
..Default::default()
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
27
cli/src/argparse/mod.rs
Normal file
27
cli/src/argparse/mod.rs
Normal file
@@ -0,0 +1,27 @@
|
||||
/*!
|
||||
|
||||
This module is responsible for parsing command lines (`Arglist`, an alias for `&[&str]`) into `Command` instances.
|
||||
It removes some redundancy from the command line, for example combining the multiple ways to modify a task into a single `Modification` struct.
|
||||
|
||||
The module is organized as a nom parser over ArgList, and each struct has a `parse` method to parse such a list.
|
||||
|
||||
The exception to this rule is the `args` sub-module, which contains string parsers that are applied to indivdual command-line elements.
|
||||
|
||||
All of the structs produced by this module are fully-owned, data-only structs.
|
||||
That is, they contain no references, and have no methods to aid in their execution -- that is the `invocation` module's job.
|
||||
|
||||
*/
|
||||
mod args;
|
||||
mod command;
|
||||
mod filter;
|
||||
mod modification;
|
||||
mod report;
|
||||
mod subcommand;
|
||||
|
||||
pub(crate) use command::Command;
|
||||
pub(crate) use filter::Filter;
|
||||
pub(crate) use modification::{DescriptionMod, Modification};
|
||||
pub(crate) use report::Report;
|
||||
pub(crate) use subcommand::Subcommand;
|
||||
|
||||
type ArgList<'a> = &'a [&'a str];
|
||||
119
cli/src/argparse/modification.rs
Normal file
119
cli/src/argparse/modification.rs
Normal file
@@ -0,0 +1,119 @@
|
||||
use super::args::{any, arg_matching};
|
||||
use super::ArgList;
|
||||
use nom::{combinator::*, multi::fold_many0, IResult};
|
||||
use taskchampion::Status;
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub enum DescriptionMod {
|
||||
/// do not change the description
|
||||
None,
|
||||
|
||||
/// Prepend the given value to the description, with a space separator
|
||||
Prepend(String),
|
||||
|
||||
/// Append the given value to the description, with a space separator
|
||||
Append(String),
|
||||
|
||||
/// Set the description
|
||||
Set(String),
|
||||
}
|
||||
|
||||
impl Default for DescriptionMod {
|
||||
fn default() -> Self {
|
||||
Self::None
|
||||
}
|
||||
}
|
||||
|
||||
/// A modification represents a change to a task: adding or removing tags, setting the
|
||||
/// description, and so on.
|
||||
#[derive(Debug, PartialEq, Clone, Default)]
|
||||
pub struct Modification {
|
||||
/// Change the description
|
||||
pub description: DescriptionMod,
|
||||
|
||||
/// Set the status
|
||||
pub status: Option<Status>,
|
||||
|
||||
/// Set the "active" status, that is, start (true) or stop (false) the task.
|
||||
pub active: Option<bool>,
|
||||
}
|
||||
|
||||
/// A single argument that is part of a modification, used internally to this module
|
||||
enum ModArg<'a> {
|
||||
Description(&'a str),
|
||||
}
|
||||
|
||||
impl Modification {
|
||||
pub(super) fn parse(input: ArgList) -> IResult<ArgList, Modification> {
|
||||
fn fold(mut acc: Modification, mod_arg: ModArg) -> Modification {
|
||||
match mod_arg {
|
||||
ModArg::Description(description) => {
|
||||
if let DescriptionMod::Set(existing) = acc.description {
|
||||
acc.description =
|
||||
DescriptionMod::Set(format!("{} {}", existing, description));
|
||||
} else {
|
||||
acc.description = DescriptionMod::Set(description.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
acc
|
||||
}
|
||||
fold_many0(
|
||||
Self::description,
|
||||
Modification {
|
||||
..Default::default()
|
||||
},
|
||||
fold,
|
||||
)(input)
|
||||
}
|
||||
|
||||
fn description(input: ArgList) -> IResult<ArgList, ModArg> {
|
||||
fn to_modarg(input: &str) -> Result<ModArg, ()> {
|
||||
Ok(ModArg::Description(input))
|
||||
}
|
||||
map_res(arg_matching(any), to_modarg)(input)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_empty() {
|
||||
let (input, modification) = Modification::parse(argv![]).unwrap();
|
||||
assert_eq!(input.len(), 0);
|
||||
assert_eq!(
|
||||
modification,
|
||||
Modification {
|
||||
..Default::default()
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_single_arg_description() {
|
||||
let (input, modification) = Modification::parse(argv!["newdesc"]).unwrap();
|
||||
assert_eq!(input.len(), 0);
|
||||
assert_eq!(
|
||||
modification,
|
||||
Modification {
|
||||
description: DescriptionMod::Set("newdesc".to_owned()),
|
||||
..Default::default()
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multi_arg_description() {
|
||||
let (input, modification) = Modification::parse(argv!["new", "desc", "fun"]).unwrap();
|
||||
assert_eq!(input.len(), 0);
|
||||
assert_eq!(
|
||||
modification,
|
||||
Modification {
|
||||
description: DescriptionMod::Set("new desc fun".to_owned()),
|
||||
..Default::default()
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
33
cli/src/argparse/report.rs
Normal file
33
cli/src/argparse/report.rs
Normal file
@@ -0,0 +1,33 @@
|
||||
use super::{ArgList, Filter};
|
||||
use nom::IResult;
|
||||
|
||||
/// A report specifies a filter as well as a sort order and information about which
|
||||
/// task attributes to display
|
||||
#[derive(Debug, PartialEq, Default)]
|
||||
pub(crate) struct Report {
|
||||
pub filter: Filter,
|
||||
}
|
||||
|
||||
impl Report {
|
||||
pub(super) fn parse(input: ArgList) -> IResult<ArgList, Report> {
|
||||
let (input, filter) = Filter::parse(input)?;
|
||||
Ok((input, Report { filter }))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_empty() {
|
||||
let (input, report) = Report::parse(argv![]).unwrap();
|
||||
assert_eq!(input.len(), 0);
|
||||
assert_eq!(
|
||||
report,
|
||||
Report {
|
||||
..Default::default()
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
526
cli/src/argparse/subcommand.rs
Normal file
526
cli/src/argparse/subcommand.rs
Normal file
@@ -0,0 +1,526 @@
|
||||
use super::args::*;
|
||||
use super::{ArgList, DescriptionMod, Filter, Modification, Report};
|
||||
use nom::{branch::alt, combinator::*, sequence::*, IResult};
|
||||
use taskchampion::Status;
|
||||
|
||||
/// A subcommand is the specific operation that the CLI should execute.
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub(crate) enum Subcommand {
|
||||
/// Display the tool version
|
||||
Version,
|
||||
|
||||
/// Display the help output
|
||||
Help {
|
||||
/// Give the summary help (fitting on a few lines)
|
||||
summary: bool,
|
||||
},
|
||||
|
||||
/// Add a new task
|
||||
Add {
|
||||
modification: Modification,
|
||||
},
|
||||
|
||||
/// Modify existing tasks
|
||||
Modify {
|
||||
filter: Filter,
|
||||
modification: Modification,
|
||||
},
|
||||
|
||||
/// Lists (reports)
|
||||
List {
|
||||
report: Report,
|
||||
},
|
||||
|
||||
/// Per-task information (typically one task)
|
||||
Info {
|
||||
filter: Filter,
|
||||
debug: bool,
|
||||
},
|
||||
|
||||
/// Basic operations without args
|
||||
Gc,
|
||||
Sync,
|
||||
}
|
||||
|
||||
impl Subcommand {
|
||||
pub(super) fn parse(input: ArgList) -> IResult<ArgList, Subcommand> {
|
||||
alt((
|
||||
Self::version,
|
||||
Self::help,
|
||||
Self::add,
|
||||
Self::modify_prepend_append,
|
||||
Self::start_stop_done,
|
||||
Self::list,
|
||||
Self::info,
|
||||
Self::gc,
|
||||
Self::sync,
|
||||
))(input)
|
||||
}
|
||||
|
||||
fn version(input: ArgList) -> IResult<ArgList, Subcommand> {
|
||||
fn to_subcommand(_: &str) -> Result<Subcommand, ()> {
|
||||
Ok(Subcommand::Version)
|
||||
}
|
||||
map_res(
|
||||
alt((
|
||||
arg_matching(literal("version")),
|
||||
arg_matching(literal("--version")),
|
||||
)),
|
||||
to_subcommand,
|
||||
)(input)
|
||||
}
|
||||
|
||||
fn help(input: ArgList) -> IResult<ArgList, Subcommand> {
|
||||
fn to_subcommand(input: &str) -> Result<Subcommand, ()> {
|
||||
Ok(Subcommand::Help {
|
||||
summary: input == "-h",
|
||||
})
|
||||
}
|
||||
map_res(
|
||||
alt((
|
||||
arg_matching(literal("help")),
|
||||
arg_matching(literal("--help")),
|
||||
arg_matching(literal("-h")),
|
||||
)),
|
||||
to_subcommand,
|
||||
)(input)
|
||||
}
|
||||
|
||||
fn add(input: ArgList) -> IResult<ArgList, Subcommand> {
|
||||
fn to_subcommand(input: (&str, Modification)) -> Result<Subcommand, ()> {
|
||||
Ok(Subcommand::Add {
|
||||
modification: input.1,
|
||||
})
|
||||
}
|
||||
map_res(
|
||||
pair(arg_matching(literal("add")), Modification::parse),
|
||||
to_subcommand,
|
||||
)(input)
|
||||
}
|
||||
|
||||
fn modify_prepend_append(input: ArgList) -> IResult<ArgList, Subcommand> {
|
||||
fn to_subcommand(input: (Filter, &str, Modification)) -> Result<Subcommand, ()> {
|
||||
let filter = input.0;
|
||||
let mut modification = input.2;
|
||||
|
||||
match input.1 {
|
||||
"prepend" => {
|
||||
if let DescriptionMod::Set(s) = modification.description {
|
||||
modification.description = DescriptionMod::Prepend(s)
|
||||
}
|
||||
}
|
||||
"append" => {
|
||||
if let DescriptionMod::Set(s) = modification.description {
|
||||
modification.description = DescriptionMod::Append(s)
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
Ok(Subcommand::Modify {
|
||||
filter,
|
||||
modification,
|
||||
})
|
||||
}
|
||||
map_res(
|
||||
tuple((
|
||||
Filter::parse,
|
||||
alt((
|
||||
arg_matching(literal("modify")),
|
||||
arg_matching(literal("prepend")),
|
||||
arg_matching(literal("append")),
|
||||
)),
|
||||
Modification::parse,
|
||||
)),
|
||||
to_subcommand,
|
||||
)(input)
|
||||
}
|
||||
|
||||
fn start_stop_done(input: ArgList) -> IResult<ArgList, Subcommand> {
|
||||
// start, stop, and done are special cases of modify
|
||||
fn to_subcommand(input: (Filter, &str, Modification)) -> Result<Subcommand, ()> {
|
||||
let filter = input.0;
|
||||
let mut modification = input.2;
|
||||
match input.1 {
|
||||
"start" => modification.active = Some(true),
|
||||
"stop" => modification.active = Some(false),
|
||||
"done" => modification.status = Some(Status::Completed),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
Ok(Subcommand::Modify {
|
||||
filter,
|
||||
modification,
|
||||
})
|
||||
}
|
||||
map_res(
|
||||
tuple((
|
||||
Filter::parse,
|
||||
alt((
|
||||
arg_matching(literal("start")),
|
||||
arg_matching(literal("stop")),
|
||||
arg_matching(literal("done")),
|
||||
)),
|
||||
Modification::parse,
|
||||
)),
|
||||
to_subcommand,
|
||||
)(input)
|
||||
}
|
||||
|
||||
fn list(input: ArgList) -> IResult<ArgList, Subcommand> {
|
||||
fn to_subcommand(input: (Report, &str)) -> Result<Subcommand, ()> {
|
||||
Ok(Subcommand::List { report: input.0 })
|
||||
}
|
||||
map_res(
|
||||
pair(Report::parse, arg_matching(literal("list"))),
|
||||
to_subcommand,
|
||||
)(input)
|
||||
}
|
||||
|
||||
fn info(input: ArgList) -> IResult<ArgList, Subcommand> {
|
||||
fn to_subcommand(input: (Filter, &str)) -> Result<Subcommand, ()> {
|
||||
let debug = input.1 == "debug";
|
||||
Ok(Subcommand::Info {
|
||||
filter: input.0,
|
||||
debug,
|
||||
})
|
||||
}
|
||||
map_res(
|
||||
pair(
|
||||
Filter::parse,
|
||||
alt((
|
||||
arg_matching(literal("info")),
|
||||
arg_matching(literal("debug")),
|
||||
)),
|
||||
),
|
||||
to_subcommand,
|
||||
)(input)
|
||||
}
|
||||
|
||||
fn gc(input: ArgList) -> IResult<ArgList, Subcommand> {
|
||||
fn to_subcommand(_: &str) -> Result<Subcommand, ()> {
|
||||
Ok(Subcommand::Gc)
|
||||
}
|
||||
map_res(arg_matching(literal("gc")), to_subcommand)(input)
|
||||
}
|
||||
|
||||
fn sync(input: ArgList) -> IResult<ArgList, Subcommand> {
|
||||
fn to_subcommand(_: &str) -> Result<Subcommand, ()> {
|
||||
Ok(Subcommand::Sync)
|
||||
}
|
||||
map_res(arg_matching(literal("sync")), to_subcommand)(input)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
const EMPTY: Vec<&str> = vec![];
|
||||
|
||||
#[test]
|
||||
fn test_version() {
|
||||
assert_eq!(
|
||||
Subcommand::parse(argv!["version"]).unwrap(),
|
||||
(&EMPTY[..], Subcommand::Version)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dd_version() {
|
||||
assert_eq!(
|
||||
Subcommand::parse(argv!["--version"]).unwrap(),
|
||||
(&EMPTY[..], Subcommand::Version)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_d_h() {
|
||||
assert_eq!(
|
||||
Subcommand::parse(argv!["-h"]).unwrap(),
|
||||
(&EMPTY[..], Subcommand::Help { summary: true })
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_help() {
|
||||
assert_eq!(
|
||||
Subcommand::parse(argv!["help"]).unwrap(),
|
||||
(&EMPTY[..], Subcommand::Help { summary: false })
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dd_help() {
|
||||
assert_eq!(
|
||||
Subcommand::parse(argv!["--help"]).unwrap(),
|
||||
(&EMPTY[..], Subcommand::Help { summary: false })
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_description() {
|
||||
let subcommand = Subcommand::Add {
|
||||
modification: Modification {
|
||||
description: DescriptionMod::Set("foo".to_owned()),
|
||||
..Default::default()
|
||||
},
|
||||
};
|
||||
assert_eq!(
|
||||
Subcommand::parse(argv!["add", "foo"]).unwrap(),
|
||||
(&EMPTY[..], subcommand)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_description_multi() {
|
||||
let subcommand = Subcommand::Add {
|
||||
modification: Modification {
|
||||
description: DescriptionMod::Set("foo bar".to_owned()),
|
||||
..Default::default()
|
||||
},
|
||||
};
|
||||
assert_eq!(
|
||||
Subcommand::parse(argv!["add", "foo", "bar"]).unwrap(),
|
||||
(&EMPTY[..], subcommand)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_modify_description_multi() {
|
||||
let subcommand = Subcommand::Modify {
|
||||
filter: Filter {
|
||||
id_list: Some(vec!["123".to_owned()]),
|
||||
},
|
||||
modification: Modification {
|
||||
description: DescriptionMod::Set("foo bar".to_owned()),
|
||||
..Default::default()
|
||||
},
|
||||
};
|
||||
assert_eq!(
|
||||
Subcommand::parse(argv!["123", "modify", "foo", "bar"]).unwrap(),
|
||||
(&EMPTY[..], subcommand)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_append() {
|
||||
let subcommand = Subcommand::Modify {
|
||||
filter: Filter {
|
||||
id_list: Some(vec!["123".to_owned()]),
|
||||
},
|
||||
modification: Modification {
|
||||
description: DescriptionMod::Append("foo bar".to_owned()),
|
||||
..Default::default()
|
||||
},
|
||||
};
|
||||
assert_eq!(
|
||||
Subcommand::parse(argv!["123", "append", "foo", "bar"]).unwrap(),
|
||||
(&EMPTY[..], subcommand)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_prepend() {
|
||||
let subcommand = Subcommand::Modify {
|
||||
filter: Filter {
|
||||
id_list: Some(vec!["123".to_owned()]),
|
||||
},
|
||||
modification: Modification {
|
||||
description: DescriptionMod::Prepend("foo bar".to_owned()),
|
||||
..Default::default()
|
||||
},
|
||||
};
|
||||
assert_eq!(
|
||||
Subcommand::parse(argv!["123", "prepend", "foo", "bar"]).unwrap(),
|
||||
(&EMPTY[..], subcommand)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_done() {
|
||||
let subcommand = Subcommand::Modify {
|
||||
filter: Filter {
|
||||
id_list: Some(vec!["123".to_owned()]),
|
||||
},
|
||||
modification: Modification {
|
||||
status: Some(Status::Completed),
|
||||
..Default::default()
|
||||
},
|
||||
};
|
||||
assert_eq!(
|
||||
Subcommand::parse(argv!["123", "done"]).unwrap(),
|
||||
(&EMPTY[..], subcommand)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_done_modify() {
|
||||
let subcommand = Subcommand::Modify {
|
||||
filter: Filter {
|
||||
id_list: Some(vec!["123".to_owned()]),
|
||||
},
|
||||
modification: Modification {
|
||||
description: DescriptionMod::Set("now-finished".to_owned()),
|
||||
status: Some(Status::Completed),
|
||||
..Default::default()
|
||||
},
|
||||
};
|
||||
assert_eq!(
|
||||
Subcommand::parse(argv!["123", "done", "now-finished"]).unwrap(),
|
||||
(&EMPTY[..], subcommand)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_start() {
|
||||
let subcommand = Subcommand::Modify {
|
||||
filter: Filter {
|
||||
id_list: Some(vec!["123".to_owned()]),
|
||||
},
|
||||
modification: Modification {
|
||||
active: Some(true),
|
||||
..Default::default()
|
||||
},
|
||||
};
|
||||
assert_eq!(
|
||||
Subcommand::parse(argv!["123", "start"]).unwrap(),
|
||||
(&EMPTY[..], subcommand)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_start_modify() {
|
||||
let subcommand = Subcommand::Modify {
|
||||
filter: Filter {
|
||||
id_list: Some(vec!["123".to_owned()]),
|
||||
},
|
||||
modification: Modification {
|
||||
active: Some(true),
|
||||
description: DescriptionMod::Set("mod".to_owned()),
|
||||
..Default::default()
|
||||
},
|
||||
};
|
||||
assert_eq!(
|
||||
Subcommand::parse(argv!["123", "start", "mod"]).unwrap(),
|
||||
(&EMPTY[..], subcommand)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_stop() {
|
||||
let subcommand = Subcommand::Modify {
|
||||
filter: Filter {
|
||||
id_list: Some(vec!["123".to_owned()]),
|
||||
},
|
||||
modification: Modification {
|
||||
active: Some(false),
|
||||
..Default::default()
|
||||
},
|
||||
};
|
||||
assert_eq!(
|
||||
Subcommand::parse(argv!["123", "stop"]).unwrap(),
|
||||
(&EMPTY[..], subcommand)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_stop_modify() {
|
||||
let subcommand = Subcommand::Modify {
|
||||
filter: Filter {
|
||||
id_list: Some(vec!["123".to_owned()]),
|
||||
},
|
||||
modification: Modification {
|
||||
description: DescriptionMod::Set("mod".to_owned()),
|
||||
active: Some(false),
|
||||
..Default::default()
|
||||
},
|
||||
};
|
||||
assert_eq!(
|
||||
Subcommand::parse(argv!["123", "stop", "mod"]).unwrap(),
|
||||
(&EMPTY[..], subcommand)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list() {
|
||||
let subcommand = Subcommand::List {
|
||||
report: Report {
|
||||
..Default::default()
|
||||
},
|
||||
};
|
||||
assert_eq!(
|
||||
Subcommand::parse(argv!["list"]).unwrap(),
|
||||
(&EMPTY[..], subcommand)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_filter() {
|
||||
let subcommand = Subcommand::List {
|
||||
report: Report {
|
||||
filter: Filter {
|
||||
id_list: Some(vec!["12".to_owned(), "13".to_owned()]),
|
||||
},
|
||||
},
|
||||
};
|
||||
assert_eq!(
|
||||
Subcommand::parse(argv!["12,13", "list"]).unwrap(),
|
||||
(&EMPTY[..], subcommand)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_info_filter() {
|
||||
let subcommand = Subcommand::Info {
|
||||
debug: false,
|
||||
filter: Filter {
|
||||
id_list: Some(vec!["12".to_owned(), "13".to_owned()]),
|
||||
},
|
||||
};
|
||||
assert_eq!(
|
||||
Subcommand::parse(argv!["12,13", "info"]).unwrap(),
|
||||
(&EMPTY[..], subcommand)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_debug_filter() {
|
||||
let subcommand = Subcommand::Info {
|
||||
debug: true,
|
||||
filter: Filter {
|
||||
id_list: Some(vec!["12".to_owned()]),
|
||||
},
|
||||
};
|
||||
assert_eq!(
|
||||
Subcommand::parse(argv!["12", "debug"]).unwrap(),
|
||||
(&EMPTY[..], subcommand)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_gc() {
|
||||
let subcommand = Subcommand::Gc;
|
||||
assert_eq!(
|
||||
Subcommand::parse(argv!["gc"]).unwrap(),
|
||||
(&EMPTY[..], subcommand)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_gc_extra_args() {
|
||||
let subcommand = Subcommand::Gc;
|
||||
assert_eq!(
|
||||
Subcommand::parse(argv!["gc", "foo"]).unwrap(),
|
||||
(&vec!["foo"][..], subcommand)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sync() {
|
||||
let subcommand = Subcommand::Sync;
|
||||
assert_eq!(
|
||||
Subcommand::parse(argv!["sync"]).unwrap(),
|
||||
(&EMPTY[..], subcommand)
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user