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:
Dustin J. Mitchell
2020-12-03 06:58:10 +00:00
parent 87bb829634
commit 2c579b9f01
45 changed files with 1720 additions and 1072 deletions

View File

@@ -1,60 +1,63 @@
use clap::{App, AppSettings};
use failure::Fallible;
use std::ffi::OsString;
/*!
This crate implements the command-line interface to TaskChampion.
mod cmd;
pub(crate) mod settings;
## Design
The crate is split into two parts: argument parsing (`argparse`) and command invocation (`invocation`).
Both are fairly complex operations, and the split serves both to isolate that complexity and to facilitate testing.
### Argparse
The TaskChampion command line API is modeled on TaskWarrior's API, which is far from that of a typical UNIX command.
Tools like `clap` and `structopt` are not flexible enough to handle this syntax.
Instead, the `argparse` module uses [nom](https://crates.io/crates/nom) to parse command lines as a sequence of words.
These parsers act on a list of strings, `&[&str]`, and at the top level return a `crate::argparse::Command`.
This is a wholly-owned repesentation of the command line's meaning, but with some interpretation.
For example, `task start`, `task stop`, and `task append` all map to a `crate::argparse::Subcommand::Modify` variant.
### Invocation
The `invocation` module executes a `Command`, given some settings and other ancillary data.
Most of its functionality is in common functions to handle filtering tasks, modifying tasks, and so on.
## Rust API
Note that this crate does not expose a Rust API for use from other crates.
For the public TaskChampion Rust API, see the `taskchampion` crate.
*/
use failure::Fallible;
use std::os::unix::ffi::OsStringExt;
use std::string::FromUtf8Error;
// NOTE: it's important that this 'mod' comes first so that the macros can be used in other modules
mod macros;
mod argparse;
mod invocation;
mod settings;
mod table;
use cmd::ArgMatchResult;
pub(crate) use cmd::CommandInvocation;
/// The main entry point for the command-line interface. This builds an Invocation
/// from the particulars of the operating-system interface, and then executes it.
pub fn main() -> Fallible<()> {
env_logger::init();
/// Parse the given command line and return an as-yet un-executed CommandInvocation.
pub fn parse_command_line<I, T>(iter: I) -> Fallible<CommandInvocation>
where
I: IntoIterator<Item = T>,
T: Into<OsString> + Clone,
{
let subcommands = cmd::subcommands();
// parse the command line into a vector of &str, failing if
// there are invalid utf-8 sequences.
let argv: Vec<String> = std::env::args_os()
.map(|oss| String::from_utf8(oss.into_vec()))
.collect::<Result<_, FromUtf8Error>>()?;
let argv: Vec<&str> = argv.iter().map(|s| s.as_ref()).collect();
let mut app = App::new("TaskChampion")
.version(env!("CARGO_PKG_VERSION"))
.about("Personal task-tracking")
.setting(AppSettings::ColoredHelp);
// parse the command line
let command = argparse::Command::from_argv(&argv[..])?;
for subcommand in subcommands.iter() {
app = subcommand.decorate_app(app);
}
// load the application settings
let settings = settings::read_settings()?;
let matches = app.get_matches_from_safe(iter)?;
for subcommand in subcommands.iter() {
match subcommand.arg_match(&matches) {
ArgMatchResult::Ok(invocation) => return Ok(CommandInvocation::new(invocation)),
ArgMatchResult::Err(err) => return Err(err),
ArgMatchResult::None => {}
}
}
// one of the subcommands also matches the lack of subcommands, so this never
// occurrs.
unreachable!()
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_parse_command_line_success() -> Fallible<()> {
// This just verifies that one of the subcommands works; the subcommands themselves
// are tested in their own unit tests.
parse_command_line(vec!["task", "pending"].iter())?;
Ok(())
}
#[test]
fn test_parse_command_line_failure() {
assert!(parse_command_line(vec!["task", "--no-such-arg"].iter()).is_err());
}
invocation::invoke(command, settings)?;
Ok(())
}