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:
107
cli/src/lib.rs
107
cli/src/lib.rs
@@ -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(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user