nu_command/formats/from/
ods.rs1use calamine::*;
2use indexmap::IndexMap;
3use nu_engine::command_prelude::*;
4
5use std::io::Cursor;
6
7#[derive(Clone)]
8pub struct FromOds;
9
10impl Command for FromOds {
11 fn name(&self) -> &str {
12 "from ods"
13 }
14
15 fn signature(&self) -> Signature {
16 Signature::build("from ods")
17 .input_output_types(vec![(Type::String, Type::table())])
18 .allow_variants_without_examples(true)
19 .named(
20 "sheets",
21 SyntaxShape::List(Box::new(SyntaxShape::String)),
22 "Only convert specified sheets",
23 Some('s'),
24 )
25 .category(Category::Formats)
26 }
27
28 fn description(&self) -> &str {
29 "Parse OpenDocument Spreadsheet(.ods) data and create table."
30 }
31
32 fn run(
33 &self,
34 engine_state: &EngineState,
35 stack: &mut Stack,
36 call: &Call,
37 input: PipelineData,
38 ) -> Result<PipelineData, ShellError> {
39 let head = call.head;
40
41 let sel_sheets = if let Some(Value::List { vals: columns, .. }) =
42 call.get_flag(engine_state, stack, "sheets")?
43 {
44 convert_columns(columns.as_slice())?
45 } else {
46 vec![]
47 };
48
49 let metadata = input.metadata().map(|md| md.with_content_type(None));
50 from_ods(input, head, sel_sheets).map(|pd| pd.set_metadata(metadata))
51 }
52
53 fn examples(&self) -> Vec<Example<'_>> {
54 vec![
55 Example {
56 description: "Convert binary .ods data to a table",
57 example: "open --raw test.ods | from ods",
58 result: None,
59 },
60 Example {
61 description: "Convert binary .ods data to a table, specifying the tables",
62 example: "open --raw test.ods | from ods --sheets [Spreadsheet1]",
63 result: None,
64 },
65 ]
66 }
67}
68
69fn convert_columns(columns: &[Value]) -> Result<Vec<String>, ShellError> {
70 let res = columns
71 .iter()
72 .map(|value| match &value {
73 Value::String { val: s, .. } => Ok(s.clone()),
74 _ => Err(ShellError::IncompatibleParametersSingle {
75 msg: "Incorrect column format, Only string as column name".to_string(),
76 span: value.span(),
77 }),
78 })
79 .collect::<Result<Vec<String>, _>>()?;
80
81 Ok(res)
82}
83
84fn collect_binary(input: PipelineData, span: Span) -> Result<Vec<u8>, ShellError> {
85 if let PipelineData::ByteStream(stream, ..) = input {
86 stream.into_bytes()
87 } else {
88 let mut bytes = vec![];
89 let mut values = input.into_iter();
90
91 loop {
92 match values.next() {
93 Some(Value::Binary { val: b, .. }) => {
94 bytes.extend_from_slice(&b);
95 }
96 Some(Value::Error { error, .. }) => return Err(*error),
97 Some(x) => {
98 return Err(ShellError::UnsupportedInput {
99 msg: "Expected binary from pipeline".to_string(),
100 input: "value originates from here".into(),
101 msg_span: span,
102 input_span: x.span(),
103 });
104 }
105 None => break,
106 }
107 }
108
109 Ok(bytes)
110 }
111}
112
113fn from_ods(
114 input: PipelineData,
115 head: Span,
116 sel_sheets: Vec<String>,
117) -> Result<PipelineData, ShellError> {
118 let span = input.span();
119 let bytes = collect_binary(input, head)?;
120 let buf: Cursor<Vec<u8>> = Cursor::new(bytes);
121 let mut ods = Ods::<_>::new(buf).map_err(|_| ShellError::UnsupportedInput {
122 msg: "Could not load ODS file".to_string(),
123 input: "value originates from here".into(),
124 msg_span: head,
125 input_span: span.unwrap_or(head),
126 })?;
127
128 let mut dict = IndexMap::new();
129
130 let mut sheet_names = ods.sheet_names();
131 if !sel_sheets.is_empty() {
132 sheet_names.retain(|e| sel_sheets.contains(e));
133 }
134
135 for sheet_name in sheet_names {
136 let mut sheet_output = vec![];
137
138 if let Ok(current_sheet) = ods.worksheet_range(&sheet_name) {
139 for row in current_sheet.rows() {
140 let record = row
141 .iter()
142 .enumerate()
143 .map(|(i, cell)| {
144 let value = match cell {
145 Data::Empty => Value::nothing(head),
146 Data::String(s) => Value::string(s, head),
147 Data::Float(f) => Value::float(*f, head),
148 Data::Int(i) => Value::int(*i, head),
149 Data::Bool(b) => Value::bool(*b, head),
150 _ => Value::nothing(head),
151 };
152
153 (format!("column{i}"), value)
154 })
155 .collect();
156
157 sheet_output.push(Value::record(record, head));
158 }
159
160 dict.insert(sheet_name, Value::list(sheet_output, head));
161 } else {
162 return Err(ShellError::UnsupportedInput {
163 msg: "Could not load sheet".to_string(),
164 input: "value originates from here".into(),
165 msg_span: head,
166 input_span: span.unwrap_or(head),
167 });
168 }
169 }
170
171 Ok(PipelineData::value(
172 Value::record(dict.into_iter().collect(), head),
173 None,
174 ))
175}
176
177#[cfg(test)]
178mod tests {
179 use super::*;
180
181 #[test]
182 fn test_examples() {
183 use crate::test_examples;
184
185 test_examples(FromOds {})
186 }
187}