Browse Source

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 6 years ago
parent
commit
9ec7965b8f
9 changed files with 369 additions and 5 deletions
  1. 9 0
      Cargo.toml
  2. 1 0
      samples/books.mustache
  3. 30 0
      samples/books.rec
  4. 2 1
      samples/entries.rec
  5. 99 0
      src/contlines.rs
  6. 45 3
      src/lib.rs
  7. 132 0
      src/tools/format.rs
  8. 1 1
      src/tools/pretty.rs
  9. 50 0
      src/tools/select.rs

+ 9 - 0
Cargo.toml

@@ -12,6 +12,7 @@ regex = "0.2"
 serde = "*"
 serde_json = "*"
 clap = "2.27.1"
+rustache = "*"
 
 [[bin]]
 name = "rr-pretty"
@@ -20,3 +21,11 @@ path = "src/tools/pretty.rs"
 [[bin]]
 name = "rr-to-json"
 path = "src/tools/tojson.rs"
+
+[[bin]]
+name = "rr-sel"
+path = "src/tools/select.rs"
+
+[[bin]]
+name = "rr-format"
+path = "src/tools/format.rs"

+ 1 - 0
samples/books.mustache

@@ -0,0 +1 @@
+the book "{{Title}}" by {{Author}} is a {{%rec}}

+ 30 - 0
samples/books.rec

@@ -0,0 +1,30 @@
+# -*- mode: rec -*-
+
+%rec: Book
+%mandatory: Title
+%type: Location enum loaned home unknown
+%doc:
++ A book in my personal collection.
+
+Title: GNU Emacs Manual
+Author: Richard M. Stallman
+Publisher: FSF
+Location: home
+
+Title: The Colour of Magic
+Author: Terry Pratchett
+Location: loaned
+
+Title: Mio Cid
+Author: Anonymous
+Location: home
+
+Title: chapters.gnu.org administration guide
+Author: Nacho Gonzalez
+Author: Jose E. Marchesi
+Location: unknown
+
+Title: Yeelong User Manual
+Location: home
+
+# End of books.rec

+ 2 - 1
samples/entries.rec

@@ -10,7 +10,8 @@ Title: Article 2
 
 Id: 1
 Type: sell
-Date: 20 April 2011
+Date: 20 April 2011\
+ and a thing
 
 Id: 2
 Type: stock

+ 99 - 0
src/contlines.rs

@@ -0,0 +1,99 @@
+use std::io;
+
+/// An iterator that abstracts over continuation characters on
+/// subsequent lines
+pub struct ContinuationLines<R: Iterator<Item=io::Result<String>>> {
+    underlying: R,
+}
+
+impl<R: Iterator<Item=io::Result<String>>> ContinuationLines<R> {
+    fn join_next(&mut self, mut past: String) -> Option<io::Result<String>> {
+        let next = self.underlying.next();
+        match next {
+            None => Some(Ok(past)),
+            Some(Err(err)) => Some(Err(err)),
+            Some(Ok(ref new)) => {
+                if new.ends_with("\\") {
+                    let end = new.len() - 1;
+                    past.push_str(&new[(0..end)]);
+                    self.join_next(past)
+                } else {
+                    past.push_str(&new);
+                    Some(Ok(past))
+                }
+            }
+        }
+    }
+
+    pub fn new(iter: R) -> ContinuationLines<R> {
+        ContinuationLines { underlying: iter }
+    }
+}
+
+impl<R: Iterator<Item=io::Result<String>>> Iterator for ContinuationLines<R> {
+    type Item = io::Result<String>;
+
+    fn next(&mut self) -> Option<io::Result<String>> {
+        let next = self.underlying.next();
+        match next {
+            None => None,
+            Some(Err(err)) => Some(Err(err)),
+            Some(Ok(x)) => {
+                if x.ends_with("\\") {
+                    let end = x.len() - 1;
+                    self.join_next(x[(0..end)].to_owned())
+                } else {
+                    Some(Ok(x))
+                }
+            }
+        }
+    }
+
+}
+
+#[cfg(test)]
+mod tests {
+    use super::ContinuationLines;
+    use std::io::{BufRead, Cursor};
+
+    fn test_contlines(input: &[u8], expected: Vec<&str>) {
+        // build a ContinuationLines iterator from our input buffer,
+        // and unwrap all the IO exceptions we would get
+        let mut i = ContinuationLines::new(Cursor::new(input).lines())
+            .map(Result::unwrap);
+        // walk the expected values and make sure those are the ones
+        // we're getting
+        for e in expected.into_iter() {
+            assert_eq!(i.next(), Some(e.to_owned()));
+        }
+        // and then make sure we're at the end
+        assert_eq!(i.next(), None);
+    }
+
+    #[test]
+    fn no_contlines() {
+        test_contlines(b"foo\nbar\n", vec!["foo", "bar"]);
+    }
+
+    #[test]
+    fn two_joined_lines() {
+        test_contlines(b"foo\\\nbar\n", vec!["foobar"]);
+    }
+
+    #[test]
+    fn three_joined_lines() {
+        test_contlines(
+            b"foo\\\nbar\\\nbaz\n",
+            vec!["foobarbaz"],
+        );
+    }
+
+    #[test]
+    fn mixed_joins() {
+        test_contlines(
+            b"foo\nbar\\\nbaz\nquux\n",
+            vec!["foo", "barbaz", "quux"],
+        );
+    }
+
+}

