gdritter repos rrecutils / 9ec7965
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
9 changed file(s) with 369 addition(s) and 5 deletion(s). Collapse all Expand all
1212 serde = "*"
1313 serde_json = "*"
1414 clap = "2.27.1"
15 rustache = "*"
1516
1617 [[bin]]
1718 name = "rr-pretty"
2021 [[bin]]
2122 name = "rr-to-json"
2223 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
1010
1111 Id: 1
1212 Type: sell
13 Date: 20 April 2011
13 Date: 20 April 2011\
14 and a thing
1415
1516 Id: 2
1617 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
16 struct ParsingContext {
2 continuation_line: bool,
37 current_record_type: Option<String>,
48 }
9
510
611 #[derive(Eq, PartialEq, Debug)]
712 pub struct Record {
914 pub fields: Vec<(String, String)>,
1015 }
1116
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
1234 #[derive(Eq, PartialEq, Debug)]
1335 pub struct Recfile {
1436 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 }
1556 }
1657
1758
1960 pub fn parse<I>(i: I) -> Result<Recfile, String>
2061 where I: std::io::BufRead
2162 {
22 let mut iter = i.lines();
63 let mut iter = ContinuationLines::new(i.lines());
2364 let mut current = Record {
2465 fields: vec![],
2566 rec_type: None,
2667 };
2768 let mut buf = vec![];
2869 let mut ctx = ParsingContext {
29 continuation_line: false,
3070 current_record_type: None,
3171 };
3272
76116
77117 Ok(Recfile { records: buf })
78118 }
119
79120 }
80121
81122 #[cfg(test)]
86127 let file = Recfile {
87128 records: expected.iter().map( |v| {
88129 Record {
130 rec_type: None,
89131 fields: v.iter().map( |&(k, v)| {
90132 (k.to_owned(), v.to_owned())
91133 }).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 }
22 extern crate rrecutils;
33
44 fn main() {
5 let matches = clap::App::new("rr-pretty")
5 let _matches = clap::App::new("rr-pretty")
66 .version("0.0")
77 .author("Getty Ritter <rrecutils@infinitenegativeutility.com>")
88 .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 }