Merge pull request #221 from taskchampion/issue177

Require a filter be specified for modifications
This commit is contained in:
Dustin J. Mitchell 2021-05-21 10:25:45 -04:00 committed by GitHub
commit 9d78654573
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 109 additions and 34 deletions

View file

@ -1,8 +1,13 @@
use super::args::{arg_matching, id_list, minus_tag, plus_tag, status_colon, TaskId}; use super::args::{arg_matching, id_list, literal, minus_tag, plus_tag, status_colon, TaskId};
use super::ArgList; use super::ArgList;
use crate::usage; use crate::usage;
use anyhow::bail; use anyhow::bail;
use nom::{branch::alt, combinator::*, multi::fold_many0, IResult}; use nom::{
branch::alt,
combinator::*,
multi::{fold_many0, fold_many1},
IResult,
};
use taskchampion::Status; use taskchampion::Status;
/// A filter represents a selection of a particular set of tasks. /// A filter represents a selection of a particular set of tasks.
@ -85,7 +90,9 @@ impl Condition {
} }
impl Filter { impl Filter {
pub(super) fn parse(input: ArgList) -> IResult<ArgList, Filter> { /// Parse a filter that can include an empty set of args (meaning
/// all tasks)
pub(super) fn parse0(input: ArgList) -> IResult<ArgList, Filter> {
fold_many0( fold_many0(
Condition::parse, Condition::parse,
Filter { Filter {
@ -95,6 +102,30 @@ impl Filter {
)(input) )(input)
} }
/// Parse a filter that must have at least one arg, which can be `all`
/// to mean all tasks
pub(super) fn parse1(input: ArgList) -> IResult<ArgList, Filter> {
alt((
Filter::parse_all,
fold_many1(
Condition::parse,
Filter {
..Default::default()
},
|acc, arg| acc.with_arg(arg),
),
))(input)
}
fn parse_all(input: ArgList) -> IResult<ArgList, Filter> {
fn to_filter(_: &str) -> Result<Filter, ()> {
Ok(Filter {
..Default::default()
})
}
map_res(arg_matching(literal("all")), to_filter)(input)
}
/// fold multiple filter args into a single Filter instance /// fold multiple filter args into a single Filter instance
fn with_arg(mut self, cond: Condition) -> Filter { fn with_arg(mut self, cond: Condition) -> Filter {
if let Condition::IdList(mut id_list) = cond { if let Condition::IdList(mut id_list) = cond {
@ -157,6 +188,13 @@ impl Filter {
description: " description: "
Select tasks with the given status.", Select tasks with the given status.",
}); });
u.filters.push(usage::Filter {
syntax: "all",
summary: "All tasks",
description: "
When specified alone for task-modification commands, `all` matches all tasks.
For example, `task all done` will mark all tasks as done.",
});
} }
} }
@ -165,8 +203,8 @@ mod test {
use super::*; use super::*;
#[test] #[test]
fn test_empty() { fn test_empty_parse0() {
let (input, filter) = Filter::parse(argv![]).unwrap(); let (input, filter) = Filter::parse0(argv![]).unwrap();
assert_eq!(input.len(), 0); assert_eq!(input.len(), 0);
assert_eq!( assert_eq!(
filter, filter,
@ -176,9 +214,46 @@ mod test {
); );
} }
#[test]
fn test_empty_parse1() {
// parse1 does not allow empty input
assert!(Filter::parse1(argv![]).is_err());
}
#[test]
fn test_all_parse0() {
let (input, _) = Filter::parse0(argv!["all"]).unwrap();
assert_eq!(input.len(), 1); // did not parse "all"
}
#[test]
fn test_all_parse1() {
let (input, filter) = Filter::parse1(argv!["all"]).unwrap();
assert_eq!(input.len(), 0);
assert_eq!(
filter,
Filter {
..Default::default()
}
);
}
#[test]
fn test_all_with_other_stuff() {
let (input, filter) = Filter::parse1(argv!["all", "+foo"]).unwrap();
// filter ends after `all`
assert_eq!(input.len(), 1);
assert_eq!(
filter,
Filter {
..Default::default()
}
);
}
#[test] #[test]
fn test_id_list_single() { fn test_id_list_single() {
let (input, filter) = Filter::parse(argv!["1"]).unwrap(); let (input, filter) = Filter::parse0(argv!["1"]).unwrap();
assert_eq!(input.len(), 0); assert_eq!(input.len(), 0);
assert_eq!( assert_eq!(
filter, filter,
@ -190,7 +265,7 @@ mod test {
#[test] #[test]
fn test_id_list_commas() { fn test_id_list_commas() {
let (input, filter) = Filter::parse(argv!["1,2,3"]).unwrap(); let (input, filter) = Filter::parse0(argv!["1,2,3"]).unwrap();
assert_eq!(input.len(), 0); assert_eq!(input.len(), 0);
assert_eq!( assert_eq!(
filter, filter,
@ -206,7 +281,7 @@ mod test {
#[test] #[test]
fn test_id_list_multi_arg() { fn test_id_list_multi_arg() {
let (input, filter) = Filter::parse(argv!["1,2", "3,4"]).unwrap(); let (input, filter) = Filter::parse0(argv!["1,2", "3,4"]).unwrap();
assert_eq!(input.len(), 0); assert_eq!(input.len(), 0);
assert_eq!( assert_eq!(
filter, filter,
@ -223,7 +298,7 @@ mod test {
#[test] #[test]
fn test_id_list_uuids() { fn test_id_list_uuids() {
let (input, filter) = Filter::parse(argv!["1,abcd1234"]).unwrap(); let (input, filter) = Filter::parse0(argv!["1,abcd1234"]).unwrap();
assert_eq!(input.len(), 0); assert_eq!(input.len(), 0);
assert_eq!( assert_eq!(
filter, filter,
@ -238,7 +313,7 @@ mod test {
#[test] #[test]
fn test_tags() { fn test_tags() {
let (input, filter) = Filter::parse(argv!["1", "+yes", "-no"]).unwrap(); let (input, filter) = Filter::parse0(argv!["1", "+yes", "-no"]).unwrap();
assert_eq!(input.len(), 0); assert_eq!(input.len(), 0);
assert_eq!( assert_eq!(
filter, filter,
@ -254,7 +329,7 @@ mod test {
#[test] #[test]
fn test_status() { fn test_status() {
let (input, filter) = Filter::parse(argv!["status:completed", "status:pending"]).unwrap(); let (input, filter) = Filter::parse0(argv!["status:completed", "status:pending"]).unwrap();
assert_eq!(input.len(), 0); assert_eq!(input.len(), 0);
assert_eq!( assert_eq!(
filter, filter,
@ -269,8 +344,8 @@ mod test {
#[test] #[test]
fn intersect_idlist_idlist() { fn intersect_idlist_idlist() {
let left = Filter::parse(argv!["1,2", "+yes"]).unwrap().1; let left = Filter::parse0(argv!["1,2", "+yes"]).unwrap().1;
let right = Filter::parse(argv!["2,3", "+no"]).unwrap().1; let right = Filter::parse0(argv!["2,3", "+no"]).unwrap().1;
let both = left.intersect(right); let both = left.intersect(right);
assert_eq!( assert_eq!(
both, both,
@ -289,8 +364,8 @@ mod test {
#[test] #[test]
fn intersect_idlist_alltasks() { fn intersect_idlist_alltasks() {
let left = Filter::parse(argv!["1,2", "+yes"]).unwrap().1; let left = Filter::parse0(argv!["1,2", "+yes"]).unwrap().1;
let right = Filter::parse(argv!["+no"]).unwrap().1; let right = Filter::parse0(argv!["+no"]).unwrap().1;
let both = left.intersect(right); let both = left.intersect(right);
assert_eq!( assert_eq!(
both, both,
@ -308,8 +383,8 @@ mod test {
#[test] #[test]
fn intersect_alltasks_alltasks() { fn intersect_alltasks_alltasks() {
let left = Filter::parse(argv!["+yes"]).unwrap().1; let left = Filter::parse0(argv!["+yes"]).unwrap().1;
let right = Filter::parse(argv!["+no"]).unwrap().1; let right = Filter::parse0(argv!["+no"]).unwrap().1;
let both = left.intersect(right); let both = left.intersect(right);
assert_eq!( assert_eq!(
both, both,

View file

@ -217,7 +217,7 @@ impl Modify {
} }
map_res( map_res(
tuple(( tuple((
Filter::parse, Filter::parse1,
alt(( alt((
arg_matching(literal("modify")), arg_matching(literal("modify")),
arg_matching(literal("prepend")), arg_matching(literal("prepend")),
@ -235,47 +235,47 @@ impl Modify {
fn get_usage(u: &mut usage::Usage) { fn get_usage(u: &mut usage::Usage) {
u.subcommands.push(usage::Subcommand { u.subcommands.push(usage::Subcommand {
name: "modify", name: "modify",
syntax: "[filter] modify [modification]", syntax: "<filter> modify [modification]",
summary: "Modify tasks", summary: "Modify tasks",
description: " description: "
Modify all tasks matching the filter.", Modify all tasks matching the required filter.",
}); });
u.subcommands.push(usage::Subcommand { u.subcommands.push(usage::Subcommand {
name: "prepend", name: "prepend",
syntax: "[filter] prepend [modification]", syntax: "<filter> prepend [modification]",
summary: "Prepend task description", summary: "Prepend task description",
description: " description: "
Modify all tasks matching the filter by inserting the given description before each Modify all tasks matching the required filter by inserting the given description before each
task's description.", task's description.",
}); });
u.subcommands.push(usage::Subcommand { u.subcommands.push(usage::Subcommand {
name: "append", name: "append",
syntax: "[filter] append [modification]", syntax: "<filter> append [modification]",
summary: "Append task description", summary: "Append task description",
description: " description: "
Modify all tasks matching the filter by adding the given description to the end Modify all tasks matching the required filter by adding the given description to the end
of each task's description.", of each task's description.",
}); });
u.subcommands.push(usage::Subcommand { u.subcommands.push(usage::Subcommand {
name: "start", name: "start",
syntax: "[filter] start [modification]", syntax: "<filter> start [modification]",
summary: "Start tasks", summary: "Start tasks",
description: " description: "
Start all tasks matching the filter, additionally applying any given modifications." Start all tasks matching the required filter, additionally applying any given modifications."
}); });
u.subcommands.push(usage::Subcommand { u.subcommands.push(usage::Subcommand {
name: "stop", name: "stop",
syntax: "[filter] stop [modification]", syntax: "<filter> stop [modification]",
summary: "Stop tasks", summary: "Stop tasks",
description: " description: "
Stop all tasks matching the filter, additionally applying any given modifications.", Stop all tasks matching the required filter, additionally applying any given modifications.",
}); });
u.subcommands.push(usage::Subcommand { u.subcommands.push(usage::Subcommand {
name: "done", name: "done",
syntax: "[filter] done [modification]", syntax: "<filter> done [modification]",
summary: "Mark tasks as completed", summary: "Mark tasks as completed",
description: " description: "
Mark all tasks matching the filter as completed, additionally applying any given Mark all tasks matching the required filter as completed, additionally applying any given
modifications.", modifications.",
}); });
} }
@ -293,14 +293,14 @@ impl Report {
} }
// allow the filter expression before or after the report name // allow the filter expression before or after the report name
alt(( alt((
map_res(pair(arg_matching(report_name), Filter::parse), |input| { map_res(pair(arg_matching(report_name), Filter::parse0), |input| {
to_subcommand(input.1, input.0) to_subcommand(input.1, input.0)
}), }),
map_res(pair(Filter::parse, arg_matching(report_name)), |input| { map_res(pair(Filter::parse0, arg_matching(report_name)), |input| {
to_subcommand(input.0, input.1) to_subcommand(input.0, input.1)
}), }),
// default to a "next" report // default to a "next" report
map_res(Filter::parse, |input| to_subcommand(input, "next")), map_res(Filter::parse0, |input| to_subcommand(input, "next")),
))(input) ))(input)
} }
@ -335,7 +335,7 @@ impl Info {
} }
map_res( map_res(
pair( pair(
Filter::parse, Filter::parse1,
alt(( alt((
arg_matching(literal("info")), arg_matching(literal("info")),
arg_matching(literal("debug")), arg_matching(literal("debug")),