diff --git a/src/cli.rs b/src/cli.rs index db2244b8e8..0933dfe353 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -320,6 +320,7 @@ pub async fn cli() -> Result<(), Box> { whole_stream_command(FromINI), whole_stream_command(FromBSON), whole_stream_command(FromJSON), + whole_stream_command(FromODS), whole_stream_command(FromDB), whole_stream_command(FromSQLite), whole_stream_command(FromTOML), diff --git a/src/commands.rs b/src/commands.rs index 843042b2a3..d28ce3b279 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -31,6 +31,7 @@ pub(crate) mod from_bson; pub(crate) mod from_csv; pub(crate) mod from_ini; pub(crate) mod from_json; +pub(crate) mod from_ods; pub(crate) mod from_sqlite; pub(crate) mod from_ssv; pub(crate) mod from_toml; @@ -125,6 +126,7 @@ pub(crate) use from_bson::FromBSON; pub(crate) use from_csv::FromCSV; pub(crate) use from_ini::FromINI; pub(crate) use from_json::FromJSON; +pub(crate) use from_ods::FromODS; pub(crate) use from_sqlite::FromDB; pub(crate) use from_sqlite::FromSQLite; pub(crate) use from_ssv::FromSSV; diff --git a/src/commands/from_ods.rs b/src/commands/from_ods.rs new file mode 100644 index 0000000000..0f1910250f --- /dev/null +++ b/src/commands/from_ods.rs @@ -0,0 +1,113 @@ +use crate::commands::WholeStreamCommand; +use crate::prelude::*; +use crate::TaggedListBuilder; +use calamine::*; +use nu_errors::ShellError; +use nu_protocol::{Primitive, ReturnSuccess, Signature, TaggedDictBuilder, UntaggedValue, Value}; +use std::io::Cursor; + +pub struct FromODS; + +#[derive(Deserialize)] +pub struct FromODSArgs { + headerless: bool, +} + +impl WholeStreamCommand for FromODS { + fn name(&self) -> &str { + "from-ods" + } + + fn signature(&self) -> Signature { + Signature::build("from-ods") + .switch("headerless", "don't treat the first row as column names") + } + + fn usage(&self) -> &str { + "Parse OpenDocument Spreadsheet(.ods) data and create table." + } + + fn run( + &self, + args: CommandArgs, + registry: &CommandRegistry, + ) -> Result { + args.process(registry, from_ods)?.run() + } +} + +fn from_ods( + FromODSArgs { + headerless: _headerless, + }: FromODSArgs, + runnable_context: RunnableContext, +) -> Result { + let input = runnable_context.input; + let tag = runnable_context.name; + + let stream = async_stream! { + let values: Vec = input.values.collect().await; + + for value in values { + let value_span = value.tag.span; + let value_tag = value.tag.clone(); + + match value.value { + UntaggedValue::Primitive(Primitive::Binary(vb)) => { + let mut buf: Cursor> = Cursor::new(vb); + let mut ods = Ods::<_>::new(buf).map_err(|_| ShellError::labeled_error( + "Could not load ods file", + "could not load ods file", + &tag))?; + + let mut dict = TaggedDictBuilder::new(&tag); + + let sheet_names = ods.sheet_names().to_owned(); + + for sheet_name in &sheet_names { + let mut sheet_output = TaggedListBuilder::new(&tag); + + if let Some(Ok(current_sheet)) = ods.worksheet_range(sheet_name) { + for row in current_sheet.rows() { + let mut row_output = TaggedDictBuilder::new(&tag); + for (i, cell) in row.iter().enumerate() { + let value = match cell { + DataType::Empty => UntaggedValue::nothing(), + DataType::String(s) => UntaggedValue::string(s), + DataType::Float(f) => UntaggedValue::decimal(*f), + DataType::Int(i) => UntaggedValue::int(*i), + DataType::Bool(b) => UntaggedValue::boolean(*b), + _ => UntaggedValue::nothing(), + }; + + row_output.insert_untagged(&format!("Column{}", i), value); + } + + sheet_output.push_untagged(row_output.into_untagged_value()); + } + + dict.insert_untagged(sheet_name, sheet_output.into_untagged_value()); + } else { + yield Err(ShellError::labeled_error( + "Could not load sheet", + "could not load sheet", + &tag)); + } + } + + yield ReturnSuccess::value(dict.into_value()); + } + _ => yield Err(ShellError::labeled_error_with_secondary( + "Expected binary data from pipeline", + "requires binary data input", + &tag, + "value originates from here", + value_tag, + )), + + } + } + }; + + Ok(stream.to_output_stream()) +} diff --git a/src/utils.rs b/src/utils.rs index 3cb42b894b..1d45307636 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -344,6 +344,10 @@ mod tests { loc: fixtures().join("sample.url"), at: 0 }, + Res { + loc: fixtures().join("sample_data.ods"), + at: 0 + }, Res { loc: fixtures().join("sample_data.xlsx"), at: 0 diff --git a/tests/converting_formats/mod.rs b/tests/converting_formats/mod.rs index eff8d68556..c222f449d4 100644 --- a/tests/converting_formats/mod.rs +++ b/tests/converting_formats/mod.rs @@ -1,6 +1,7 @@ mod bson; mod csv; mod json; +mod ods; mod sqlite; mod ssv; mod toml; diff --git a/tests/converting_formats/ods.rs b/tests/converting_formats/ods.rs new file mode 100644 index 0000000000..1597f9b522 --- /dev/null +++ b/tests/converting_formats/ods.rs @@ -0,0 +1,17 @@ +use nu_test_support::{nu, pipeline}; + +#[test] +fn from_ods_file_to_table() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + open sample_data.ods + | get SalesOrders + | nth 4 + | get Column2 + | echo $it + "# + )); + + assert_eq!(actual, "Gill"); +} diff --git a/tests/fixtures/formats/sample_data.ods b/tests/fixtures/formats/sample_data.ods new file mode 100644 index 0000000000..5bcd2bda44 Binary files /dev/null and b/tests/fixtures/formats/sample_data.ods differ