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
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 | } |