Two new tools + better contlines support
The two new tools are:
- rr-sel, an unfinished port of part of recsel
- rr-format, a tool to render/massage recfiles using
Mustache templates
Backslashes also now correctly parse as continuation lines
Getty Ritter
7 years ago
| 12 | 12 | serde = "*" |
| 13 | 13 | serde_json = "*" |
| 14 | 14 | clap = "2.27.1" |
| 15 | rustache = "*" | |
| 15 | 16 | |
| 16 | 17 | [[bin]] |
| 17 | 18 | name = "rr-pretty" |
| 20 | 21 | [[bin]] |
| 21 | 22 | name = "rr-to-json" |
| 22 | 23 | path = "src/tools/tojson.rs" |
| 24 | ||
| 25 | [[bin]] | |
| 26 | name = "rr-sel" | |
| 27 | path = "src/tools/select.rs" | |
| 28 | ||
| 29 | [[bin]] | |
| 30 | name = "rr-format" | |
| 31 | path = "src/tools/format.rs" | |
| 1 | the book "{{Title}}" by {{Author}} is a {{%rec}} |
| 1 | # -*- mode: rec -*- | |
| 2 | ||
| 3 | %rec: Book | |
| 4 | %mandatory: Title | |
| 5 | %type: Location enum loaned home unknown | |
| 6 | %doc: | |
| 7 | + A book in my personal collection. | |
| 8 | ||
| 9 | Title: GNU Emacs Manual | |
| 10 | Author: Richard M. Stallman | |
| 11 | Publisher: FSF | |
| 12 | Location: home | |
| 13 | ||
| 14 | Title: The Colour of Magic | |
| 15 | Author: Terry Pratchett | |
| 16 | Location: loaned | |
| 17 | ||
| 18 | Title: Mio Cid | |
| 19 | Author: Anonymous | |
| 20 | Location: home | |
| 21 | ||
| 22 | Title: chapters.gnu.org administration guide | |
| 23 | Author: Nacho Gonzalez | |
| 24 | Author: Jose E. Marchesi | |
| 25 | Location: unknown | |
| 26 | ||
| 27 | Title: Yeelong User Manual | |
| 28 | Location: home | |
| 29 | ||
| 30 | # End of books.rec⏎ |
| 10 | 10 | |
| 11 | 11 | Id: 1 |
| 12 | 12 | Type: sell |
| 13 |
Date: 20 April 2011 |
|
| 13 | Date: 20 April 2011\ | |
| 14 | and a thing | |
| 14 | 15 | |
| 15 | 16 | Id: 2 |
| 16 | 17 | Type: stock |
| 1 | use std::io; | |
| 2 | ||
| 3 | /// An iterator that abstracts over continuation characters on | |
| 4 | /// subsequent lines | |
| 5 | pub struct ContinuationLines<R: Iterator<Item=io::Result<String>>> { | |
| 6 | underlying: R, | |
| 7 | } | |
| 8 | ||
| 9 | impl<R: Iterator<Item=io::Result<String>>> ContinuationLines<R> { | |
| 10 | fn join_next(&mut self, mut past: String) -> Option<io::Result<String>> { | |
| 11 | let next = self.underlying.next(); | |
| 12 | match next { | |
| 13 | None => Some(Ok(past)), | |
| 14 | Some(Err(err)) => Some(Err(err)), | |
| 15 | Some(Ok(ref new)) => { | |
| 16 | if new.ends_with("\\") { | |
| 17 | let end = new.len() - 1; | |
| 18 | past.push_str(&new[(0..end)]); | |
| 19 | self.join_next(past) | |
| 20 | } else { | |
| 21 | past.push_str(&new); | |
| 22 | Some(Ok(past)) | |
| 23 | } | |
| 24 | } | |
| 25 | } | |
| 26 | } | |
| 27 | ||
| 28 | pub fn new(iter: R) -> ContinuationLines<R> { | |
| 29 | ContinuationLines { underlying: iter } | |
| 30 | } | |
| 31 | } | |
| 32 | ||
| 33 | impl<R: Iterator<Item=io::Result<String>>> Iterator for ContinuationLines<R> { | |
| 34 | type Item = io::Result<String>; | |
| 35 | ||
| 36 | fn next(&mut self) -> Option<io::Result<String>> { | |
| 37 | let next = self.underlying.next(); | |
| 38 | match next { | |
| 39 | None => None, | |
| 40 | Some(Err(err)) => Some(Err(err)), | |
| 41 | Some(Ok(x)) => { | |
| 42 | if x.ends_with("\\") { | |
| 43 | let end = x.len() - 1; | |
| 44 | self.join_next(x[(0..end)].to_owned()) | |
| 45 | } else { | |
| 46 | Some(Ok(x)) | |
| 47 | } | |
| 48 | } | |
| 49 | } | |
| 50 | } | |
| 51 | ||
| 52 | } | |
| 53 | ||
| 54 | #[cfg(test)] | |
| 55 | mod tests { | |
| 56 | use super::ContinuationLines; | |
| 57 | use std::io::{BufRead, Cursor}; | |
| 58 | ||
| 59 | fn test_contlines(input: &[u8], expected: Vec<&str>) { | |
| 60 | // build a ContinuationLines iterator from our input buffer, | |
| 61 | // and unwrap all the IO exceptions we would get | |
| 62 | let mut i = ContinuationLines::new(Cursor::new(input).lines()) | |
| 63 | .map(Result::unwrap); | |
| 64 | // walk the expected values and make sure those are the ones | |
| 65 | // we're getting | |
| 66 | for e in expected.into_iter() { | |
| 67 | assert_eq!(i.next(), Some(e.to_owned())); | |
| 68 | } | |
| 69 | // and then make sure we're at the end | |
| 70 | assert_eq!(i.next(), None); | |
| 71 | } | |
| 72 | ||
| 73 | #[test] | |
| 74 | fn no_contlines() { | |
| 75 | test_contlines(b"foo\nbar\n", vec!["foo", "bar"]); | |
| 76 | } | |
| 77 | ||
| 78 | #[test] | |
| 79 | fn two_joined_lines() { | |
| 80 | test_contlines(b"foo\\\nbar\n", vec!["foobar"]); | |
| 81 | } | |
| 82 | ||
| 83 | #[test] | |
| 84 | fn three_joined_lines() { | |
| 85 | test_contlines( | |
| 86 | b"foo\\\nbar\\\nbaz\n", | |
| 87 | vec!["foobarbaz"], | |
| 88 | ); | |
| 89 | } | |
| 90 | ||
| 91 | #[test] | |
| 92 | fn mixed_joins() { | |
| 93 | test_contlines( | |
| 94 | b"foo\nbar\\\nbaz\nquux\n", | |
| 95 | vec!["foo", "barbaz", "quux"], | |
| 96 | ); | |
| 97 | } | |
| 98 | ||
| 99 | } |
| 1 | pub mod contlines; | |
| 2 | ||
| 3 | use contlines::ContinuationLines; | |
| 4 | ||
| 5 | ||
| 1 | 6 | struct ParsingContext { |
| 2 | continuation_line: bool, | |
| 3 | 7 | current_record_type: Option<String>, |
| 4 | 8 | } |
| 9 | ||
| 5 | 10 | |
| 6 | 11 | #[derive(Eq, PartialEq, Debug)] |
| 7 | 12 | pub struct Record { |
| 9 | 14 | pub fields: Vec<(String, String)>, |
| 10 | 15 | } |
| 11 | 16 | |
| 17 | impl Record { | |
| 18 | pub fn write<W>(&self, w: &mut W) -> std::io::Result<()> | |
| 19 | where W: std::io::Write | |
| 20 | { | |
| 21 | for &(ref name, ref value) in self.fields.iter() { | |
| 22 | write!(w, "{}: {}\n", name, value)?; | |
| 23 | } | |
| 24 | ||
| 25 | write!(w, "\n") | |
| 26 | } | |
| 27 | ||
| 28 | pub fn size(&self) -> usize { | |
| 29 | self.fields.len() | |
| 30 | } | |
| 31 | } | |
| 32 | ||
| 33 | ||
| 12 | 34 | #[derive(Eq, PartialEq, Debug)] |
| 13 | 35 | pub struct Recfile { |
| 14 | 36 | pub records: Vec<Record>, |
| 37 | } | |
| 38 | ||
| 39 | impl Recfile { | |
| 40 | pub fn write<W>(&self, w: &mut W) -> std::io::Result<()> | |
| 41 | where W: std::io::Write | |
| 42 | { | |
| 43 | for r in self.records.iter() { | |
| 44 | r.write(w)?; | |
| 45 | } | |
| 46 | ||
| 47 | Ok(()) | |
| 48 | } | |
| 49 | ||
| 50 | pub fn filter_by_type(&mut self, type_name: &str) { | |
| 51 | self.records.retain(|r| match r.rec_type { | |
| 52 | Some(ref t) => t == type_name, | |
| 53 | None => false, | |
| 54 | }); | |
| 55 | } | |
| 15 | 56 | } |
| 16 | 57 | |
| 17 | 58 | |
| 19 | 60 | pub fn parse<I>(i: I) -> Result<Recfile, String> |
| 20 | 61 | where I: std::io::BufRead |
| 21 | 62 | { |
| 22 |
let mut iter = |
|
| 63 | let mut iter = ContinuationLines::new(i.lines()); | |
| 23 | 64 | let mut current = Record { |
| 24 | 65 | fields: vec![], |
| 25 | 66 | rec_type: None, |
| 26 | 67 | }; |
| 27 | 68 | let mut buf = vec![]; |
| 28 | 69 | let mut ctx = ParsingContext { |
| 29 | continuation_line: false, | |
| 30 | 70 | current_record_type: None, |
| 31 | 71 | }; |
| 32 | 72 | |
| 76 | 116 | |
| 77 | 117 | Ok(Recfile { records: buf }) |
| 78 | 118 | } |
| 119 | ||
| 79 | 120 | } |
| 80 | 121 | |
| 81 | 122 | #[cfg(test)] |
| 86 | 127 | let file = Recfile { |
| 87 | 128 | records: expected.iter().map( |v| { |
| 88 | 129 | Record { |
| 130 | rec_type: None, | |
| 89 | 131 | fields: v.iter().map( |&(k, v)| { |
| 90 | 132 | (k.to_owned(), v.to_owned()) |
| 91 | 133 | }).collect(), |
| 1 | extern crate clap; | |
| 2 | extern crate rrecutils; | |
| 3 | extern crate rustache; | |
| 4 | ||
| 5 | use std::{fs,io}; | |
| 6 | use std::convert::From; | |
| 7 | use std::string::FromUtf8Error; | |
| 8 | ||
| 9 | use rustache::Render; | |
| 10 | ||
| 11 | struct R { | |
| 12 | rec: rrecutils::Record | |
| 13 | } | |
| 14 | ||
| 15 | impl Render for R { | |
| 16 | fn render<W: io::Write>( | |
| 17 | &self, | |
| 18 | template: &str, | |
| 19 | writer: &mut W, | |
| 20 | ) -> Result<(), rustache::RustacheError> | |
| 21 | { | |
| 22 | use rustache::HashBuilder; | |
| 23 | let mut hb = HashBuilder::new(); | |
| 24 | if let Some(ref t) = self.rec.rec_type { | |
| 25 | hb = hb.insert("%rec", t.clone()); | |
| 26 | } | |
| 27 | for field in self.rec.fields.iter() { | |
| 28 | hb = hb.insert(&field.0, field.1.clone()); | |
| 29 | } | |
| 30 | hb.render(template, writer) | |
| 31 | } | |
| 32 | } | |
| 33 | ||
| 34 | enum FormatErr { | |
| 35 | IOError(io::Error), | |
| 36 | Utf8Error(FromUtf8Error), | |
| 37 | Rustache(rustache::RustacheError), | |
| 38 | Generic(String), | |
| 39 | } | |
| 40 | ||
| 41 | impl From<io::Error> for FormatErr { | |
| 42 | fn from(err: io::Error) -> FormatErr { | |
| 43 | FormatErr::IOError(err) | |
| 44 | } | |
| 45 | } | |
| 46 | ||
| 47 | impl From<FromUtf8Error> for FormatErr { | |
| 48 | fn from(err: FromUtf8Error) -> FormatErr { | |
| 49 | FormatErr::Utf8Error(err) | |
| 50 | } | |
| 51 | } | |
| 52 | ||
| 53 | impl From<rustache::RustacheError> for FormatErr { | |
| 54 | fn from(err: rustache::RustacheError) -> FormatErr { | |
| 55 | FormatErr::Rustache(err) | |
| 56 | } | |
| 57 | } | |
| 58 | ||
| 59 | impl From<String> for FormatErr { | |
| 60 | fn from(err: String) -> FormatErr { | |
| 61 | FormatErr::Generic(err) | |
| 62 | } | |
| 63 | } | |
| 64 | ||
| 65 | ||
| 66 | fn run() -> Result<(), FormatErr> { | |
| 67 | let matches = clap::App::new("rr-format") | |
| 68 | .version("0.0") | |
| 69 | .author("Getty Ritter <rrecutils@infinitenegativeutility.com>") | |
| 70 | .about("Display the Rust AST for a Recutils file") | |
| 71 | .arg(clap::Arg::with_name("input") | |
| 72 | .short("i") | |
| 73 | .long("input") | |
| 74 | .value_name("FILE") | |
| 75 | .help("The input recfile (or - for stdin)")) | |
| 76 | .arg(clap::Arg::with_name("output") | |
| 77 | .short("o") | |
| 78 | .long("output") | |
| 79 | .value_name("FILE") | |
| 80 | .help("The desired output location (or - for stdout)")) | |
| 81 | .arg(clap::Arg::with_name("template") | |
| 82 | .short("t") | |
| 83 | .long("template") | |
| 84 | .value_name("FILE") | |
| 85 | .help("The template to use")) | |
| 86 | .arg(clap::Arg::with_name("joiner") | |
| 87 | .short("j") | |
| 88 | .long("joiner") | |
| 89 | .value_name("STRING") | |
| 90 | .help("The string used to separate each fragment")) | |
| 91 | .get_matches(); | |
| 92 | ||
| 93 | let stdin = io::stdin(); | |
| 94 | ||
| 95 | let input: Box<io::BufRead> = | |
| 96 | match matches.value_of("input").unwrap_or("-") { | |
| 97 | "-" => Box::new(stdin.lock()), | |
| 98 | path => | |
| 99 | Box::new(io::BufReader::new(fs::File::open(path)?)), | |
| 100 | }; | |
| 101 | ||
| 102 | let template: String = match matches.value_of("template") { | |
| 103 | Some(path) => { | |
| 104 | use io::Read; | |
| 105 | let mut buf = Vec::new(); | |
| 106 | fs::File::open(path)?.read_to_end(&mut buf)?; | |
| 107 | String::from_utf8(buf)? | |
| 108 | }, | |
| 109 | None => panic!("No template specified!"), | |
| 110 | }; | |
| 111 | ||
| 112 | let recfile = rrecutils::Recfile::parse(input)?; | |
| 113 | ||
| 114 | let mut output: Box<io::Write> = | |
| 115 | match matches.value_of("output").unwrap_or("-") { | |
| 116 | "-" => Box::new(io::stdout()), | |
| 117 | path => Box::new(fs::File::open(path)?), | |
| 118 | }; | |
| 119 | ||
| 120 | for r in recfile.records.into_iter() { | |
| 121 | R { rec: r }.render(&template, &mut output.as_mut())?; | |
| 122 | } | |
| 123 | ||
| 124 | Ok(()) | |
| 125 | } | |
| 126 | ||
| 127 | fn main() { | |
| 128 | match run() { | |
| 129 | Ok(()) => (), | |
| 130 | Err(err) => panic!(err), | |
| 131 | } | |
| 132 | } |
| 2 | 2 | extern crate rrecutils; |
| 3 | 3 | |
| 4 | 4 | fn main() { |
| 5 |
let |
|
| 5 | let _matches = clap::App::new("rr-pretty") | |
| 6 | 6 | .version("0.0") |
| 7 | 7 | .author("Getty Ritter <rrecutils@infinitenegativeutility.com>") |
| 8 | 8 | .about("Display the Rust AST for a Recutils file") |
| 1 | extern crate clap; | |
| 2 | extern crate rrecutils; | |
| 3 | ||
| 4 | fn main() { | |
| 5 | let matches = clap::App::new("rr-sel") | |
| 6 | .version("0.0") | |
| 7 | .author("Getty Ritter <rrecutils@infinitenegativeutility.com>") | |
| 8 | .about("Print records from a recfile") | |
| 9 | ||
| 10 | .arg(clap::Arg::with_name("type") | |
| 11 | .long("type") | |
| 12 | .short("t") | |
| 13 | .required(false) | |
| 14 | .takes_value(true)) | |
| 15 | ||
| 16 | .arg(clap::Arg::with_name("include-descriptors") | |
| 17 | .long("include-descriptors") | |
| 18 | .short("d") | |
| 19 | .required(false) | |
| 20 | .takes_value(false)) | |
| 21 | ||
| 22 | .arg(clap::Arg::with_name("collapse") | |
| 23 | .long("collapse") | |
| 24 | .short("C") | |
| 25 | .required(false) | |
| 26 | .takes_value(false)) | |
| 27 | ||
| 28 | .arg(clap::Arg::with_name("sort") | |
| 29 | .long("sort") | |
| 30 | .short("S") | |
| 31 | .required(false) | |
| 32 | .takes_value(true)) | |
| 33 | ||
| 34 | .arg(clap::Arg::with_name("group-by") | |
| 35 | .long("group-by") | |
| 36 | .short("G") | |
| 37 | .required(false) | |
| 38 | .takes_value(true)) | |
| 39 | ||
| 40 | .get_matches(); | |
| 41 | ||
| 42 | let source = std::io::stdin(); | |
| 43 | let mut records = rrecutils::Recfile::parse(source.lock()).unwrap(); | |
| 44 | ||
| 45 | if let Some(typ) = matches.value_of("type") { | |
| 46 | records.filter_by_type(typ); | |
| 47 | } | |
| 48 | ||
| 49 | records.write(&mut std::io::stdout()); | |
| 50 | } |