1 #![deny(clippy::all)]
2 #![warn(clippy::pedantic)]
3 #![allow(clippy::similar_names)]
4
5 use calamine::{open_workbook_auto, DataType, Reader};
6 use std::error::Error;
7 use std::fmt::{self, Display, Debug, Formatter, Write};
8 use std::io;
9 use clap::{App, Arg, crate_name, crate_version};
10 use std::convert::TryFrom;
11
12 pub enum Errors {
13 InvalidSeparator,
14 Empty, NotFound(String),
15 Csv(csv::Error),
16 Spreadsheet(calamine::Error),
17 CellError(calamine::CellErrorType),
18 }
19
20 impl Error for Errors {}
21 // delegate to Display so the error message is not crap
22 impl Debug for Errors {
fmt(&self, fmt: &mut Formatter) -> fmt::Result23 fn fmt(&self, fmt: &mut Formatter) -> fmt::Result {
24 (&self as &dyn Display).fmt(fmt)
25 }
26 }
27 impl Display for Errors {
fmt(&self, fmt: &mut Formatter) -> fmt::Result28 fn fmt(&self, fmt: &mut Formatter) -> fmt::Result {
29 use Errors::*;
30 match self {
31 Empty => write!(fmt, "Empty spreadsheet"),
32 NotFound(ref s) => write!(fmt, "Could not find sheet \"{}\" in spreadsheet", s),
33 InvalidSeparator => write!(fmt, "A provided separator is invalid, separators need to be a single ascii chacter"),
34 Csv(ref e) => write!(fmt, "{}", e),
35 Spreadsheet(ref e) => write!(fmt, "{}", e),
36 CellError(ref e) => write!(fmt, "Error found in cell ({:?})", e)
37 }
38 }
39 }
40 impl From<csv::Error> for Errors {
from(err: csv::Error) -> Self41 fn from(err: csv::Error) -> Self {
42 Self::Csv(err)
43 }
44 }
45 impl From<calamine::Error> for Errors {
from(err: calamine::Error) -> Self46 fn from(err: calamine::Error) -> Self {
47 Self::Spreadsheet(err)
48 }
49 }
50
separator_to_byte(s: &str) -> Result<u8, Errors>51 fn separator_to_byte(s: &str) -> Result<u8, Errors> {
52 s.chars().next()
53 .ok_or(Errors::InvalidSeparator)
54 .and_then(|c| u8::try_from(c as u32).map_err(|_| Errors::InvalidSeparator))
55 }
run(n: &'static str, default_rs: &'static str, default_fs: &'static str) -> Result<(), Errors>56 pub fn run(n: &'static str, default_rs: &'static str, default_fs: &'static str) -> Result<(), Errors> {
57
58 let matches = App::new(crate_name!())
59 .version(crate_version!())
60 .about("Converts spreadsheets to text")
61 .long_about(&format!("Converts the first sheet of the spreadsheet at PATH (or <sheet> if \
62 requested) to {} sent to stdout.
63
64 Should be able to convert from (and automatically guess between) XLS, XLSX, XLSB and ODS.", n) as &str)
65 .arg(Arg::with_name("PATH").help("Spreadsheet file path").required(true))
66 .arg(Arg::with_name("sheet")
67 .short("s").long("sheet")
68 .default_value("1")
69 .help("Name or index (1 is first) of the sheet to convert")
70 )
71 .arg(Arg::with_name("RS")
72 .short("r").long("rs").long("record-separator")
73 .takes_value(true)
74 .help("Record separator (a single character)")
75 )
76 .arg(Arg::with_name("FS")
77 .short("f").long("fs").long("field-separator")
78 .takes_value(true)
79 .help("Field separator (a single character)")
80 )
81 .get_matches();
82
83 let rs = separator_to_byte(matches.value_of("RS").unwrap_or(default_rs))?;
84 let fs = separator_to_byte(matches.value_of("FS").unwrap_or(default_fs))?;
85
86 let mut workbook = open_workbook_auto(matches.value_of("PATH").unwrap())?;
87
88 let sheet = matches.value_of("sheet").unwrap_or("1");
89 // if sheet is a number get corresponding sheet in list
90 let name = String::from(
91 sheet.parse::<usize>().ok()
92 .and_then(|n| workbook.sheet_names().get(n.saturating_sub(1)))
93 .map_or(sheet, |s| s as &str)
94 );
95
96 let range = if let Some(Ok(r)) = workbook.worksheet_range(&name) {
97 r
98 } else {
99 return Err(Errors::NotFound(name));
100 };
101
102 let stdout = io::stdout();
103 let mut out = csv::WriterBuilder::new()
104 .terminator(csv::Terminator::Any(rs))
105 .delimiter(fs)
106 .from_writer(stdout.lock());
107
108 let mut contents = vec![String::new();range.width()];
109 for row in range.rows() {
110 for (c, cell) in row.iter().zip(contents.iter_mut()) {
111 cell.clear();
112 match *c {
113 DataType::Error(ref e) => return Err(Errors::CellError(e.clone())),
114 // don't go through fmt for strings
115 DataType::String(ref s) => cell.push_str(s),
116 ref rest => write!(cell, "{}", rest).expect("formatting basic types to a string should never fail"),
117 };
118 }
119 out.write_record(&contents)?;
120 }
121
122 Ok(())
123 }
124