From aa5ea533c7c2234b194b531f9134f4c0e764e1a3 Mon Sep 17 00:00:00 2001 From: Matthias Beyer Date: Fri, 15 Feb 2019 20:23:40 +0100 Subject: [PATCH] Add imag-header imag-header is a tool for querying information of an entry in a defined format. imag-header is intended for scripting with imag commands. The output format is not stable yet, though. Signed-off-by: Matthias Beyer --- Cargo.toml | 1 + bin/core/imag-header/Cargo.toml | 49 +++++ bin/core/imag-header/README.md | 1 + bin/core/imag-header/src/main.rs | 363 +++++++++++++++++++++++++++++++ bin/core/imag-header/src/ui.rs | 244 +++++++++++++++++++++ doc/src/04020-module-header.md | 4 + scripts/release.sh | 1 + 7 files changed, 663 insertions(+) create mode 100644 bin/core/imag-header/Cargo.toml create mode 120000 bin/core/imag-header/README.md create mode 100644 bin/core/imag-header/src/main.rs create mode 100644 bin/core/imag-header/src/ui.rs create mode 100644 doc/src/04020-module-header.md diff --git a/Cargo.toml b/Cargo.toml index 742f533f..20f7d34f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ members = [ "bin/core/imag-git", "bin/core/imag-gps", "bin/core/imag-grep", + "bin/core/imag-header", "bin/core/imag-ids", "bin/core/imag-init", "bin/core/imag-link", diff --git a/bin/core/imag-header/Cargo.toml b/bin/core/imag-header/Cargo.toml new file mode 100644 index 00000000..9287f3b6 --- /dev/null +++ b/bin/core/imag-header/Cargo.toml @@ -0,0 +1,49 @@ +[package] +name = "imag-header" +version = "0.10.0" +authors = ["Matthias Beyer "] + +description = "Part of the imag core distribution: imag-entry command" + +keywords = ["imag", "PIM", "personal", "information", "management"] +readme = "../../../README.md" +license = "LGPL-2.1" + +documentation = "https://imag-pim.org/doc/" +repository = "https://github.com/matthiasbeyer/imag" +homepage = "http://imag-pim.org" + +build = "../../../build.rs" + +[badges] +travis-ci = { repository = "matthiasbeyer/imag" } +is-it-maintained-issue-resolution = { repository = "matthiasbeyer/imag" } +is-it-maintained-open-issues = { repository = "matthiasbeyer/imag" } +maintenance = { status = "actively-developed" } + +[dependencies] +log = "0.4" +version = "2.0.1" +toml = "0.4" +toml-query = "0.8" +filters = "0.3" +failure = "0.1" + +libimagstore = { version = "0.10.0", path = "../../../lib/core/libimagstore" } +libimagrt = { version = "0.10.0", path = "../../../lib/core/libimagrt" } +libimagerror = { version = "0.10.0", path = "../../../lib/core/libimagerror" } +libimagutil = { version = "0.10.0", path = "../../../lib/etc/libimagutil" } +libimagentryedit = { version = "0.10.0", path = "../../../lib/entry/libimagentryedit" } +libimagentryview = { version = "0.10.0", path = "../../../lib/entry/libimagentryview" } + +[dependencies.clap] +version = "^2.29" +default-features = false +features = ["color", "suggestions", "wrap_help"] + +[dev-dependencies.libimagrt] +version = "0.10.0" +path = "../../../lib/core/libimagrt" +default-features = false +features = ["testing"] + diff --git a/bin/core/imag-header/README.md b/bin/core/imag-header/README.md new file mode 120000 index 00000000..490d7257 --- /dev/null +++ b/bin/core/imag-header/README.md @@ -0,0 +1 @@ +../../../doc/src/04020-module-header.md \ No newline at end of file diff --git a/bin/core/imag-header/src/main.rs b/bin/core/imag-header/src/main.rs new file mode 100644 index 00000000..1ee8440c --- /dev/null +++ b/bin/core/imag-header/src/main.rs @@ -0,0 +1,363 @@ +// +// imag - the personal information management suite for the commandline +// Copyright (C) 2015-2019 Matthias Beyer and contributors +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; version +// 2.1 of the License. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +// + +#![deny( + non_camel_case_types, + non_snake_case, + path_statements, + trivial_numeric_casts, + unstable_features, + unused_allocation, + unused_import_braces, + unused_imports, + unused_must_use, + unused_mut, + unused_qualifications, + while_true, +)] + +extern crate clap; +#[macro_use] extern crate log; +extern crate toml; +extern crate toml_query; +extern crate filters; +extern crate failure; + +extern crate libimagentryedit; +extern crate libimagerror; +#[macro_use] extern crate libimagrt; +extern crate libimagstore; +extern crate libimagutil; + +use std::io::Write; +use std::str::FromStr; + +use clap::ArgMatches; +use filters::filter::Filter; +use failure::Error; + +use libimagerror::exit::ExitCode; +use libimagerror::io::ToExitCode; +use libimagerror::iter::TraceIterator; +use libimagerror::trace::MapErrTrace; +use libimagrt::runtime::Runtime; +use libimagrt::setup::generate_runtime_setup; +use libimagstore::iter::get::StoreIdGetIteratorExtension; +use libimagstore::store::FileLockEntry; +use libimagstore::storeid::StoreIdIterator; + +use toml_query::read::TomlValueReadExt; + +mod ui; + +fn main() { + let version = make_imag_version!(); + let rt = generate_runtime_setup("imag-header", + &version, + "Plumbing tool for reading/writing structured data in entries", + ui::build_ui); + + let list_output_with_ids = rt.cli().is_present("list-id"); + let list_output_with_ids_fmt = rt.cli().value_of("list-id-format"); + + trace!("list_output_with_ids = {:?}", list_output_with_ids ); + trace!("list_output_with_ids_fmt = {:?}", list_output_with_ids_fmt); + + let sids = rt.ids::<::ui::PathProvider>().map_err_trace_exit_unwrap(); + + let iter = StoreIdIterator::new(Box::new(sids.into_iter().map(Ok))) + .into_get_iter(rt.store()) + .trace_unwrap_exit() + .filter_map(|x| x); + + let exit_code = match rt.cli().subcommand() { + ("read", Some(mtch)) => read(&rt, mtch, iter), + ("has", Some(mtch)) => has(&rt, mtch, iter), + ("hasnt", Some(mtch)) => hasnt(&rt, mtch, iter), + ("int", Some(mtch)) => int(&rt, mtch, iter), + ("float", Some(mtch)) => float(&rt, mtch, iter), + ("string", Some(mtch)) => string(&rt, mtch, iter), + ("bool", Some(mtch)) => boolean(&rt, mtch, iter), + (other, _mtchs) => { + debug!("Unknown command"); + rt.handle_unknown_subcommand("imag-header", other, rt.cli()) + .map_err_trace_exit_unwrap() + .code() + .unwrap_or(1) + }, + }; + + ::std::process::exit(exit_code) +} + +fn read<'a, 'e, I>(rt: &Runtime, mtch: &ArgMatches<'a>, iter: I) -> i32 + where I: Iterator> +{ + debug!("Processing headers: reading value"); + let header_path = get_header_path(mtch, "header-value-path"); + let mut output = rt.stdout(); + + iter.fold(0, |accu, entry| { + trace!("Processing headers: working on {:?}", entry.get_location()); + entry.get_header() + .read(header_path) + .map_err(Error::from) + .map_err_trace_exit_unwrap() + .map(|value| { + trace!("Processing headers: Got value {:?}", value); + writeln!(output, "{}", value) + .to_exit_code() + .map(|_| accu) + .unwrap_or_else(ExitCode::code) + }) + .unwrap_or_else(|| { + // if value not present and configured + error!("Value not present for entry {} at {}", entry.get_location(), header_path); + 1 + }) + }) +} + +fn has<'a, 'e, I>(rt: &Runtime, mtch: &ArgMatches<'a>, iter: I) -> i32 + where I: Iterator> +{ + debug!("Processing headers: has value"); + let header_path = get_header_path(mtch, "header-value-path"); + let mut output = rt.stdout(); + + iter.fold(0, |accu, entry| { + trace!("Processing headers: working on {:?}", entry.get_location()); + let value = entry.get_header() + .read(header_path) + .map_err(Error::from) + .map_err_trace_exit_unwrap() + .is_some(); + + writeln!(output, "{} - {}", entry.get_location(), value) + .to_exit_code() + .map(|_| if value { accu } else { 1 }) + .unwrap_or_else(ExitCode::code) + }) +} + +fn hasnt<'a, 'e, I>(rt: &Runtime, mtch: &ArgMatches<'a>, iter: I) -> i32 + where I: Iterator> +{ + debug!("Processing headers: hasnt value"); + let header_path = get_header_path(mtch, "header-value-path"); + let mut output = rt.stdout(); + + iter.fold(0, |accu, entry| { + trace!("Processing headers: working on {:?}", entry.get_location()); + let value = entry.get_header() + .read(header_path) + .map_err(Error::from) + .map_err_trace_exit_unwrap() + .is_none(); + + writeln!(output, "{} - {}", entry.get_location(), value) + .to_exit_code() + .map(|_| if value { accu } else { 1 }) + .unwrap_or_else(ExitCode::code) + }) +} + +macro_rules! implement_compare { + { $mtch: ident, $path: expr, $t: ty, $compare: expr } => {{ + trace!("Getting value at {}, comparing as {}", $path, stringify!($t)); + if let Some(cmp) = $mtch.value_of($path).map(FromStr::from_str) { + let cmp : $t = cmp.unwrap(); // safe by clap + trace!("Getting value at {} = {}", $path, cmp); + $compare(cmp) + } else { + true + } + }} +} + +fn int<'a, 'e, I>(rt: &Runtime, mtch: &ArgMatches<'a>, iter: I) -> i32 + where I: Iterator> +{ + debug!("Processing headers: int value"); + let header_path = get_header_path(mtch, "header-value-path"); + let mut output = rt.stdout(); + + let filter = ::filters::ops::bool::Bool::new(true) + .and(|i: &i64| -> bool { + implement_compare!(mtch, "header-int-eq", i64, |cmp| *i == cmp) + }) + .and(|i: &i64| -> bool { + implement_compare!(mtch, "header-int-neq", i64, |cmp| *i != cmp) + }) + .and(|i: &i64| -> bool { + implement_compare!(mtch, "header-int-lt", i64, |cmp| *i < cmp) + }) + .and(|i: &i64| -> bool { + implement_compare!(mtch, "header-int-gt", i64, |cmp| *i > cmp) + }) + .and(|i: &i64| -> bool { + implement_compare!(mtch, "header-int-lte", i64, |cmp| *i <= cmp) + }) + .and(|i: &i64| -> bool { + implement_compare!(mtch, "header-int-gte", i64, |cmp| *i >= cmp) + }); + + iter.fold(0, |accu, entry| { + trace!("Processing headers: working on {:?}", entry.get_location()); + if let Some(v) = entry.get_header() + .read(header_path) + .map_err(Error::from) + .map_err_trace_exit_unwrap() + { + match v { + ::toml::Value::Integer(i) => if filter.filter(&i) { + writeln!(output, "{} - {}", entry.get_location(), i) + .to_exit_code() + .map(|_| accu) + .unwrap_or_else(ExitCode::code) + } else { 1 }, + _ => 1 + } + } else { 1 } + }) +} + +fn float<'a, 'e, I>(rt: &Runtime, mtch: &ArgMatches<'a>, iter: I) -> i32 + where I: Iterator> +{ + debug!("Processing headers: float value"); + let header_path = get_header_path(mtch, "header-value-path"); + let mut output = rt.stdout(); + + let filter = ::filters::ops::bool::Bool::new(true) + .and(|i: &f64| -> bool { + implement_compare!(mtch, "header-float-eq", f64, |cmp| *i == cmp) + }) + .and(|i: &f64| -> bool { + implement_compare!(mtch, "header-float-neq", f64, |cmp| *i != cmp) + }) + .and(|i: &f64| -> bool { + implement_compare!(mtch, "header-float-lt", f64, |cmp| *i < cmp) + }) + .and(|i: &f64| -> bool { + implement_compare!(mtch, "header-float-gt", f64, |cmp| *i > cmp) + }) + .and(|i: &f64| -> bool { + implement_compare!(mtch, "header-float-lte", f64, |cmp| *i <= cmp) + }) + .and(|i: &f64| -> bool { + implement_compare!(mtch, "header-float-gte", f64, |cmp| *i >= cmp) + }); + + iter.fold(0, |accu, entry| { + trace!("Processing headers: working on {:?}", entry.get_location()); + if let Some(v) = entry.get_header() + .read(header_path) + .map_err(Error::from) + .map_err_trace_exit_unwrap() + { + match v { + ::toml::Value::Float(i) => if filter.filter(&i) { + writeln!(output, "{} - {}", entry.get_location(), i) + .to_exit_code() + .map(|_| accu) + .unwrap_or_else(ExitCode::code) + } else { 1 }, + _ => 1 + } + } else { 1 } + }) +} + +fn string<'a, 'e, I>(rt: &Runtime, mtch: &ArgMatches<'a>, iter: I) -> i32 + where I: Iterator> +{ + debug!("Processing headers: string value"); + let header_path = get_header_path(mtch, "header-value-path"); + let mut output = rt.stdout(); + + let filter = ::filters::ops::bool::Bool::new(true) + .and(|i: &String| -> bool { + implement_compare!(mtch, "header-string-eq", String, |cmp| *i == cmp) + }) + .and(|i: &String| -> bool { + implement_compare!(mtch, "header-string-neq", String, |cmp| *i != cmp) + }); + + iter.fold(0, |accu, entry| { + trace!("Processing headers: working on {:?}", entry.get_location()); + if let Some(v) = entry.get_header() + .read(header_path) + .map_err(Error::from) + .map_err_trace_exit_unwrap() + { + match v { + ::toml::Value::String(s) => if filter.filter(&s) { + writeln!(output, "{} - {}", entry.get_location(), s) + .to_exit_code() + .map(|_| accu) + .unwrap_or_else(ExitCode::code) + } else { 1 }, + _ => 1 + } + } else { 1 } + }) +} + +fn boolean<'a, 'e, I>(rt: &Runtime, mtch: &ArgMatches<'a>, iter: I) -> i32 + where I: Iterator> +{ + debug!("Processing headers: bool value"); + let header_path = get_header_path(mtch, "header-value-path"); + let mut output = rt.stdout(); + + let filter = ::filters::ops::bool::Bool::new(true) + .and(|i: &bool| -> bool { *i }) + .and(|i: &bool| -> bool { *i }); + + iter.fold(0, |accu, entry| { + trace!("Processing headers: working on {:?}", entry.get_location()); + if let Some(v) = entry.get_header() + .read(header_path) + .map_err(Error::from) + .map_err_trace_exit_unwrap() + { + match v { + ::toml::Value::Boolean(b) => if filter.filter(&b) { + writeln!(output, "{} - {}", entry.get_location(), b) + .to_exit_code() + .map(|_| accu) + .unwrap_or_else(ExitCode::code) + } else { 1 }, + _ => 1 + } + } else { 1 } + }) +} + + + +// helpers +// +fn get_header_path<'a>(mtch: &'a ArgMatches<'a>, path: &'static str) -> &'a str { + let header_path = mtch.value_of(path).unwrap(); // safe by clap + debug!("Processing headers: header path = {}", header_path); + header_path +} + diff --git a/bin/core/imag-header/src/ui.rs b/bin/core/imag-header/src/ui.rs new file mode 100644 index 00000000..f42030eb --- /dev/null +++ b/bin/core/imag-header/src/ui.rs @@ -0,0 +1,244 @@ +// +// imag - the personal information management suite for the commandline +// Copyright (C) 2015-2019 Matthias Beyer and contributors +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; version +// 2.1 of the License. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +// + +use std::path::PathBuf; + +use clap::{Arg, ArgMatches, App, SubCommand}; + +use libimagstore::storeid::StoreId; +use libimagstore::storeid::IntoStoreId; +use libimagrt::runtime::IdPathProvider; +use libimagerror::trace::MapErrTrace; + +pub fn build_ui<'a>(app: App<'a, 'a>) -> App<'a, 'a> { + app + .arg(Arg::with_name("id") + .index(1) + .takes_value(true) + .required(false) + .multiple(true) + .help("The id of the entry/entries to edit") + .value_name("ENTRY")) + + .arg(Arg::with_name("list-id") + .long("list-id") + .takes_value(false) + .required(false) + .multiple(false) + .help("List Store Id in output (format: ' - '")) + .arg(Arg::with_name("list-id-format") + .long("list-id-format") + .takes_value(true) + .required(false) + .multiple(false) + .help("List Store Id in output with format")) + + .subcommand(SubCommand::with_name("read") + .about("Read a header value by path") + .version("0.1") + .arg(Arg::with_name("header-value-path") + .index(1) + .takes_value(true) + .required(true) + .multiple(false) + .help("Path of the header value") + .value_name("PATH")) + ) + + .subcommand(SubCommand::with_name("has") + .about("Check whether a header value is present") + .version("0.1") + .arg(Arg::with_name("header-value-path") + .index(1) + .takes_value(true) + .required(true) + .multiple(false) + .help("Path of the header value") + .value_name("PATH")) + ) + + .subcommand(SubCommand::with_name("hasnt") + .about("Check whether a header value is not present") + .version("0.1") + .arg(Arg::with_name("header-value-path") + .index(1) + .takes_value(true) + .required(true) + .multiple(false) + .help("Path of the header value") + .value_name("PATH")) + ) + + .subcommand(SubCommand::with_name("int") + .about("Check whether a header value is a number") + .version("0.1") + .arg(Arg::with_name("header-value-path") + .index(1) + .takes_value(true) + .required(true) + .multiple(false) + .help("Path of the header value") + .value_name("PATH")) + + .arg(Arg::with_name("header-int-eq") + .long("eq") + .takes_value(true) + .required(false) + .help("Check whether the value is equal to VALUE") + .validator(::libimagutil::cli_validators::is_integer) + .value_name("VALUE")) + .arg(Arg::with_name("header-int-neq") + .long("neq") + .takes_value(true) + .required(false) + .help("Check whether the value is not equal to VALUE") + .validator(::libimagutil::cli_validators::is_integer) + .value_name("VALUE")) + .arg(Arg::with_name("header-int-lt") + .long("lt") + .takes_value(true) + .required(false) + .help("Check whether the value is lower than VALUE") + .validator(::libimagutil::cli_validators::is_integer) + .value_name("VALUE")) + .arg(Arg::with_name("header-int-gt") + .long("gt") + .takes_value(true) + .required(false) + .help("Check whether the value is greater than VALUE") + .validator(::libimagutil::cli_validators::is_integer) + .value_name("VALUE")) + .arg(Arg::with_name("header-int-lte") + .long("lte") + .takes_value(true) + .required(false) + .help("Check whether the value is lower than or equal VALUE") + .validator(::libimagutil::cli_validators::is_integer) + .value_name("VALUE")) + .arg(Arg::with_name("header-int-gte") + .long("gte") + .takes_value(true) + .required(false) + .help("Check whether the value is greater than or equal VALUE") + .validator(::libimagutil::cli_validators::is_integer) + .value_name("VALUE")) + ) + + .subcommand(SubCommand::with_name("float") + .about("Check whether a header value is a floating number") + .version("0.1") + .arg(Arg::with_name("header-value-path") + .index(1) + .takes_value(true) + .required(true) + .multiple(false) + .help("Path of the header value") + .value_name("PATH")) + + .arg(Arg::with_name("header-float-eq") + .long("eq") + .takes_value(true) + .required(false) + .help("Check whether the value is equal to VALUE") + .validator(::libimagutil::cli_validators::is_float) + .value_name("VALUE")) + .arg(Arg::with_name("header-float-neq") + .long("neq") + .takes_value(true) + .required(false) + .help("Check whether the value is not equal to VALUE") + .validator(::libimagutil::cli_validators::is_float) + .value_name("VALUE")) + .arg(Arg::with_name("header-float-lt") + .long("lt") + .takes_value(true) + .required(false) + .help("Check whether the value is lower than VALUE") + .validator(::libimagutil::cli_validators::is_float) + .value_name("VALUE")) + .arg(Arg::with_name("header-float-gt") + .long("gt") + .takes_value(true) + .required(false) + .help("Check whether the value is greater than VALUE") + .validator(::libimagutil::cli_validators::is_float) + .value_name("VALUE")) + ) + + .subcommand(SubCommand::with_name("string") + .about("Check whether a header value is a string") + .version("0.1") + .arg(Arg::with_name("header-value-path") + .index(1) + .takes_value(true) + .required(true) + .multiple(false) + .help("Path of the header value") + .value_name("PATH")) + + .arg(Arg::with_name("header-string-eq") + .long("eq") + .takes_value(true) + .required(false) + .help("Check whether the value is equal to VALUE") + .value_name("VALUE")) + .arg(Arg::with_name("header-string-neq") + .long("neq") + .takes_value(true) + .required(false) + .help("Check whether the value is not equal to VALUE") + .value_name("VALUE")) + ) + + .subcommand(SubCommand::with_name("bool") + .about("Check whether a header value is a bool") + .version("0.1") + .arg(Arg::with_name("header-value-path") + .index(1) + .takes_value(true) + .required(true) + .multiple(false) + .help("Path of the header value") + .value_name("PATH")) + + .arg(Arg::with_name("header-bool-set") + .short("t") + .long("set") + .takes_value(false) + .required(false) + .help("Check whether the flag is set (true)")) + .arg(Arg::with_name("header-bool-unset") + .short("f") + .long("unset") + .takes_value(false) + .required(false) + .help("Check whether the flag is unset (false)")) + ) +} + +pub struct PathProvider; +impl IdPathProvider for PathProvider { + fn get_ids(matches: &ArgMatches) -> Vec { + matches.values_of("id") + .unwrap() + .map(|s| PathBuf::from(s).into_storeid().map_err_trace_exit_unwrap()) + .collect() + } +} + diff --git a/doc/src/04020-module-header.md b/doc/src/04020-module-header.md new file mode 100644 index 00000000..888a9f82 --- /dev/null +++ b/doc/src/04020-module-header.md @@ -0,0 +1,4 @@ +## Entry {#sec:modules:entry} + +Plumbing tool for modifying and querying structured data in entries. + diff --git a/scripts/release.sh b/scripts/release.sh index 0d971bb2..5c41d0e8 100644 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -63,6 +63,7 @@ CRATES=( ./bin/core/imag-ids ./bin/core/imag-git ./bin/core/imag-category + ./bin/core/imag-header ./bin/core/imag )