+ 45 - 3
src/lib.rs

@@ -1,32 +1,72 @@
+pub mod contlines;
+
+use contlines::ContinuationLines;
+
+
 struct ParsingContext {
-    continuation_line: bool,
     current_record_type: Option<String>,
 }
 
+
 #[derive(Eq, PartialEq, Debug)]
 pub struct Record {
     pub rec_type: Option<String>,
     pub fields: Vec<(String, String)>,
 }
 
+impl Record {
+    pub fn write<W>(&self, w: &mut W) -> std::io::Result<()>
+        where W: std::io::Write
+    {
+        for &(ref name, ref value) in self.fields.iter() {
+            write!(w, "{}: {}\n", name, value)?;
+        }
+
+        write!(w, "\n")
+    }
+
+    pub fn size(&self) -> usize {
+        self.fields.len()
+    }
+}
+
+
 #[derive(Eq, PartialEq, Debug)]
 pub struct Recfile {
     pub records: Vec<Record>,
 }
 
+impl Recfile {
+    pub fn write<W>(&self, w: &mut W) -> std::io::Result<()>
+        where W: std::io::Write
+    {
+        for r in self.records.iter() {
+            r.write(w)?;
+        }
+
+        Ok(())
+    }
+
+    pub fn filter_by_type(&mut self, type_name: &str) {
+        self.records.retain(|r| match r.rec_type {
+            Some(ref t) => t == type_name,
+            None => false,
+        });
+    }
+}
+
 
 impl Recfile {
     pub fn parse<I>(i: I) -> Result<Recfile, String>
         where I: std::io::BufRead
     {
-        let mut iter = i.lines();
+        let mut iter = ContinuationLines::new(i.lines());
         let mut current = Record {
             fields: vec![],
             rec_type: None,
         };
         let mut buf = vec![];
         let mut ctx = ParsingContext {
-            continuation_line: false,
             current_record_type: None,
         };
 
@@ -76,6 +116,7 @@ impl Recfile {
 
         Ok(Recfile { records: buf })
     }
+
 }
 
 #[cfg(test)]
@@ -86,6 +127,7 @@ mod tests {
         let file = Recfile {
             records: expected.iter().map( |v| {
                 Record {
+                    rec_type: None,
                     fields: v.iter().map( |&(k, v)| {
                         (k.to_owned(), v.to_owned())
                     }).collect(),

+ 132 - 0
src/tools/format.rs

@@ -0,0 +1,132 @@
+extern crate clap;
+extern crate rrecutils;
+extern crate rustache;
+
+use std::{fs,io};
+use std::convert::From;
+use std::string::FromUtf8Error;
+
+use rustache::Render;
+
+struct R {
+    rec: rrecutils::Record
+}
+
+impl Render for R {
+    fn render<W: io::Write>(
+        &self,
+        template: &str,
+        writer: &mut W,
+    ) -> Result<(), rustache::RustacheError>
+    {
+        use rustache::HashBuilder;
+        let mut hb = HashBuilder::new();
+        if let Some(ref t) = self.rec.rec_type {
+            hb = hb.insert("%rec", t.clone());
+        }
+        for field in self.rec.fields.iter() {
+            hb = hb.insert(&field.0, field.1.clone());
+        }
+        hb.render(template, writer)
+    }
+}
+
+enum FormatErr {
+    IOError(io::Error),
+    Utf8Error(FromUtf8Error),
+    Rustache(rustache::RustacheError),
+    Generic(String),
+}
+
+impl From<io::Error> for FormatErr {
+    fn from(err: io::Error) -> FormatErr {
+        FormatErr::IOError(err)
+    }
+}
+
+impl From<FromUtf8Error> for FormatErr {
+    fn from(err: FromUtf8Error) -> FormatErr {
+        FormatErr::Utf8Error(err)
+    }
+}
+
+impl From<rustache::RustacheError> for FormatErr {
+    fn from(err: rustache::RustacheError) -> FormatErr {
+        FormatErr::Rustache(err)
+    }
+}
+
+impl From<String> for FormatErr {
+    fn from(err: String) -> FormatErr {
+        FormatErr::Generic(err)
+    }
+}
+
+
+fn run() -> Result<(), FormatErr> {
+    let matches = clap::App::new("rr-format")
+        .version("0.0")
+        .author("Getty Ritter <rrecutils@infinitenegativeutility.com>")
+        .about("Display the Rust AST for a Recutils file")
+        .arg(clap::Arg::with_name("input")
+             .short("i")
+             .long("input")
+             .value_name("FILE")
+             .help("The input recfile (or - for stdin)"))
+        .arg(clap::Arg::with_name("output")
+             .short("o")
+             .long("output")
+             .value_name("FILE")
+             .help("The desired output location (or - for stdout)"))
+        .arg(clap::Arg::with_name("template")
+             .short("t")
+             .long("template")
+             .value_name("FILE")
+             .help("The template to use"))
+        .arg(clap::Arg::with_name("joiner")
+             .short("j")
+             .long("joiner")
+             .value_name("STRING")
+             .help("The string used to separate each fragment"))
+        .get_matches();
+
+    let stdin = io::stdin();
+
+    let input: Box<io::BufRead> =
+        match matches.value_of("input").unwrap_or("-") {
+            "-" => Box::new(stdin.lock()),
+            path =>
+                Box::new(io::BufReader::new(fs::File::open(path)?)),
+        };
+
+    let template: String = match matches.value_of("template") {
+        Some(path) => {
+            use io::Read;
+            let mut buf = Vec::new();
+            fs::File::open(path)?.read_to_end(&mut buf)?;
+            String::from_utf8(buf)?
+        },
+        None => panic!("No template specified!"),
+    };
+
+    let recfile = rrecutils::Recfile::parse(input)?;
+
+    let mut output: Box<io::Write> =
+        match matches.value_of("output").unwrap_or("-") {
+            "-" => Box::new(io::stdout()),
+            path => Box::new(fs::File::open(path)?),
+        };
+
+    for r in recfile.records.into_iter() {
+        R { rec: r }.render(&template, &mut output.as_mut())?;
+    }
+
+    Ok(())
+}
+
+fn main() {
+    match run() {
+        Ok(()) => (),
+        Err(err) => panic!(err),
+    }
+}

+ 1 - 1
src/tools/pretty.rs

@@ -2,7 +2,7 @@ extern crate clap;
 extern crate rrecutils;
 
 fn main() {
-    let matches = clap::App::new("rr-pretty")
+    let _matches = clap::App::new("rr-pretty")
         .version("0.0")
         .author("Getty Ritter <rrecutils@infinitenegativeutility.com>")
         .about("Display the Rust AST for a Recutils file")

+ 50 - 0
src/tools/select.rs

@@ -0,0 +1,50 @@
+extern crate clap;
+extern crate rrecutils;
+
+fn main() {
+    let matches = clap::App::new("rr-sel")
+        .version("0.0")
+        .author("Getty Ritter <rrecutils@infinitenegativeutility.com>")
+        .about("Print records from a recfile")
+
+        .arg(clap::Arg::with_name("type")
+             .long("type")
+             .short("t")
+             .required(false)
+             .takes_value(true))
+
+        .arg(clap::Arg::with_name("include-descriptors")
+             .long("include-descriptors")
+             .short("d")
+             .required(false)
+             .takes_value(false))
+
+        .arg(clap::Arg::with_name("collapse")
+             .long("collapse")
+             .short("C")
+             .required(false)
+             .takes_value(false))
+
+        .arg(clap::Arg::with_name("sort")
+             .long("sort")
+             .short("S")
+             .required(false)
+             .takes_value(true))
+
+        .arg(clap::Arg::with_name("group-by")
+             .long("group-by")
+             .short("G")
+             .required(false)
+             .takes_value(true))
+
+        .get_matches();
+
+    let source = std::io::stdin();
+    let mut records = rrecutils::Recfile::parse(source.lock()).unwrap();
+
+    if let Some(typ) = matches.value_of("type") {
+        records.filter_by_type(typ);
+    }
+
+    records.write(&mut std::io::stdout());
+}