nu_command/formats/from/
ods.rs

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