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