From 0852bfd195f3b916f9cb39cba409cf7b4ba675d3 Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Sat, 1 May 2021 12:49:49 -0400 Subject: [PATCH] Require a filter be specified for modifications This filter can either be `*` or some "real" filter. But an empty set of arguments no longer automatically matches all tasks. --- cli/src/argparse/filter.rs | 109 ++++++++++++++++++++++++++++----- cli/src/argparse/subcommand.rs | 34 +++++----- 2 files changed, 109 insertions(+), 34 deletions(-) diff --git a/cli/src/argparse/filter.rs b/cli/src/argparse/filter.rs index 49273999b..be63aa1af 100644 --- a/cli/src/argparse/filter.rs +++ b/cli/src/argparse/filter.rs @@ -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 { + /// Parse a filter that can include an empty set of args (meaning + /// all tasks) + pub(super) fn parse0(input: ArgList) -> IResult { 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 { + alt(( + Filter::parse_all, + fold_many1( + Condition::parse, + Filter { + ..Default::default() + }, + |acc, arg| acc.with_arg(arg), + ), + ))(input) + } + + fn parse_all(input: ArgList) -> IResult { + fn to_filter(_: &str) -> Result { + 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, diff --git a/cli/src/argparse/subcommand.rs b/cli/src/argparse/subcommand.rs index 5609192b7..53d67556f 100644 --- a/cli/src/argparse/subcommand.rs +++ b/cli/src/argparse/subcommand.rs @@ -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: " 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: " 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: " 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: " 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: " 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: " 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")),