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 crate::usage;
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;
/// A filter represents a selection of a particular set of tasks.
@ -85,7 +90,9 @@ impl Condition {
}
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(
Condition::parse,
Filter {
@ -95,6 +102,30 @@ impl Filter {
)(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
fn with_arg(mut self, cond: Condition) -> Filter {
if let Condition::IdList(mut id_list) = cond {
@ -157,6 +188,13 @@ impl Filter {
description: "
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::*;
#[test]
fn test_empty() {
let (input, filter) = Filter::parse(argv![]).unwrap();
fn test_empty_parse0() {
let (input, filter) = Filter::parse0(argv![]).unwrap();
assert_eq!(input.len(), 0);
assert_eq!(
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]
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!(
filter,
@ -190,7 +265,7 @@ mod test {
#[test]
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!(
filter,
@ -206,7 +281,7 @@ mod test {
#[test]
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!(
filter,
@ -223,7 +298,7 @@ mod test {
#[test]
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!(
filter,
@ -238,7 +313,7 @@ mod test {
#[test]
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!(
filter,
@ -254,7 +329,7 @@ mod test {
#[test]
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!(
filter,
@ -269,8 +344,8 @@ mod test {
#[test]
fn intersect_idlist_idlist() {
let left = Filter::parse(argv!["1,2", "+yes"]).unwrap().1;
let right = Filter::parse(argv!["2,3", "+no"]).unwrap().1;
let left = Filter::parse0(argv!["1,2", "+yes"]).unwrap().1;
let right = Filter::parse0(argv!["2,3", "+no"]).unwrap().1;
let both = left.intersect(right);
assert_eq!(
both,
@ -289,8 +364,8 @@ mod test {
#[test]
fn intersect_idlist_alltasks() {
let left = Filter::parse(argv!["1,2", "+yes"]).unwrap().1;
let right = Filter::parse(argv!["+no"]).unwrap().1;
let left = Filter::parse0(argv!["1,2", "+yes"]).unwrap().1;
let right = Filter::parse0(argv!["+no"]).unwrap().1;
let both = left.intersect(right);
assert_eq!(
both,
@ -308,8 +383,8 @@ mod test {
#[test]
fn intersect_alltasks_alltasks() {
let left = Filter::parse(argv!["+yes"]).unwrap().1;
let right = Filter::parse(argv!["+no"]).unwrap().1;
let left = Filter::parse0(argv!["+yes"]).unwrap().1;
let right = Filter::parse0(argv!["+no"]).unwrap().1;
let both = left.intersect(right);
assert_eq!(
both,

View file

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