Begin directory contrib docs and split commands (#3650)

* Begin directory contrib docs and split commands

* Fix unused import warning
This commit is contained in:
JT
2021-06-19 12:06:44 +12:00
committed by GitHub
parent 4140834e4c
commit a74d05061d
261 changed files with 794 additions and 808 deletions

View File

@ -0,0 +1,39 @@
use crate::prelude::*;
use nu_engine::WholeStreamCommand;
use nu_errors::ShellError;
use nu_protocol::{Signature, UntaggedValue};
pub struct From;
impl WholeStreamCommand for From {
fn name(&self) -> &str {
"from"
}
fn signature(&self) -> Signature {
Signature::build("from")
}
fn usage(&self) -> &str {
"Parse content (string or binary) as a table (input format based on subcommand, like csv, ini, json, toml)."
}
fn run(&self, args: CommandArgs) -> Result<OutputStream, ShellError> {
Ok(OutputStream::one(
UntaggedValue::string(get_full_help(&From, args.scope())).into_value(Tag::unknown()),
))
}
}
#[cfg(test)]
mod tests {
use super::From;
use super::ShellError;
#[test]
fn examples_work_as_expected() -> Result<(), ShellError> {
use crate::examples::test as test_examples;
test_examples(From {})
}
}

View File

@ -0,0 +1,107 @@
use super::delimited::from_delimited_data;
use crate::prelude::*;
use nu_engine::WholeStreamCommand;
use nu_errors::ShellError;
use nu_protocol::{Primitive, Signature, SyntaxShape, UntaggedValue, Value};
pub struct FromCsv;
impl WholeStreamCommand for FromCsv {
fn name(&self) -> &str {
"from csv"
}
fn signature(&self) -> Signature {
Signature::build("from csv")
.named(
"separator",
SyntaxShape::String,
"a character to separate columns, defaults to ','",
Some('s'),
)
.switch(
"noheaders",
"don't treat the first row as column names",
Some('n'),
)
}
fn usage(&self) -> &str {
"Parse text as .csv and create table."
}
fn run(&self, args: CommandArgs) -> Result<OutputStream, ShellError> {
from_csv(args)
}
fn examples(&self) -> Vec<Example> {
vec![
Example {
description: "Convert comma-separated data to a table",
example: "open data.txt | from csv",
result: None,
},
Example {
description: "Convert comma-separated data to a table, ignoring headers",
example: "open data.txt | from csv --noheaders",
result: None,
},
Example {
description: "Convert comma-separated data to a table, ignoring headers",
example: "open data.txt | from csv -n",
result: None,
},
Example {
description: "Convert semicolon-separated data to a table",
example: "open data.txt | from csv --separator ';'",
result: None,
},
]
}
}
fn from_csv(args: CommandArgs) -> Result<OutputStream, ShellError> {
let name = args.call_info.name_tag.clone();
let noheaders = args.has_flag("noheaders");
let separator: Option<Value> = args.get_flag("separator")?;
let input = args.input;
let sep = match separator {
Some(Value {
value: UntaggedValue::Primitive(Primitive::String(s)),
tag,
..
}) => {
if s == r"\t" {
'\t'
} else {
let vec_s: Vec<char> = s.chars().collect();
if vec_s.len() != 1 {
return Err(ShellError::labeled_error(
"Expected a single separator char from --separator",
"requires a single character string input",
tag,
));
};
vec_s[0]
}
}
_ => ',',
};
from_delimited_data(noheaders, sep, "CSV", input, name)
}
#[cfg(test)]
mod tests {
use super::FromCsv;
use super::ShellError;
#[test]
fn examples_work_as_expected() -> Result<(), ShellError> {
use crate::examples::test as test_examples;
test_examples(FromCsv {})
}
}

View File

@ -0,0 +1,112 @@
use crate::prelude::*;
use csv::{ErrorKind, ReaderBuilder};
use nu_errors::ShellError;
use nu_protocol::{TaggedDictBuilder, UntaggedValue, Value};
fn from_delimited_string_to_value(
s: String,
noheaders: bool,
separator: char,
tag: impl Into<Tag>,
) -> Result<Value, csv::Error> {
let mut reader = ReaderBuilder::new()
.has_headers(!noheaders)
.delimiter(separator as u8)
.from_reader(s.as_bytes());
let tag = tag.into();
let span = tag.span;
let headers = if noheaders {
(1..=reader.headers()?.len())
.map(|i| format!("Column{}", i))
.collect::<Vec<String>>()
} else {
reader.headers()?.iter().map(String::from).collect()
};
let mut rows = vec![];
for row in reader.records() {
let mut tagged_row = TaggedDictBuilder::new(&tag);
for (value, header) in row?.iter().zip(headers.iter()) {
if let Ok(i) = value.parse::<i64>() {
tagged_row.insert_value(header, UntaggedValue::int(i).into_value(&tag))
} else if let Ok(f) = value.parse::<f64>() {
tagged_row.insert_value(
header,
UntaggedValue::decimal_from_float(f, span).into_value(&tag),
)
} else {
tagged_row.insert_value(header, UntaggedValue::string(value).into_value(&tag))
}
}
rows.push(tagged_row.into_value());
}
Ok(UntaggedValue::Table(rows).into_value(&tag))
}
pub fn from_delimited_data(
noheaders: bool,
sep: char,
format_name: &'static str,
input: InputStream,
name: Tag,
) -> Result<OutputStream, ShellError> {
let name_tag = name;
let concat_string = input.collect_string(name_tag.clone())?;
let sample_lines = concat_string.item.lines().take(3).collect_vec().join("\n");
match from_delimited_string_to_value(concat_string.item, noheaders, sep, name_tag.clone()) {
Ok(x) => match x {
Value {
value: UntaggedValue::Table(list),
..
} => Ok(list.into_iter().into_output_stream()),
x => Ok(OutputStream::one(x)),
},
Err(err) => {
let line_one = match pretty_csv_error(err) {
Some(pretty) => format!(
"Could not parse as {} split by '{}' ({})",
format_name, sep, pretty
),
None => format!("Could not parse as {} split by '{}'", format_name, sep),
};
let line_two = format!(
"input cannot be parsed as {} split by '{}'. Input's first lines:\n{}",
format_name, sep, sample_lines
);
Err(ShellError::labeled_error_with_secondary(
line_one,
line_two,
name_tag,
"value originates from here",
concat_string.tag,
))
}
}
}
fn pretty_csv_error(err: csv::Error) -> Option<String> {
match err.kind() {
ErrorKind::UnequalLengths {
pos,
expected_len,
len,
} => {
if let Some(pos) = pos {
Some(format!(
"Line {}: expected {} fields, found {}",
pos.line(),
expected_len,
len
))
} else {
Some(format!("Expected {} fields, found {}", expected_len, len))
}
}
ErrorKind::Seek => Some("Internal error while parsing csv".to_string()),
_ => None,
}
}

View File

@ -0,0 +1,125 @@
use crate::prelude::*;
use ::eml_parser::eml::*;
use ::eml_parser::EmlParser;
use nu_engine::WholeStreamCommand;
use nu_errors::ShellError;
use nu_protocol::{Signature, SyntaxShape, TaggedDictBuilder, UntaggedValue};
use nu_source::Tagged;
pub struct FromEml;
const DEFAULT_BODY_PREVIEW: usize = 50;
impl WholeStreamCommand for FromEml {
fn name(&self) -> &str {
"from eml"
}
fn signature(&self) -> Signature {
Signature::build("from eml").named(
"preview-body",
SyntaxShape::Int,
"How many bytes of the body to preview",
Some('b'),
)
}
fn usage(&self) -> &str {
"Parse text as .eml and create table."
}
fn run(&self, args: CommandArgs) -> Result<OutputStream, ShellError> {
from_eml(args)
}
}
fn emailaddress_to_value(tag: &Tag, email_address: &EmailAddress) -> TaggedDictBuilder {
let mut dict = TaggedDictBuilder::with_capacity(tag, 2);
let (n, a) = match email_address {
EmailAddress::AddressOnly { address } => {
(UntaggedValue::nothing(), UntaggedValue::string(address))
}
EmailAddress::NameAndEmailAddress { name, address } => {
(UntaggedValue::string(name), UntaggedValue::string(address))
}
};
dict.insert_untagged("Name", n);
dict.insert_untagged("Address", a);
dict
}
fn headerfieldvalue_to_value(tag: &Tag, value: &HeaderFieldValue) -> UntaggedValue {
use HeaderFieldValue::*;
match value {
SingleEmailAddress(address) => emailaddress_to_value(tag, address).into_untagged_value(),
MultipleEmailAddresses(addresses) => UntaggedValue::Table(
addresses
.iter()
.map(|a| emailaddress_to_value(tag, a).into_value())
.collect(),
),
Unstructured(s) => UntaggedValue::string(s),
Empty => UntaggedValue::nothing(),
}
}
fn from_eml(args: CommandArgs) -> Result<OutputStream, ShellError> {
let tag = args.call_info.name_tag.clone();
let preview_body: Option<Tagged<usize>> = args.get_flag("preview-body")?;
let value = args.input.collect_string(tag.clone())?;
let body_preview = preview_body.map(|b| b.item).unwrap_or(DEFAULT_BODY_PREVIEW);
let eml = EmlParser::from_string(value.item)
.with_body_preview(body_preview)
.parse()
.map_err(|_| {
ShellError::labeled_error(
"Could not parse .eml file",
"could not parse .eml file",
&tag,
)
})?;
let mut dict = TaggedDictBuilder::new(&tag);
if let Some(subj) = eml.subject {
dict.insert_untagged("Subject", UntaggedValue::string(subj));
}
if let Some(from) = eml.from {
dict.insert_untagged("From", headerfieldvalue_to_value(&tag, &from));
}
if let Some(to) = eml.to {
dict.insert_untagged("To", headerfieldvalue_to_value(&tag, &to));
}
for HeaderField { name, value } in eml.headers.iter() {
dict.insert_untagged(name, headerfieldvalue_to_value(&tag, value));
}
if let Some(body) = eml.body {
dict.insert_untagged("Body", UntaggedValue::string(body));
}
Ok(OutputStream::one(dict.into_value()))
}
#[cfg(test)]
mod tests {
use super::FromEml;
use super::ShellError;
#[test]
fn examples_work_as_expected() -> Result<(), ShellError> {
use crate::examples::test as test_examples;
test_examples(FromEml {})
}
}

View File

@ -0,0 +1,250 @@
extern crate ical;
use crate::prelude::*;
use ical::parser::ical::component::*;
use ical::property::Property;
use nu_engine::WholeStreamCommand;
use nu_errors::ShellError;
use nu_protocol::{Primitive, Signature, TaggedDictBuilder, UntaggedValue, Value};
use std::io::BufReader;
pub struct FromIcs;
impl WholeStreamCommand for FromIcs {
fn name(&self) -> &str {
"from ics"
}
fn signature(&self) -> Signature {
Signature::build("from ics")
}
fn usage(&self) -> &str {
"Parse text as .ics and create table."
}
fn run(&self, args: CommandArgs) -> Result<OutputStream, ShellError> {
from_ics(args)
}
}
fn from_ics(args: CommandArgs) -> Result<OutputStream, ShellError> {
let tag = args.name_tag();
let input = args.input;
let input_string = input.collect_string(tag.clone())?.item;
let input_bytes = input_string.as_bytes();
let buf_reader = BufReader::new(input_bytes);
let parser = ical::IcalParser::new(buf_reader);
// TODO: it should be possible to make this a stream, but the some of the lifetime requirements make this tricky.
// Pre-computing for now
let mut output = vec![];
for calendar in parser {
match calendar {
Ok(c) => output.push(calendar_to_value(c, tag.clone())),
Err(_) => output.push(Value::error(ShellError::labeled_error(
"Could not parse as .ics",
"input cannot be parsed as .ics",
tag.clone(),
))),
}
}
Ok(output.into_iter().into_output_stream())
}
fn calendar_to_value(calendar: IcalCalendar, tag: Tag) -> Value {
let mut row = TaggedDictBuilder::new(tag.clone());
row.insert_untagged(
"properties",
properties_to_value(calendar.properties, tag.clone()),
);
row.insert_untagged("events", events_to_value(calendar.events, tag.clone()));
row.insert_untagged("alarms", alarms_to_value(calendar.alarms, tag.clone()));
row.insert_untagged("to-Dos", todos_to_value(calendar.todos, tag.clone()));
row.insert_untagged(
"journals",
journals_to_value(calendar.journals, tag.clone()),
);
row.insert_untagged(
"free-busys",
free_busys_to_value(calendar.free_busys, tag.clone()),
);
row.insert_untagged("timezones", timezones_to_value(calendar.timezones, tag));
row.into_value()
}
fn events_to_value(events: Vec<IcalEvent>, tag: Tag) -> UntaggedValue {
UntaggedValue::table(
&events
.into_iter()
.map(|event| {
let mut row = TaggedDictBuilder::new(tag.clone());
row.insert_untagged(
"properties",
properties_to_value(event.properties, tag.clone()),
);
row.insert_untagged("alarms", alarms_to_value(event.alarms, tag.clone()));
row.into_value()
})
.collect::<Vec<Value>>(),
)
}
fn alarms_to_value(alarms: Vec<IcalAlarm>, tag: Tag) -> UntaggedValue {
UntaggedValue::table(
&alarms
.into_iter()
.map(|alarm| {
let mut row = TaggedDictBuilder::new(tag.clone());
row.insert_untagged(
"properties",
properties_to_value(alarm.properties, tag.clone()),
);
row.into_value()
})
.collect::<Vec<Value>>(),
)
}
fn todos_to_value(todos: Vec<IcalTodo>, tag: Tag) -> UntaggedValue {
UntaggedValue::table(
&todos
.into_iter()
.map(|todo| {
let mut row = TaggedDictBuilder::new(tag.clone());
row.insert_untagged(
"properties",
properties_to_value(todo.properties, tag.clone()),
);
row.insert_untagged("alarms", alarms_to_value(todo.alarms, tag.clone()));
row.into_value()
})
.collect::<Vec<Value>>(),
)
}
fn journals_to_value(journals: Vec<IcalJournal>, tag: Tag) -> UntaggedValue {
UntaggedValue::table(
&journals
.into_iter()
.map(|journal| {
let mut row = TaggedDictBuilder::new(tag.clone());
row.insert_untagged(
"properties",
properties_to_value(journal.properties, tag.clone()),
);
row.into_value()
})
.collect::<Vec<Value>>(),
)
}
fn free_busys_to_value(free_busys: Vec<IcalFreeBusy>, tag: Tag) -> UntaggedValue {
UntaggedValue::table(
&free_busys
.into_iter()
.map(|free_busy| {
let mut row = TaggedDictBuilder::new(tag.clone());
row.insert_untagged(
"properties",
properties_to_value(free_busy.properties, tag.clone()),
);
row.into_value()
})
.collect::<Vec<Value>>(),
)
}
fn timezones_to_value(timezones: Vec<IcalTimeZone>, tag: Tag) -> UntaggedValue {
UntaggedValue::table(
&timezones
.into_iter()
.map(|timezone| {
let mut row = TaggedDictBuilder::new(tag.clone());
row.insert_untagged(
"properties",
properties_to_value(timezone.properties, tag.clone()),
);
row.insert_untagged(
"transitions",
timezone_transitions_to_value(timezone.transitions, tag.clone()),
);
row.into_value()
})
.collect::<Vec<Value>>(),
)
}
fn timezone_transitions_to_value(
transitions: Vec<IcalTimeZoneTransition>,
tag: Tag,
) -> UntaggedValue {
UntaggedValue::table(
&transitions
.into_iter()
.map(|transition| {
let mut row = TaggedDictBuilder::new(tag.clone());
row.insert_untagged(
"properties",
properties_to_value(transition.properties, tag.clone()),
);
row.into_value()
})
.collect::<Vec<Value>>(),
)
}
fn properties_to_value(properties: Vec<Property>, tag: Tag) -> UntaggedValue {
UntaggedValue::table(
&properties
.into_iter()
.map(|prop| {
let mut row = TaggedDictBuilder::new(tag.clone());
let name = UntaggedValue::string(prop.name);
let value = match prop.value {
Some(val) => UntaggedValue::string(val),
None => UntaggedValue::Primitive(Primitive::Nothing),
};
let params = match prop.params {
Some(param_list) => params_to_value(param_list, tag.clone()).into(),
None => UntaggedValue::Primitive(Primitive::Nothing),
};
row.insert_untagged("name", name);
row.insert_untagged("value", value);
row.insert_untagged("params", params);
row.into_value()
})
.collect::<Vec<Value>>(),
)
}
fn params_to_value(params: Vec<(String, Vec<String>)>, tag: Tag) -> Value {
let mut row = TaggedDictBuilder::new(tag);
for (param_name, param_values) in params {
let values: Vec<Value> = param_values.into_iter().map(|val| val.into()).collect();
let values = UntaggedValue::table(&values);
row.insert_untagged(param_name, values);
}
row.into_value()
}
#[cfg(test)]
mod tests {
use super::FromIcs;
use super::ShellError;
#[test]
fn examples_work_as_expected() -> Result<(), ShellError> {
use crate::examples::test as test_examples;
test_examples(FromIcs {})
}
}

View File

@ -0,0 +1,96 @@
use crate::prelude::*;
use nu_engine::WholeStreamCommand;
use nu_errors::ShellError;
use nu_protocol::{Primitive, Signature, TaggedDictBuilder, UntaggedValue, Value};
use std::collections::HashMap;
pub struct FromIni;
impl WholeStreamCommand for FromIni {
fn name(&self) -> &str {
"from ini"
}
fn signature(&self) -> Signature {
Signature::build("from ini")
}
fn usage(&self) -> &str {
"Parse text as .ini and create table"
}
fn run(&self, args: CommandArgs) -> Result<OutputStream, ShellError> {
from_ini(args)
}
}
fn convert_ini_second_to_nu_value(v: &HashMap<String, String>, tag: impl Into<Tag>) -> Value {
let mut second = TaggedDictBuilder::new(tag);
for (key, value) in v.iter() {
second.insert_untagged(key.clone(), Primitive::String(value.clone()));
}
second.into_value()
}
fn convert_ini_top_to_nu_value(
v: &HashMap<String, HashMap<String, String>>,
tag: impl Into<Tag>,
) -> Value {
let tag = tag.into();
let mut top_level = TaggedDictBuilder::new(tag.clone());
for (key, value) in v.iter() {
top_level.insert_value(
key.clone(),
convert_ini_second_to_nu_value(value, tag.clone()),
);
}
top_level.into_value()
}
pub fn from_ini_string_to_value(
s: String,
tag: impl Into<Tag>,
) -> Result<Value, serde_ini::de::Error> {
let v: HashMap<String, HashMap<String, String>> = serde_ini::from_str(&s)?;
Ok(convert_ini_top_to_nu_value(&v, tag))
}
fn from_ini(args: CommandArgs) -> Result<OutputStream, ShellError> {
let tag = args.name_tag();
let input = args.input;
let concat_string = input.collect_string(tag.clone())?;
match from_ini_string_to_value(concat_string.item, tag.clone()) {
Ok(x) => match x {
Value {
value: UntaggedValue::Table(list),
..
} => Ok(list.into_iter().into_output_stream()),
x => Ok(OutputStream::one(x)),
},
Err(_) => Err(ShellError::labeled_error_with_secondary(
"Could not parse as INI",
"input cannot be parsed as INI",
&tag,
"value originates from here",
concat_string.tag,
)),
}
}
#[cfg(test)]
mod tests {
use super::FromIni;
use super::ShellError;
#[test]
fn examples_work_as_expected() -> Result<(), ShellError> {
use crate::examples::test as test_examples;
test_examples(FromIni {})
}
}

View File

@ -0,0 +1,140 @@
use crate::prelude::*;
use nu_engine::WholeStreamCommand;
use nu_errors::ShellError;
use nu_protocol::{Primitive, Signature, TaggedDictBuilder, UntaggedValue, Value};
pub struct FromJson;
impl WholeStreamCommand for FromJson {
fn name(&self) -> &str {
"from json"
}
fn signature(&self) -> Signature {
Signature::build("from json").switch(
"objects",
"treat each line as a separate value",
Some('o'),
)
}
fn usage(&self) -> &str {
"Parse text as .json and create table."
}
fn run(&self, args: CommandArgs) -> Result<OutputStream, ShellError> {
from_json(args)
}
}
fn convert_json_value_to_nu_value(v: &nu_json::Value, tag: impl Into<Tag>) -> Value {
let tag = tag.into();
let span = tag.span;
match v {
nu_json::Value::Null => UntaggedValue::Primitive(Primitive::Nothing).into_value(&tag),
nu_json::Value::Bool(b) => UntaggedValue::boolean(*b).into_value(&tag),
nu_json::Value::F64(n) => UntaggedValue::decimal_from_float(*n, span).into_value(&tag),
nu_json::Value::U64(n) => UntaggedValue::big_int(*n).into_value(&tag),
nu_json::Value::I64(n) => UntaggedValue::int(*n).into_value(&tag),
nu_json::Value::String(s) => {
UntaggedValue::Primitive(Primitive::String(String::from(s))).into_value(&tag)
}
nu_json::Value::Array(a) => UntaggedValue::Table(
a.iter()
.map(|x| convert_json_value_to_nu_value(x, &tag))
.collect(),
)
.into_value(tag),
nu_json::Value::Object(o) => {
let mut collected = TaggedDictBuilder::new(&tag);
for (k, v) in o.iter() {
collected.insert_value(k.clone(), convert_json_value_to_nu_value(v, &tag));
}
collected.into_value()
}
}
}
pub fn from_json_string_to_value(s: String, tag: impl Into<Tag>) -> nu_json::Result<Value> {
let v: nu_json::Value = nu_json::from_str(&s)?;
Ok(convert_json_value_to_nu_value(&v, tag))
}
fn from_json(args: CommandArgs) -> Result<OutputStream, ShellError> {
let name_tag = args.call_info.name_tag.clone();
let objects = args.has_flag("objects");
let concat_string = args.input.collect_string(name_tag.clone())?;
let string_clone: Vec<_> = concat_string.item.lines().map(|x| x.to_string()).collect();
if objects {
Ok(string_clone
.into_iter()
.filter_map(move |json_str| {
if json_str.is_empty() {
return None;
}
match from_json_string_to_value(json_str, &name_tag) {
Ok(x) => Some(x),
Err(e) => {
let mut message = "Could not parse as JSON (".to_string();
message.push_str(&e.to_string());
message.push(')');
Some(Value::error(ShellError::labeled_error_with_secondary(
message,
"input cannot be parsed as JSON",
name_tag.clone(),
"value originates from here",
concat_string.tag.clone(),
)))
}
}
})
.into_output_stream())
} else {
match from_json_string_to_value(concat_string.item, name_tag.clone()) {
Ok(x) => match x {
Value {
value: UntaggedValue::Table(list),
..
} => Ok(list.into_iter().into_output_stream()),
x => Ok(OutputStream::one(x)),
},
Err(e) => {
let mut message = "Could not parse as JSON (".to_string();
message.push_str(&e.to_string());
message.push(')');
Ok(OutputStream::one(Value::error(
ShellError::labeled_error_with_secondary(
message,
"input cannot be parsed as JSON",
name_tag,
"value originates from here",
concat_string.tag,
),
)))
}
}
}
}
#[cfg(test)]
mod tests {
use super::FromJson;
use super::ShellError;
#[test]
fn examples_work_as_expected() -> Result<(), ShellError> {
use crate::examples::test as test_examples;
test_examples(FromJson {})
}
}

View File

@ -0,0 +1,32 @@
mod command;
pub(crate) mod csv;
mod delimited;
mod eml;
mod ics;
mod ini;
mod json;
mod ods;
mod ssv;
pub(crate) mod toml;
mod tsv;
pub(crate) mod url;
mod vcf;
mod xlsx;
mod xml;
mod yaml;
pub use self::csv::FromCsv;
pub use self::toml::FromToml;
pub use self::url::FromUrl;
pub use command::From;
pub use eml::FromEml;
pub use ics::FromIcs;
pub use ini::FromIni;
pub use json::FromJson;
pub use ods::FromOds;
pub use ssv::FromSsv;
pub use tsv::FromTsv;
pub use vcf::FromVcf;
pub use xlsx::FromXlsx;
pub use xml::FromXml;
pub use yaml::{FromYaml, FromYml};

View File

@ -0,0 +1,120 @@
use crate::prelude::*;
use calamine::*;
use nu_data::TaggedListBuilder;
use nu_engine::WholeStreamCommand;
use nu_errors::ShellError;
use nu_protocol::{Primitive, Signature, SyntaxShape, TaggedDictBuilder, UntaggedValue, Value};
use std::io::Cursor;
pub struct FromOds;
impl WholeStreamCommand for FromOds {
fn name(&self) -> &str {
"from ods"
}
fn signature(&self) -> Signature {
Signature::build("from ods").named(
"sheets",
SyntaxShape::Table,
"Only convert specified sheets",
Some('s'),
)
}
fn usage(&self) -> &str {
"Parse OpenDocument Spreadsheet(.ods) data and create table."
}
fn run(&self, args: CommandArgs) -> Result<OutputStream, ShellError> {
from_ods(args)
}
}
// Adapted from crates/nu-command/src/commands/dataframe/utils.rs
fn convert_columns(columns: &[Value]) -> Result<Vec<String>, ShellError> {
let res = columns
.iter()
.map(|value| match &value.value {
UntaggedValue::Primitive(Primitive::String(s)) => Ok(s.clone()),
_ => Err(ShellError::labeled_error(
"Incorrect column format",
"Only string as column name",
&value.tag,
)),
})
.collect::<Result<Vec<String>, _>>()?;
Ok(res)
}
fn from_ods(args: CommandArgs) -> Result<OutputStream, ShellError> {
let tag = args.call_info.name_tag.clone();
let span = tag.span;
let mut sel_sheets = vec![];
if let Some(columns) = args.get_flag::<Vec<Value>>("sheets")? {
sel_sheets = convert_columns(columns.as_slice())?;
}
let bytes = args.input.collect_binary(tag.clone())?;
let buf: Cursor<Vec<u8>> = Cursor::new(bytes.item);
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 mut sheet_names = ods.sheet_names().to_owned();
if !sel_sheets.is_empty() {
sheet_names.retain(|e| sel_sheets.contains(e));
}
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_from_float(*f, span),
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 {
return Err(ShellError::labeled_error(
"Could not load sheet",
"could not load sheet",
&tag,
));
}
}
Ok(OutputStream::one(dict.into_value()))
}
#[cfg(test)]
mod tests {
use super::FromOds;
use super::ShellError;
#[test]
fn examples_work_as_expected() -> Result<(), ShellError> {
use crate::examples::test as test_examples;
test_examples(FromOds {})
}
}

View File

@ -0,0 +1,480 @@
use crate::prelude::*;
use nu_engine::WholeStreamCommand;
use nu_errors::ShellError;
use nu_protocol::{Primitive, Signature, SyntaxShape, TaggedDictBuilder, UntaggedValue, Value};
use nu_source::Tagged;
pub struct FromSsv;
const STRING_REPRESENTATION: &str = "from ssv";
const DEFAULT_MINIMUM_SPACES: usize = 2;
impl WholeStreamCommand for FromSsv {
fn name(&self) -> &str {
STRING_REPRESENTATION
}
fn signature(&self) -> Signature {
Signature::build(STRING_REPRESENTATION)
.switch(
"noheaders",
"don't treat the first row as column names",
Some('n'),
)
.switch("aligned-columns", "assume columns are aligned", Some('a'))
.named(
"minimum-spaces",
SyntaxShape::Int,
"the minimum spaces to separate columns",
Some('m'),
)
}
fn usage(&self) -> &str {
"Parse text as space-separated values and create a table. The default minimum number of spaces counted as a separator is 2."
}
fn run(&self, args: CommandArgs) -> Result<OutputStream, ShellError> {
from_ssv(args)
}
}
enum HeaderOptions<'a> {
WithHeaders(&'a str),
WithoutHeaders,
}
fn parse_aligned_columns<'a>(
lines: impl Iterator<Item = &'a str>,
headers: HeaderOptions,
separator: &str,
) -> Vec<Vec<(String, String)>> {
fn construct<'a>(
lines: impl Iterator<Item = &'a str>,
headers: Vec<(String, usize)>,
) -> Vec<Vec<(String, String)>> {
lines
.map(|l| {
headers
.iter()
.enumerate()
.map(|(i, (header_name, start_position))| {
let val = match headers.get(i + 1) {
Some((_, end)) => {
if *end < l.len() {
l.get(*start_position..*end)
} else {
l.get(*start_position..)
}
}
None => l.get(*start_position..),
}
.unwrap_or("")
.trim()
.into();
(header_name.clone(), val)
})
.collect()
})
.collect()
}
let find_indices = |line: &str| {
let values = line
.split(&separator)
.map(str::trim)
.filter(|s| !s.is_empty());
values
.fold(
(0, vec![]),
|(current_pos, mut indices), value| match line[current_pos..].find(value) {
None => (current_pos, indices),
Some(index) => {
let absolute_index = current_pos + index;
indices.push(absolute_index);
(absolute_index + value.len(), indices)
}
},
)
.1
};
let parse_with_headers = |lines, headers_raw: &str| {
let indices = find_indices(headers_raw);
let headers = headers_raw
.split(&separator)
.map(str::trim)
.filter(|s| !s.is_empty())
.map(String::from)
.zip(indices);
let columns = headers.collect::<Vec<(String, usize)>>();
construct(lines, columns)
};
let parse_without_headers = |ls: Vec<&str>| {
let mut indices = ls
.iter()
.flat_map(|s| find_indices(*s))
.collect::<Vec<usize>>();
indices.sort_unstable();
indices.dedup();
let headers: Vec<(String, usize)> = indices
.iter()
.enumerate()
.map(|(i, position)| (format!("Column{}", i + 1), *position))
.collect();
construct(ls.iter().map(|s| s.to_owned()), headers)
};
match headers {
HeaderOptions::WithHeaders(headers_raw) => parse_with_headers(lines, headers_raw),
HeaderOptions::WithoutHeaders => parse_without_headers(lines.collect()),
}
}
fn parse_separated_columns<'a>(
lines: impl Iterator<Item = &'a str>,
headers: HeaderOptions,
separator: &str,
) -> Vec<Vec<(String, String)>> {
fn collect<'a>(
headers: Vec<String>,
rows: impl Iterator<Item = &'a str>,
separator: &str,
) -> Vec<Vec<(String, String)>> {
rows.map(|r| {
headers
.iter()
.zip(r.split(separator).map(str::trim).filter(|s| !s.is_empty()))
.map(|(a, b)| (a.to_owned(), b.to_owned()))
.collect()
})
.collect()
}
let parse_with_headers = |lines, headers_raw: &str| {
let headers = headers_raw
.split(&separator)
.map(str::trim)
.map(str::to_owned)
.filter(|s| !s.is_empty())
.collect();
collect(headers, lines, separator)
};
let parse_without_headers = |ls: Vec<&str>| {
let num_columns = ls.iter().map(|r| r.len()).max().unwrap_or(0);
let headers = (1..=num_columns)
.map(|i| format!("Column{}", i))
.collect::<Vec<String>>();
collect(headers, ls.into_iter(), separator)
};
match headers {
HeaderOptions::WithHeaders(headers_raw) => parse_with_headers(lines, headers_raw),
HeaderOptions::WithoutHeaders => parse_without_headers(lines.collect()),
}
}
fn string_to_table(
s: &str,
noheaders: bool,
aligned_columns: bool,
split_at: usize,
) -> Vec<Vec<(String, String)>> {
let mut lines = s.lines().filter(|l| !l.trim().is_empty());
let separator = " ".repeat(std::cmp::max(split_at, 1));
let (ls, header_options) = if noheaders {
(lines, HeaderOptions::WithoutHeaders)
} else {
match lines.next() {
Some(header) => (lines, HeaderOptions::WithHeaders(header)),
None => return vec![],
}
};
let f = if aligned_columns {
parse_aligned_columns
} else {
parse_separated_columns
};
f(ls, header_options, &separator)
}
fn from_ssv_string_to_value(
s: &str,
noheaders: bool,
aligned_columns: bool,
split_at: usize,
tag: impl Into<Tag>,
) -> Value {
let tag = tag.into();
let rows = string_to_table(s, noheaders, aligned_columns, split_at)
.iter()
.map(|row| {
let mut tagged_dict = TaggedDictBuilder::new(&tag);
for (col, entry) in row {
tagged_dict.insert_value(
col,
UntaggedValue::Primitive(Primitive::String(String::from(entry)))
.into_value(&tag),
)
}
tagged_dict.into_value()
})
.collect();
UntaggedValue::Table(rows).into_value(&tag)
}
fn from_ssv(args: CommandArgs) -> Result<OutputStream, ShellError> {
let name = args.call_info.name_tag.clone();
let noheaders = args.has_flag("noheaders");
let aligned_columns = args.has_flag("aligned-columns");
let minimum_spaces: Option<Tagged<usize>> = args.get_flag("minimum-spaces")?;
let concat_string = args.input.collect_string(name.clone())?;
let split_at = match minimum_spaces {
Some(number) => number.item,
None => DEFAULT_MINIMUM_SPACES,
};
Ok(
match from_ssv_string_to_value(
&concat_string.item,
noheaders,
aligned_columns,
split_at,
name,
) {
Value {
value: UntaggedValue::Table(list),
..
} => list.into_iter().into_output_stream(),
x => OutputStream::one(x),
},
)
}
#[cfg(test)]
mod tests {
use super::ShellError;
use super::*;
fn owned(x: &str, y: &str) -> (String, String) {
(String::from(x), String::from(y))
}
#[test]
fn it_trims_empty_and_whitespace_only_lines() {
let input = r#"
a b
1 2
3 4
"#;
let result = string_to_table(input, false, true, 1);
assert_eq!(
result,
vec![
vec![owned("a", "1"), owned("b", "2")],
vec![owned("a", "3"), owned("b", "4")]
]
);
}
#[test]
fn it_deals_with_single_column_input() {
let input = r#"
a
1
2
"#;
let result = string_to_table(input, false, true, 1);
assert_eq!(result, vec![vec![owned("a", "1")], vec![owned("a", "2")]]);
}
#[test]
fn it_uses_first_row_as_data_when_noheaders() {
let input = r#"
a b
1 2
3 4
"#;
let result = string_to_table(input, true, true, 1);
assert_eq!(
result,
vec![
vec![owned("Column1", "a"), owned("Column2", "b")],
vec![owned("Column1", "1"), owned("Column2", "2")],
vec![owned("Column1", "3"), owned("Column2", "4")]
]
);
}
#[test]
fn it_allows_a_predefined_number_of_spaces() {
let input = r#"
column a column b
entry 1 entry number 2
3 four
"#;
let result = string_to_table(input, false, true, 3);
assert_eq!(
result,
vec![
vec![
owned("column a", "entry 1"),
owned("column b", "entry number 2")
],
vec![owned("column a", "3"), owned("column b", "four")]
]
);
}
#[test]
fn it_trims_remaining_separator_space() {
let input = r#"
colA colB colC
val1 val2 val3
"#;
let trimmed = |s: &str| s.trim() == s;
let result = string_to_table(input, false, true, 2);
assert!(result
.iter()
.all(|row| row.iter().all(|(a, b)| trimmed(a) && trimmed(b))));
}
#[test]
fn it_keeps_empty_columns() {
let input = r#"
colA col B col C
val2 val3
val4 val 5 val 6
val7 val8
"#;
let result = string_to_table(input, false, true, 2);
assert_eq!(
result,
vec![
vec![
owned("colA", ""),
owned("col B", "val2"),
owned("col C", "val3")
],
vec![
owned("colA", "val4"),
owned("col B", "val 5"),
owned("col C", "val 6")
],
vec![
owned("colA", "val7"),
owned("col B", ""),
owned("col C", "val8")
],
]
);
}
#[test]
fn it_can_produce_an_empty_stream_for_header_only_input() {
let input = "colA col B";
let result = string_to_table(input, false, true, 2);
let expected: Vec<Vec<(String, String)>> = vec![];
assert_eq!(expected, result);
}
#[test]
fn it_uses_the_full_final_column() {
let input = r#"
colA col B
val1 val2 trailing value that should be included
"#;
let result = string_to_table(input, false, true, 2);
assert_eq!(
result,
vec![vec![
owned("colA", "val1"),
owned("col B", "val2 trailing value that should be included"),
]]
);
}
#[test]
fn it_handles_empty_values_when_noheaders_and_aligned_columns() {
let input = r#"
a multi-word value b d
1 3-3 4
last
"#;
let result = string_to_table(input, true, true, 2);
assert_eq!(
result,
vec![
vec![
owned("Column1", "a multi-word value"),
owned("Column2", "b"),
owned("Column3", ""),
owned("Column4", "d"),
owned("Column5", "")
],
vec![
owned("Column1", "1"),
owned("Column2", ""),
owned("Column3", "3-3"),
owned("Column4", "4"),
owned("Column5", "")
],
vec![
owned("Column1", ""),
owned("Column2", ""),
owned("Column3", ""),
owned("Column4", ""),
owned("Column5", "last")
],
]
);
}
#[test]
fn input_is_parsed_correctly_if_either_option_works() {
let input = r#"
docker-registry docker-registry=default docker-registry=default 172.30.78.158 5000/TCP
kubernetes component=apiserver,provider=kubernetes <none> 172.30.0.2 443/TCP
kubernetes-ro component=apiserver,provider=kubernetes <none> 172.30.0.1 80/TCP
"#;
let aligned_columns_noheaders = string_to_table(input, true, true, 2);
let separator_noheaders = string_to_table(input, true, false, 2);
let aligned_columns_with_headers = string_to_table(input, false, true, 2);
let separator_with_headers = string_to_table(input, false, false, 2);
assert_eq!(aligned_columns_noheaders, separator_noheaders);
assert_eq!(aligned_columns_with_headers, separator_with_headers);
}
#[test]
fn examples_work_as_expected() -> Result<(), ShellError> {
use super::FromSsv;
use crate::examples::test as test_examples;
test_examples(FromSsv {})
}
}

View File

@ -0,0 +1,101 @@
use crate::prelude::*;
use nu_engine::WholeStreamCommand;
use nu_errors::ShellError;
use nu_protocol::{Primitive, Signature, TaggedDictBuilder, UntaggedValue, Value};
pub struct FromToml;
impl WholeStreamCommand for FromToml {
fn name(&self) -> &str {
"from toml"
}
fn signature(&self) -> Signature {
Signature::build("from toml")
}
fn usage(&self) -> &str {
"Parse text as .toml and create table."
}
fn run(&self, args: CommandArgs) -> Result<OutputStream, ShellError> {
from_toml(args)
}
}
pub fn convert_toml_value_to_nu_value(v: &toml::Value, tag: impl Into<Tag>) -> Value {
let tag = tag.into();
let span = tag.span;
match v {
toml::Value::Boolean(b) => UntaggedValue::boolean(*b).into_value(tag),
toml::Value::Integer(n) => UntaggedValue::int(*n).into_value(tag),
toml::Value::Float(n) => UntaggedValue::decimal_from_float(*n, span).into_value(tag),
toml::Value::String(s) => {
UntaggedValue::Primitive(Primitive::String(String::from(s))).into_value(tag)
}
toml::Value::Array(a) => UntaggedValue::Table(
a.iter()
.map(|x| convert_toml_value_to_nu_value(x, &tag))
.collect(),
)
.into_value(tag),
toml::Value::Datetime(dt) => {
UntaggedValue::Primitive(Primitive::String(dt.to_string())).into_value(tag)
}
toml::Value::Table(t) => {
let mut collected = TaggedDictBuilder::new(&tag);
for (k, v) in t.iter() {
collected.insert_value(k.clone(), convert_toml_value_to_nu_value(v, &tag));
}
collected.into_value()
}
}
}
pub fn from_toml_string_to_value(s: String, tag: impl Into<Tag>) -> Result<Value, toml::de::Error> {
let v: toml::Value = s.parse::<toml::Value>()?;
Ok(convert_toml_value_to_nu_value(&v, tag))
}
pub fn from_toml(args: CommandArgs) -> Result<OutputStream, ShellError> {
let tag = args.name_tag();
let input = args.input;
let concat_string = input.collect_string(tag.clone())?;
Ok(
match from_toml_string_to_value(concat_string.item, tag.clone()) {
Ok(x) => match x {
Value {
value: UntaggedValue::Table(list),
..
} => list.into_iter().into_output_stream(),
x => OutputStream::one(x),
},
Err(_) => {
return Err(ShellError::labeled_error_with_secondary(
"Could not parse as TOML",
"input cannot be parsed as TOML",
&tag,
"value originates from here",
concat_string.tag,
))
}
},
)
}
#[cfg(test)]
mod tests {
use super::FromToml;
use super::ShellError;
#[test]
fn examples_work_as_expected() -> Result<(), ShellError> {
use crate::examples::test as test_examples;
test_examples(FromToml {})
}
}

View File

@ -0,0 +1,50 @@
use super::delimited::from_delimited_data;
use crate::prelude::*;
use nu_engine::WholeStreamCommand;
use nu_errors::ShellError;
use nu_protocol::Signature;
pub struct FromTsv;
impl WholeStreamCommand for FromTsv {
fn name(&self) -> &str {
"from tsv"
}
fn signature(&self) -> Signature {
Signature::build("from tsv").switch(
"noheaders",
"don't treat the first row as column names",
Some('n'),
)
}
fn usage(&self) -> &str {
"Parse text as .tsv and create table."
}
fn run(&self, args: CommandArgs) -> Result<OutputStream, ShellError> {
from_tsv(args)
}
}
fn from_tsv(args: CommandArgs) -> Result<OutputStream, ShellError> {
let name = args.call_info.name_tag.clone();
let noheaders = args.has_flag("noheaders");
let input = args.input;
from_delimited_data(noheaders, '\t', "TSV", input, name)
}
#[cfg(test)]
mod tests {
use super::FromTsv;
use super::ShellError;
#[test]
fn examples_work_as_expected() -> Result<(), ShellError> {
use crate::examples::test as test_examples;
test_examples(FromTsv {})
}
}

View File

@ -0,0 +1,65 @@
use crate::prelude::*;
use nu_engine::WholeStreamCommand;
use nu_errors::ShellError;
use nu_protocol::{Signature, TaggedDictBuilder, UntaggedValue};
pub struct FromUrl;
impl WholeStreamCommand for FromUrl {
fn name(&self) -> &str {
"from url"
}
fn signature(&self) -> Signature {
Signature::build("from url")
}
fn usage(&self) -> &str {
"Parse url-encoded string as a table."
}
fn run(&self, args: CommandArgs) -> Result<OutputStream, ShellError> {
from_url(args)
}
}
fn from_url(args: CommandArgs) -> Result<OutputStream, ShellError> {
let tag = args.name_tag();
let input = args.input;
let concat_string = input.collect_string(tag.clone())?;
let result = serde_urlencoded::from_str::<Vec<(String, String)>>(&concat_string.item);
match result {
Ok(result) => {
let mut row = TaggedDictBuilder::new(tag);
for (k, v) in result {
row.insert_untagged(k, UntaggedValue::string(v));
}
Ok(OutputStream::one(row.into_value()))
}
_ => Err(ShellError::labeled_error_with_secondary(
"String not compatible with url-encoding",
"input not url-encoded",
tag,
"value originates from here",
concat_string.tag,
)),
}
}
#[cfg(test)]
mod tests {
use super::FromUrl;
use super::ShellError;
#[test]
fn examples_work_as_expected() -> Result<(), ShellError> {
use crate::examples::test as test_examples;
test_examples(FromUrl {})
}
}

View File

@ -0,0 +1,106 @@
use crate::prelude::*;
use ical::parser::vcard::component::*;
use ical::property::Property;
use nu_engine::WholeStreamCommand;
use nu_errors::ShellError;
use nu_protocol::{Primitive, Signature, TaggedDictBuilder, UntaggedValue, Value};
pub struct FromVcf;
impl WholeStreamCommand for FromVcf {
fn name(&self) -> &str {
"from vcf"
}
fn signature(&self) -> Signature {
Signature::build("from vcf")
}
fn usage(&self) -> &str {
"Parse text as .vcf and create table."
}
fn run(&self, args: CommandArgs) -> Result<OutputStream, ShellError> {
from_vcf(args)
}
}
fn from_vcf(args: CommandArgs) -> Result<OutputStream, ShellError> {
let tag = args.name_tag();
let input = args.input;
let input_string = input.collect_string(tag.clone())?.item;
let input_bytes = input_string.into_bytes();
let cursor = std::io::Cursor::new(input_bytes);
let parser = ical::VcardParser::new(cursor);
let iter = parser.map(move |contact| match contact {
Ok(c) => contact_to_value(c, tag.clone()),
Err(_) => Value::error(ShellError::labeled_error(
"Could not parse as .vcf",
"input cannot be parsed as .vcf",
tag.clone(),
)),
});
let collected: Vec<_> = iter.collect();
Ok(collected.into_iter().into_output_stream())
}
fn contact_to_value(contact: VcardContact, tag: Tag) -> Value {
let mut row = TaggedDictBuilder::new(tag.clone());
row.insert_untagged("properties", properties_to_value(contact.properties, tag));
row.into_value()
}
fn properties_to_value(properties: Vec<Property>, tag: Tag) -> UntaggedValue {
UntaggedValue::table(
&properties
.into_iter()
.map(|prop| {
let mut row = TaggedDictBuilder::new(tag.clone());
let name = UntaggedValue::string(prop.name);
let value = match prop.value {
Some(val) => UntaggedValue::string(val),
None => UntaggedValue::Primitive(Primitive::Nothing),
};
let params = match prop.params {
Some(param_list) => params_to_value(param_list, tag.clone()).into(),
None => UntaggedValue::Primitive(Primitive::Nothing),
};
row.insert_untagged("name", name);
row.insert_untagged("value", value);
row.insert_untagged("params", params);
row.into_value()
})
.collect::<Vec<Value>>(),
)
}
fn params_to_value(params: Vec<(String, Vec<String>)>, tag: Tag) -> Value {
let mut row = TaggedDictBuilder::new(tag);
for (param_name, param_values) in params {
let values: Vec<Value> = param_values.into_iter().map(|val| val.into()).collect();
let values = UntaggedValue::table(&values);
row.insert_untagged(param_name, values);
}
row.into_value()
}
#[cfg(test)]
mod tests {
use super::FromVcf;
use super::ShellError;
#[test]
fn examples_work_as_expected() -> Result<(), ShellError> {
use crate::examples::test as test_examples;
test_examples(FromVcf {})
}
}

View File

@ -0,0 +1,127 @@
use crate::prelude::*;
use calamine::*;
use nu_data::TaggedListBuilder;
use nu_engine::WholeStreamCommand;
use nu_errors::ShellError;
use nu_protocol::{Primitive, Signature, SyntaxShape, TaggedDictBuilder, UntaggedValue, Value};
use std::io::Cursor;
pub struct FromXlsx;
impl WholeStreamCommand for FromXlsx {
fn name(&self) -> &str {
"from xlsx"
}
fn signature(&self) -> Signature {
Signature::build("from xlsx")
.switch(
"noheaders",
"don't treat the first row as column names",
Some('n'),
)
.named(
"sheets",
SyntaxShape::Table,
"Only convert specified sheets",
Some('s'),
)
}
fn usage(&self) -> &str {
"Parse binary Excel(.xlsx) data and create table."
}
fn run(&self, args: CommandArgs) -> Result<OutputStream, ShellError> {
from_xlsx(args)
}
}
// Adapted from crates/nu-command/src/commands/dataframe/utils.rs
fn convert_columns(columns: &[Value]) -> Result<Vec<String>, ShellError> {
let res = columns
.iter()
.map(|value| match &value.value {
UntaggedValue::Primitive(Primitive::String(s)) => Ok(s.clone()),
_ => Err(ShellError::labeled_error(
"Incorrect column format",
"Only string as column name",
&value.tag,
)),
})
.collect::<Result<Vec<String>, _>>()?;
Ok(res)
}
fn from_xlsx(args: CommandArgs) -> Result<OutputStream, ShellError> {
let tag = args.call_info.name_tag.clone();
let span = tag.span;
let mut sel_sheets = vec![];
if let Some(columns) = args.get_flag::<Vec<Value>>("sheets")? {
sel_sheets = convert_columns(columns.as_slice())?;
}
let value = args.input.collect_binary(tag.clone())?;
let buf: Cursor<Vec<u8>> = Cursor::new(value.item);
let mut xls = Xlsx::<_>::new(buf).map_err(|_| {
ShellError::labeled_error("Could not load xlsx file", "could not load xlsx file", &tag)
})?;
let mut dict = TaggedDictBuilder::new(&tag);
let mut sheet_names = xls.sheet_names().to_owned();
if !sel_sheets.is_empty() {
sheet_names.retain(|e| sel_sheets.contains(e));
}
for sheet_name in &sheet_names {
let mut sheet_output = TaggedListBuilder::new(&tag);
if let Some(Ok(current_sheet)) = xls.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_from_float(*f, span),
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 {
return Err(ShellError::labeled_error(
"Could not load sheet",
"could not load sheet",
&tag,
));
}
}
Ok(OutputStream::one(dict.into_value()))
}
#[cfg(test)]
mod tests {
use super::FromXlsx;
use super::ShellError;
#[test]
fn examples_work_as_expected() -> Result<(), ShellError> {
use crate::examples::test as test_examples;
test_examples(FromXlsx {})
}
}

View File

@ -0,0 +1,302 @@
use crate::prelude::*;
use nu_engine::WholeStreamCommand;
use nu_errors::ShellError;
use nu_protocol::{Primitive, Signature, TaggedDictBuilder, UntaggedValue, Value};
pub struct FromXml;
impl WholeStreamCommand for FromXml {
fn name(&self) -> &str {
"from xml"
}
fn signature(&self) -> Signature {
Signature::build("from xml")
}
fn usage(&self) -> &str {
"Parse text as .xml and create table."
}
fn run(&self, args: CommandArgs) -> Result<OutputStream, ShellError> {
from_xml(args)
}
}
fn from_attributes_to_value(attributes: &[roxmltree::Attribute], tag: impl Into<Tag>) -> Value {
let tag = tag.into();
let mut collected = TaggedDictBuilder::new(tag);
for a in attributes {
collected.insert_untagged(String::from(a.name()), UntaggedValue::string(a.value()));
}
collected.into_value()
}
fn from_node_to_value(n: &roxmltree::Node, tag: impl Into<Tag>) -> Value {
let tag = tag.into();
if n.is_element() {
let name = n.tag_name().name().trim().to_string();
let mut children_values = vec![];
for c in n.children() {
children_values.push(from_node_to_value(&c, &tag));
}
let children_values: Vec<Value> = children_values
.into_iter()
.filter(|x| match x {
Value {
value: UntaggedValue::Primitive(Primitive::String(f)),
..
} => {
!f.trim().is_empty() // non-whitespace characters?
}
_ => true,
})
.collect();
let mut collected = TaggedDictBuilder::new(&tag);
let attribute_value: Value = from_attributes_to_value(n.attributes(), &tag);
let mut row = TaggedDictBuilder::new(&tag);
row.insert_untagged(
String::from("children"),
UntaggedValue::Table(children_values),
);
row.insert_untagged(String::from("attributes"), attribute_value);
collected.insert_untagged(name, row.into_value());
collected.into_value()
} else if n.is_comment() {
UntaggedValue::string("<comment>").into_value(tag)
} else if n.is_pi() {
UntaggedValue::string("<processing_instruction>").into_value(tag)
} else if n.is_text() {
match n.text() {
Some(text) => UntaggedValue::string(text).into_value(tag),
None => UntaggedValue::string("<error>").into_value(tag),
}
} else {
UntaggedValue::string("<unknown>").into_value(tag)
}
}
fn from_document_to_value(d: &roxmltree::Document, tag: impl Into<Tag>) -> Value {
from_node_to_value(&d.root_element(), tag)
}
pub fn from_xml_string_to_value(s: String, tag: impl Into<Tag>) -> Result<Value, roxmltree::Error> {
let parsed = roxmltree::Document::parse(&s)?;
Ok(from_document_to_value(&parsed, tag))
}
fn from_xml(args: CommandArgs) -> Result<OutputStream, ShellError> {
let tag = args.name_tag();
let input = args.input;
let concat_string = input.collect_string(tag.clone())?;
Ok(
match from_xml_string_to_value(concat_string.item, tag.clone()) {
Ok(x) => match x {
Value {
value: UntaggedValue::Table(list),
..
} => list.into_iter().into_output_stream(),
x => OutputStream::one(x),
},
Err(_) => {
return Err(ShellError::labeled_error_with_secondary(
"Could not parse as XML",
"input cannot be parsed as XML",
&tag,
"value originates from here",
&concat_string.tag,
))
}
},
)
}
#[cfg(test)]
mod tests {
use super::*;
use indexmap::IndexMap;
use nu_protocol::{UntaggedValue, Value};
fn string(input: impl Into<String>) -> Value {
UntaggedValue::string(input.into()).into_untagged_value()
}
fn row(entries: IndexMap<String, Value>) -> Value {
UntaggedValue::row(entries).into_untagged_value()
}
fn table(list: &[Value]) -> Value {
UntaggedValue::table(list).into_untagged_value()
}
fn parse(xml: &str) -> Result<Value, roxmltree::Error> {
from_xml_string_to_value(xml.to_string(), Tag::unknown())
}
#[test]
fn parses_empty_element() -> Result<(), roxmltree::Error> {
let source = "<nu></nu>";
assert_eq!(
parse(source)?,
row(indexmap! {
"nu".into() => row(indexmap! {
"children".into() => table(&[]),
"attributes".into() => row(indexmap! {})
})
})
);
Ok(())
}
#[test]
fn parses_element_with_text() -> Result<(), roxmltree::Error> {
let source = "<nu>La era de los tres caballeros</nu>";
assert_eq!(
parse(source)?,
row(indexmap! {
"nu".into() => row(indexmap! {
"children".into() => table(&[string("La era de los tres caballeros")]),
"attributes".into() => row(indexmap! {})
})
})
);
Ok(())
}
#[test]
fn parses_element_with_elements() -> Result<(), roxmltree::Error> {
let source = "\
<nu>
<dev>Andrés</dev>
<dev>Jonathan</dev>
<dev>Yehuda</dev>
</nu>";
assert_eq!(
parse(source)?,
row(indexmap! {
"nu".into() => row(indexmap! {
"children".into() => table(&[
row(indexmap! {
"dev".into() => row(indexmap! {
"children".into() => table(&[string("Andrés")]),
"attributes".into() => row(indexmap! {})
})
}),
row(indexmap! {
"dev".into() => row(indexmap! {
"children".into() => table(&[string("Jonathan")]),
"attributes".into() => row(indexmap! {})
})
}),
row(indexmap! {
"dev".into() => row(indexmap! {
"children".into() => table(&[string("Yehuda")]),
"attributes".into() => row(indexmap! {})
})
})
]),
"attributes".into() => row(indexmap! {})
})
})
);
Ok(())
}
#[test]
fn parses_element_with_attribute() -> Result<(), roxmltree::Error> {
let source = "\
<nu version=\"2.0\">
</nu>";
assert_eq!(
parse(source)?,
row(indexmap! {
"nu".into() => row(indexmap! {
"children".into() => table(&[]),
"attributes".into() => row(indexmap! {
"version".into() => string("2.0")
})
})
})
);
Ok(())
}
#[test]
fn parses_element_with_attribute_and_element() -> Result<(), roxmltree::Error> {
let source = "\
<nu version=\"2.0\">
<version>2.0</version>
</nu>";
assert_eq!(
parse(source)?,
row(indexmap! {
"nu".into() => row(indexmap! {
"children".into() => table(&[
row(indexmap! {
"version".into() => row(indexmap! {
"children".into() => table(&[string("2.0")]),
"attributes".into() => row(indexmap! {})
})
})
]),
"attributes".into() => row(indexmap! {
"version".into() => string("2.0")
})
})
})
);
Ok(())
}
#[test]
fn parses_element_with_multiple_attributes() -> Result<(), roxmltree::Error> {
let source = "\
<nu version=\"2.0\" age=\"25\">
</nu>";
assert_eq!(
parse(source)?,
row(indexmap! {
"nu".into() => row(indexmap! {
"children".into() => table(&[]),
"attributes".into() => row(indexmap! {
"version".into() => string("2.0"),
"age".into() => string("25")
})
})
})
);
Ok(())
}
#[test]
fn examples_work_as_expected() -> Result<(), ShellError> {
use super::FromXml;
use crate::examples::test as test_examples;
test_examples(FromXml {})
}
}

View File

@ -0,0 +1,205 @@
use crate::prelude::*;
use nu_engine::WholeStreamCommand;
use nu_errors::ShellError;
use nu_protocol::{Primitive, Signature, TaggedDictBuilder, UntaggedValue, Value};
pub struct FromYaml;
impl WholeStreamCommand for FromYaml {
fn name(&self) -> &str {
"from yaml"
}
fn signature(&self) -> Signature {
Signature::build("from yaml")
}
fn usage(&self) -> &str {
"Parse text as .yaml/.yml and create table."
}
fn run(&self, args: CommandArgs) -> Result<OutputStream, ShellError> {
from_yaml(args)
}
}
pub struct FromYml;
impl WholeStreamCommand for FromYml {
fn name(&self) -> &str {
"from yml"
}
fn signature(&self) -> Signature {
Signature::build("from yml")
}
fn usage(&self) -> &str {
"Parse text as .yaml/.yml and create table."
}
fn run(&self, args: CommandArgs) -> Result<OutputStream, ShellError> {
from_yaml(args)
}
}
fn convert_yaml_value_to_nu_value(
v: &serde_yaml::Value,
tag: impl Into<Tag>,
) -> Result<Value, ShellError> {
let tag = tag.into();
let span = tag.span;
let err_not_compatible_number = ShellError::labeled_error(
"Expected a compatible number",
"expected a compatible number",
&tag,
);
Ok(match v {
serde_yaml::Value::Bool(b) => UntaggedValue::boolean(*b).into_value(tag),
serde_yaml::Value::Number(n) if n.is_i64() => {
UntaggedValue::int(n.as_i64().ok_or(err_not_compatible_number)?).into_value(tag)
}
serde_yaml::Value::Number(n) if n.is_f64() => {
UntaggedValue::decimal_from_float(n.as_f64().ok_or(err_not_compatible_number)?, span)
.into_value(tag)
}
serde_yaml::Value::String(s) => UntaggedValue::string(s).into_value(tag),
serde_yaml::Value::Sequence(a) => {
let result: Result<Vec<Value>, ShellError> = a
.iter()
.map(|x| convert_yaml_value_to_nu_value(x, &tag))
.collect();
UntaggedValue::Table(result?).into_value(tag)
}
serde_yaml::Value::Mapping(t) => {
let mut collected = TaggedDictBuilder::new(&tag);
for (k, v) in t.iter() {
// A ShellError that we re-use multiple times in the Mapping scenario
let err_unexpected_map = ShellError::labeled_error(
format!("Unexpected YAML:\nKey: {:?}\nValue: {:?}", k, v),
"unexpected",
tag.clone(),
);
match (k, v) {
(serde_yaml::Value::String(k), _) => {
collected.insert_value(k.clone(), convert_yaml_value_to_nu_value(v, &tag)?);
}
// Hard-code fix for cases where "v" is a string without quotations with double curly braces
// e.g. k = value
// value: {{ something }}
// Strangely, serde_yaml returns
// "value" -> Mapping(Mapping { map: {Mapping(Mapping { map: {String("something"): Null} }): Null} })
(serde_yaml::Value::Mapping(m), serde_yaml::Value::Null) => {
return m
.iter()
.take(1)
.collect_vec()
.first()
.and_then(|e| match e {
(serde_yaml::Value::String(s), serde_yaml::Value::Null) => Some(
UntaggedValue::string("{{ ".to_owned() + s + " }}")
.into_value(tag),
),
_ => None,
})
.ok_or(err_unexpected_map);
}
(_, _) => {
return Err(err_unexpected_map);
}
}
}
collected.into_value()
}
serde_yaml::Value::Null => UntaggedValue::Primitive(Primitive::Nothing).into_value(tag),
x => unimplemented!("Unsupported yaml case: {:?}", x),
})
}
pub fn from_yaml_string_to_value(s: String, tag: impl Into<Tag>) -> Result<Value, ShellError> {
let tag = tag.into();
let v: serde_yaml::Value = serde_yaml::from_str(&s).map_err(|x| {
ShellError::labeled_error(
format!("Could not load yaml: {}", x),
"could not load yaml from text",
&tag,
)
})?;
convert_yaml_value_to_nu_value(&v, tag)
}
fn from_yaml(args: CommandArgs) -> Result<OutputStream, ShellError> {
let tag = args.name_tag();
let input = args.input;
let concat_string = input.collect_string(tag.clone())?;
match from_yaml_string_to_value(concat_string.item, tag.clone()) {
Ok(x) => match x {
Value {
value: UntaggedValue::Table(list),
..
} => Ok(list.into_iter().into_output_stream()),
x => Ok(OutputStream::one(x)),
},
Err(_) => Err(ShellError::labeled_error_with_secondary(
"Could not parse as YAML",
"input cannot be parsed as YAML",
&tag,
"value originates from here",
&concat_string.tag,
)),
}
}
#[cfg(test)]
mod tests {
use super::ShellError;
use super::*;
use nu_protocol::row;
use nu_test_support::value::string;
#[test]
fn examples_work_as_expected() -> Result<(), ShellError> {
use crate::examples::test as test_examples;
test_examples(FromYaml {})
}
#[test]
fn test_problematic_yaml() {
struct TestCase {
description: &'static str,
input: &'static str,
expected: Result<Value, ShellError>,
}
let tt: Vec<TestCase> = vec![
TestCase {
description: "Double Curly Braces With Quotes",
input: r#"value: "{{ something }}""#,
expected: Ok(row!["value".to_owned() => string("{{ something }}")]),
},
TestCase {
description: "Double Curly Braces Without Quotes",
input: r#"value: {{ something }}"#,
expected: Ok(row!["value".to_owned() => string("{{ something }}")]),
},
];
for tc in tt.into_iter() {
let actual = from_yaml_string_to_value(tc.input.to_owned(), Tag::default());
if actual.is_err() {
assert!(
tc.expected.is_err(),
"actual is Err for test:\nTest Description {}\nErr: {:?}",
tc.description,
actual
);
} else {
assert_eq!(actual, tc.expected, "{}", tc.description);
}
}
}
}

View File

@ -0,0 +1,5 @@
mod from;
mod to;
pub use from::*;
pub use to::*;

View File

@ -0,0 +1,40 @@
use crate::prelude::*;
use nu_engine::WholeStreamCommand;
use nu_errors::ShellError;
use nu_protocol::{Signature, UntaggedValue};
#[derive(Clone)]
pub struct To;
impl WholeStreamCommand for To {
fn name(&self) -> &str {
"to"
}
fn signature(&self) -> Signature {
Signature::build("to")
}
fn usage(&self) -> &str {
"Convert table into an output format (based on subcommand, like csv, html, json, yaml)."
}
fn run(&self, args: CommandArgs) -> Result<OutputStream, ShellError> {
Ok(OutputStream::one(
UntaggedValue::string(get_full_help(&To, args.scope())).into_value(Tag::unknown()),
))
}
}
#[cfg(test)]
mod tests {
use super::ShellError;
use super::To;
#[test]
fn examples_work_as_expected() -> Result<(), ShellError> {
use crate::examples::test as test_examples;
test_examples(To {})
}
}

View File

@ -0,0 +1,80 @@
use crate::commands::formats::to::delimited::to_delimited_data;
use crate::prelude::*;
use nu_engine::WholeStreamCommand;
use nu_errors::ShellError;
use nu_protocol::{Primitive, Signature, SyntaxShape, UntaggedValue, Value};
pub struct ToCsv;
impl WholeStreamCommand for ToCsv {
fn name(&self) -> &str {
"to csv"
}
fn signature(&self) -> Signature {
Signature::build("to csv")
.named(
"separator",
SyntaxShape::String,
"a character to separate columns, defaults to ','",
Some('s'),
)
.switch(
"noheaders",
"do not output the columns names as the first row",
Some('n'),
)
}
fn usage(&self) -> &str {
"Convert table into .csv text "
}
fn run(&self, args: CommandArgs) -> Result<OutputStream, ShellError> {
to_csv(args)
}
}
fn to_csv(args: CommandArgs) -> Result<OutputStream, ShellError> {
let name = args.call_info.name_tag.clone();
let separator: Option<Value> = args.get_flag("separator")?;
let noheaders = args.has_flag("noheaders");
let input = args.input;
let sep = match separator {
Some(Value {
value: UntaggedValue::Primitive(Primitive::String(s)),
tag,
..
}) => {
if s == r"\t" {
'\t'
} else {
let vec_s: Vec<char> = s.chars().collect();
if vec_s.len() != 1 {
return Err(ShellError::labeled_error(
"Expected a single separator char from --separator",
"requires a single character string input",
tag,
));
};
vec_s[0]
}
}
_ => ',',
};
to_delimited_data(noheaders, sep, "CSV", input, name)
}
#[cfg(test)]
mod tests {
use super::ShellError;
use super::ToCsv;
#[test]
fn examples_work_as_expected() -> Result<(), ShellError> {
use crate::examples::test as test_examples;
test_examples(ToCsv {})
}
}

View File

@ -0,0 +1,217 @@
use crate::prelude::*;
use csv::WriterBuilder;
use indexmap::{indexset, IndexSet};
use nu_errors::ShellError;
use nu_protocol::{Primitive, UntaggedValue, Value};
use nu_source::Spanned;
use nu_value_ext::{as_string, ValueExt};
fn from_value_to_delimited_string(
tagged_value: &Value,
separator: char,
) -> Result<String, ShellError> {
let v = &tagged_value.value;
match v {
UntaggedValue::Row(o) => {
let mut wtr = WriterBuilder::new()
.delimiter(separator as u8)
.from_writer(vec![]);
let mut fields: VecDeque<String> = VecDeque::new();
let mut values: VecDeque<String> = VecDeque::new();
for (k, v) in o.entries.iter() {
fields.push_back(k.clone());
values.push_back(to_string_tagged_value(v)?);
}
wtr.write_record(fields).expect("can not write.");
wtr.write_record(values).expect("can not write.");
let v = String::from_utf8(wtr.into_inner().map_err(|_| {
ShellError::labeled_error(
"Could not convert record",
"original value",
&tagged_value.tag,
)
})?)
.map_err(|_| {
ShellError::labeled_error(
"Could not convert record",
"original value",
&tagged_value.tag,
)
})?;
Ok(v)
}
UntaggedValue::Table(list) => {
let mut wtr = WriterBuilder::new()
.delimiter(separator as u8)
.from_writer(vec![]);
let merged_descriptors = merge_descriptors(list);
if merged_descriptors.is_empty() {
wtr.write_record(
list.iter()
.map(|ele| to_string_tagged_value(ele).unwrap_or_else(|_| String::new()))
.collect::<Vec<_>>(),
)
.expect("can not write");
} else {
wtr.write_record(merged_descriptors.iter().map(|item| &item.item[..]))
.expect("can not write.");
for l in list {
let mut row = vec![];
for desc in &merged_descriptors {
row.push(match l.get_data_by_key(desc.borrow_spanned()) {
Some(s) => to_string_tagged_value(&s)?,
None => String::new(),
});
}
wtr.write_record(&row).expect("can not write");
}
}
let v = String::from_utf8(wtr.into_inner().map_err(|_| {
ShellError::labeled_error(
"Could not convert record",
"original value",
&tagged_value.tag,
)
})?)
.map_err(|_| {
ShellError::labeled_error(
"Could not convert record",
"original value",
&tagged_value.tag,
)
})?;
Ok(v)
}
_ => to_string_tagged_value(tagged_value),
}
}
// NOTE: could this be useful more widely and implemented on Value ?
pub fn clone_tagged_value(v: &Value) -> Value {
match &v.value {
UntaggedValue::Primitive(Primitive::String(s)) => {
UntaggedValue::Primitive(Primitive::String(s.clone()))
}
UntaggedValue::Primitive(Primitive::Nothing) => {
UntaggedValue::Primitive(Primitive::Nothing)
}
UntaggedValue::Primitive(Primitive::Boolean(b)) => {
UntaggedValue::Primitive(Primitive::Boolean(*b))
}
UntaggedValue::Primitive(Primitive::Decimal(f)) => {
UntaggedValue::Primitive(Primitive::Decimal(f.clone()))
}
UntaggedValue::Primitive(Primitive::Int(i)) => UntaggedValue::Primitive(Primitive::Int(*i)),
UntaggedValue::Primitive(Primitive::FilePath(x)) => {
UntaggedValue::Primitive(Primitive::FilePath(x.clone()))
}
UntaggedValue::Primitive(Primitive::Filesize(b)) => {
UntaggedValue::Primitive(Primitive::Filesize(*b))
}
UntaggedValue::Primitive(Primitive::Date(d)) => {
UntaggedValue::Primitive(Primitive::Date(*d))
}
UntaggedValue::Row(o) => UntaggedValue::Row(o.clone()),
UntaggedValue::Table(l) => UntaggedValue::Table(l.clone()),
UntaggedValue::Block(_) => UntaggedValue::Primitive(Primitive::Nothing),
_ => UntaggedValue::Primitive(Primitive::Nothing),
}
.into_value(v.tag.clone())
}
// NOTE: could this be useful more widely and implemented on Value ?
fn to_string_tagged_value(v: &Value) -> Result<String, ShellError> {
match &v.value {
UntaggedValue::Primitive(Primitive::String(_))
| UntaggedValue::Primitive(Primitive::Filesize(_))
| UntaggedValue::Primitive(Primitive::Boolean(_))
| UntaggedValue::Primitive(Primitive::Decimal(_))
| UntaggedValue::Primitive(Primitive::FilePath(_))
| UntaggedValue::Primitive(Primitive::Int(_)) => as_string(v),
UntaggedValue::Primitive(Primitive::Date(d)) => Ok(d.to_string()),
UntaggedValue::Primitive(Primitive::Nothing) => Ok(String::new()),
UntaggedValue::Table(_) => Ok(String::from("[Table]")),
UntaggedValue::Row(_) => Ok(String::from("[Row]")),
_ => Err(ShellError::labeled_error(
"Unexpected value",
"",
v.tag.clone(),
)),
}
}
fn merge_descriptors(values: &[Value]) -> Vec<Spanned<String>> {
let mut ret: Vec<Spanned<String>> = vec![];
let mut seen: IndexSet<String> = indexset! {};
for value in values {
for desc in value.data_descriptors() {
if !seen.contains(&desc[..]) {
seen.insert(desc.clone());
ret.push(desc.spanned(value.tag.span));
}
}
}
ret
}
pub fn to_delimited_data(
noheaders: bool,
sep: char,
format_name: &'static str,
input: InputStream,
name: Tag,
) -> Result<OutputStream, ShellError> {
let name_tag = name;
let name_span = name_tag.span;
let input: Vec<Value> = input.collect();
let to_process_input = match input.len() {
x if x > 1 => {
let tag = input[0].tag.clone();
vec![Value {
value: UntaggedValue::Table(input),
tag,
}]
}
1 => input,
_ => vec![],
};
Ok((to_process_input.into_iter().map(move |value| {
match from_value_to_delimited_string(&clone_tagged_value(&value), sep) {
Ok(mut x) => {
if noheaders {
if let Some(second_line) = x.find('\n') {
let start = second_line + 1;
x.replace_range(0..start, "");
}
}
UntaggedValue::Primitive(Primitive::String(x)).into_value(&name_tag)
}
Err(_) => {
let expected = format!(
"Expected a table with {}-compatible structure from pipeline",
format_name
);
let requires = format!("requires {}-compatible input", format_name);
Value::error(ShellError::labeled_error_with_secondary(
expected,
requires,
name_span,
"originates from here".to_string(),
value.tag.span,
))
}
}
}))
.into_output_stream())
}

View File

@ -0,0 +1,742 @@
use crate::prelude::*;
use nu_data::value::format_leaf;
use nu_engine::WholeStreamCommand;
use nu_errors::ShellError;
use nu_protocol::{Primitive, Signature, SyntaxShape, UntaggedValue, Value};
use nu_source::{AnchorLocation, Tagged};
use regex::Regex;
use rust_embed::RustEmbed;
use serde::{Deserialize, Serialize};
use std::borrow::Cow;
use std::collections::HashMap;
use std::error::Error;
#[derive(Serialize, Deserialize, Debug)]
pub struct HtmlThemes {
themes: Vec<HtmlTheme>,
}
#[allow(non_snake_case)]
#[derive(Serialize, Deserialize, Debug)]
pub struct HtmlTheme {
name: String,
black: String,
red: String,
green: String,
yellow: String,
blue: String,
purple: String,
cyan: String,
white: String,
brightBlack: String,
brightRed: String,
brightGreen: String,
brightYellow: String,
brightBlue: String,
brightPurple: String,
brightCyan: String,
brightWhite: String,
background: String,
foreground: String,
}
impl Default for HtmlThemes {
fn default() -> Self {
HtmlThemes {
themes: vec![HtmlTheme::default()],
}
}
}
impl Default for HtmlTheme {
fn default() -> Self {
HtmlTheme {
name: "nu_default".to_string(),
black: "black".to_string(),
red: "red".to_string(),
green: "green".to_string(),
yellow: "#717100".to_string(),
blue: "blue".to_string(),
purple: "#c800c8".to_string(),
cyan: "#037979".to_string(),
white: "white".to_string(),
brightBlack: "black".to_string(),
brightRed: "red".to_string(),
brightGreen: "green".to_string(),
brightYellow: "#717100".to_string(),
brightBlue: "blue".to_string(),
brightPurple: "#c800c8".to_string(),
brightCyan: "#037979".to_string(),
brightWhite: "white".to_string(),
background: "white".to_string(),
foreground: "black".to_string(),
}
}
}
#[derive(RustEmbed)]
#[folder = "assets/"]
struct Assets;
pub struct ToHtml;
impl WholeStreamCommand for ToHtml {
fn name(&self) -> &str {
"to html"
}
fn signature(&self) -> Signature {
Signature::build("to html")
.switch("html_color", "change ansi colors to html colors", Some('c'))
.switch("no_color", "remove all ansi colors in output", Some('n'))
.switch(
"dark",
"indicate your background color is a darker color",
Some('d'),
)
.switch(
"partial",
"only output the html for the content itself",
Some('p'),
)
.named(
"theme",
SyntaxShape::String,
"the name of the theme to use (github, blulocolight, ...)",
Some('t'),
)
.switch("list", "list the names of all available themes", Some('l'))
}
fn usage(&self) -> &str {
"Convert table into simple HTML"
}
fn run(&self, args: CommandArgs) -> Result<OutputStream, ShellError> {
to_html(args)
}
}
fn get_theme_from_asset_file(
is_dark: bool,
theme: &Option<Tagged<String>>,
theme_tag: &Tag,
) -> Result<HashMap<&'static str, String>, ShellError> {
let theme_name = match theme {
Some(s) => s.to_string(),
None => "default".to_string(), // There is no theme named "default" so this will be HtmlTheme::default(), which is "nu_default".
};
// 228 themes come from
// https://github.com/mbadolato/iTerm2-Color-Schemes/tree/master/windowsterminal
// we should find a hit on any name in there
let asset = get_asset_by_name_as_html_themes("228_themes.zip", "228_themes.json");
// If asset doesn't work, make sure to return the default theme
let asset = match asset {
Ok(a) => a,
_ => HtmlThemes::default(),
};
// Find the theme by theme name
let th = asset
.themes
.iter()
.find(|&n| n.name.to_lowercase() == *theme_name.to_lowercase().as_str()); // case insensitive search
// If no theme is found by the name provided, ensure we return the default theme
let default_theme = HtmlTheme::default();
let th = match th {
Some(t) => t,
None => &default_theme,
};
// this just means no theme was passed in
if th.name.to_lowercase().eq(&"nu_default".to_string())
// this means there was a theme passed in
&& theme.is_some()
{
return Err(ShellError::labeled_error(
"Error finding theme name",
"Error finding theme name",
theme_tag.span,
));
}
Ok(convert_html_theme_to_hash_map(is_dark, th))
}
#[allow(unused_variables)]
fn get_asset_by_name_as_html_themes(
zip_name: &str,
json_name: &str,
) -> Result<HtmlThemes, Box<dyn Error>> {
match Assets::get(zip_name) {
Some(content) => {
let asset: Vec<u8> = match content {
Cow::Borrowed(bytes) => bytes.into(),
Cow::Owned(bytes) => bytes,
};
let reader = std::io::Cursor::new(asset);
#[cfg(feature = "zip")]
{
use std::io::Read;
let mut archive = zip::ZipArchive::new(reader)?;
let mut zip_file = archive.by_name(json_name)?;
let mut contents = String::new();
zip_file.read_to_string(&mut contents)?;
Ok(serde_json::from_str(&contents)?)
}
#[cfg(not(feature = "zip"))]
{
let th = HtmlThemes::default();
Ok(th)
}
}
None => {
let th = HtmlThemes::default();
Ok(th)
}
}
}
fn convert_html_theme_to_hash_map(
is_dark: bool,
theme: &HtmlTheme,
) -> HashMap<&'static str, String> {
let mut hm: HashMap<&str, String> = HashMap::new();
hm.insert("bold_black", theme.brightBlack[..].to_string());
hm.insert("bold_red", theme.brightRed[..].to_string());
hm.insert("bold_green", theme.brightGreen[..].to_string());
hm.insert("bold_yellow", theme.brightYellow[..].to_string());
hm.insert("bold_blue", theme.brightBlue[..].to_string());
hm.insert("bold_magenta", theme.brightPurple[..].to_string());
hm.insert("bold_cyan", theme.brightCyan[..].to_string());
hm.insert("bold_white", theme.brightWhite[..].to_string());
hm.insert("black", theme.black[..].to_string());
hm.insert("red", theme.red[..].to_string());
hm.insert("green", theme.green[..].to_string());
hm.insert("yellow", theme.yellow[..].to_string());
hm.insert("blue", theme.blue[..].to_string());
hm.insert("magenta", theme.purple[..].to_string());
hm.insert("cyan", theme.cyan[..].to_string());
hm.insert("white", theme.white[..].to_string());
// Try to make theme work with light or dark but
// flipping the foreground and background but leave
// the other colors the same.
if is_dark {
hm.insert("background", theme.black[..].to_string());
hm.insert("foreground", theme.white[..].to_string());
} else {
hm.insert("background", theme.white[..].to_string());
hm.insert("foreground", theme.black[..].to_string());
}
hm
}
fn get_list_of_theme_names() -> Vec<String> {
let asset = get_asset_by_name_as_html_themes("228_themes.zip", "228_themes.json");
// If asset doesn't work, make sure to return the default theme
let html_themes = match asset {
Ok(a) => a,
_ => HtmlThemes::default(),
};
let theme_names: Vec<String> = html_themes
.themes
.iter()
.map(|n| n.name[..].to_string())
.collect();
theme_names
}
fn to_html(args: CommandArgs) -> Result<OutputStream, ShellError> {
let name_tag = args.call_info.name_tag.clone();
let html_color = args.has_flag("html_color");
let no_color = args.has_flag("no_color");
let dark = args.has_flag("dark");
let partial = args.has_flag("partial");
let list = args.has_flag("list");
let theme: Option<Tagged<String>> = args.get_flag("theme")?;
let input: Vec<Value> = args.input.collect();
let headers = nu_protocol::merge_descriptors(&input);
let headers = Some(headers)
.filter(|headers| !headers.is_empty() && (headers.len() > 1 || !headers[0].is_empty()));
let mut output_string = String::new();
let mut regex_hm: HashMap<u32, (&str, String)> = HashMap::new();
if list {
// Get the list of theme names
let theme_names = get_list_of_theme_names();
// Put that list into the output string
for s in theme_names.iter() {
output_string.push_str(&format!("{}\n", s));
}
output_string.push_str("\nScreenshots of themes can be found here:\n");
output_string.push_str("https://github.com/mbadolato/iTerm2-Color-Schemes\n");
} else {
let theme_tag = match &theme {
Some(v) => &v.tag,
None => &name_tag,
};
let color_hm = get_theme_from_asset_file(dark, &theme, theme_tag);
let color_hm = match color_hm {
Ok(c) => c,
_ => {
return Err(ShellError::labeled_error(
"Error finding theme name",
"Error finding theme name",
theme_tag.span,
))
}
};
// change the color of the page
if !partial {
output_string.push_str(&format!(
r"<html><style>body {{ background-color:{};color:{}; }}</style><body>",
color_hm
.get("background")
.expect("Error getting background color"),
color_hm
.get("foreground")
.expect("Error getting foreground color")
));
} else {
output_string.push_str(&format!(
"<div style=\"background-color:{};color:{};\">",
color_hm
.get("background")
.expect("Error getting background color"),
color_hm
.get("foreground")
.expect("Error getting foreground color")
));
}
let inner_value = match input.len() {
0 => String::default(),
1 => match headers {
Some(headers) => html_table(input, headers),
None => {
let value = &input[0];
html_value(value)
}
},
_ => match headers {
Some(headers) => html_table(input, headers),
None => html_list(input),
},
};
output_string.push_str(&inner_value);
if !partial {
output_string.push_str("</body></html>");
} else {
output_string.push_str("</div>")
}
// Check to see if we want to remove all color or change ansi to html colors
if html_color {
setup_html_color_regexes(&mut regex_hm, &color_hm);
output_string = run_regexes(&regex_hm, &output_string);
} else if no_color {
setup_no_color_regexes(&mut regex_hm);
output_string = run_regexes(&regex_hm, &output_string);
}
}
Ok(OutputStream::one(
UntaggedValue::string(output_string).into_value(name_tag),
))
}
fn html_list(list: Vec<Value>) -> String {
let mut output_string = String::new();
output_string.push_str("<ol>");
for value in list {
output_string.push_str("<li>");
output_string.push_str(&html_value(&value));
output_string.push_str("</li>");
}
output_string.push_str("</ol>");
output_string
}
fn html_table(table: Vec<Value>, headers: Vec<String>) -> String {
let mut output_string = String::new();
// Add grid lines to html
// let mut output_string = "<html><head><style>".to_string();
// output_string.push_str("table, th, td { border: 2px solid black; border-collapse: collapse; padding: 10px; }");
// output_string.push_str("</style></head><body>");
output_string.push_str("<table>");
output_string.push_str("<tr>");
for header in &headers {
output_string.push_str("<th>");
output_string.push_str(&htmlescape::encode_minimal(header));
output_string.push_str("</th>");
}
output_string.push_str("</tr>");
for row in table {
if let UntaggedValue::Row(row) = row.value {
output_string.push_str("<tr>");
for header in &headers {
let data = row.get_data(header);
output_string.push_str("<td>");
output_string.push_str(&html_value(data.borrow()));
output_string.push_str("</td>");
}
output_string.push_str("</tr>");
}
}
output_string.push_str("</table>");
output_string
}
fn html_value(value: &Value) -> String {
let mut output_string = String::new();
match &value.value {
UntaggedValue::Primitive(Primitive::Binary(b)) => {
// This might be a bit much, but it's fun :)
match &value.tag.anchor {
Some(AnchorLocation::Url(f)) | Some(AnchorLocation::File(f)) => {
let extension = f.split('.').last().map(String::from);
match extension {
Some(s)
if ["png", "jpg", "bmp", "gif", "tiff", "jpeg"]
.contains(&s.to_lowercase().as_str()) =>
{
output_string.push_str("<img src=\"data:image/");
output_string.push_str(&s);
output_string.push_str(";base64,");
output_string.push_str(&base64::encode(&b));
output_string.push_str("\">");
}
_ => {
let output = nu_pretty_hex::pretty_hex(&b);
output_string.push_str("<pre>");
output_string.push_str(&output);
output_string.push_str("</pre>");
}
}
}
_ => {
let output = nu_pretty_hex::pretty_hex(&b);
output_string.push_str("<pre>");
output_string.push_str(&output);
output_string.push_str("</pre>");
}
}
}
UntaggedValue::Primitive(Primitive::String(ref b)) => {
// This might be a bit much, but it's fun :)
match &value.tag.anchor {
Some(AnchorLocation::Url(f)) | Some(AnchorLocation::File(f)) => {
let extension = f.split('.').last().map(String::from);
match extension {
Some(s) if s.to_lowercase() == "svg" => {
output_string.push_str("<img src=\"data:image/svg+xml;base64,");
output_string.push_str(&base64::encode(&b.as_bytes()));
output_string.push_str("\">");
return output_string;
}
_ => {}
}
}
_ => {}
}
output_string.push_str(
&htmlescape::encode_minimal(&format_leaf(&value.value).plain_string(100_000))
.replace("\n", "<br>"),
);
}
other => output_string.push_str(
&htmlescape::encode_minimal(&format_leaf(other).plain_string(100_000))
.replace("\n", "<br>"),
),
}
output_string
}
fn setup_html_color_regexes(
hash: &mut HashMap<u32, (&'static str, String)>,
color_hm: &HashMap<&str, String>,
) {
// All the bold colors
hash.insert(
0,
(
r"(?P<reset>\[0m)(?P<word>[[:alnum:][:space:][:punct:]]*)",
// Reset the text color, normal weight font
format!(
r"<span style='color:{};font-weight:normal;'>$word</span>",
color_hm
.get("foreground")
.expect("Error getting reset text color")
),
),
);
hash.insert(
1,
(
// Bold Black
r"(?P<bb>\[1;30m)(?P<word>[[:alnum:][:space:][:punct:]]*)",
format!(
r"<span style='color:{};font-weight:bold;'>$word</span>",
color_hm
.get("foreground")
.expect("Error getting bold black text color")
),
),
);
hash.insert(
2,
(
// Bold Red
r"(?P<br>\[1;31m)(?P<word>[[:alnum:][:space:][:punct:]]*)",
format!(
r"<span style='color:{};font-weight:bold;'>$word</span>",
color_hm
.get("bold_red")
.expect("Error getting bold red text color"),
),
),
);
hash.insert(
3,
(
// Bold Green
r"(?P<bg>\[1;32m)(?P<word>[[:alnum:][:space:][:punct:]]*)",
format!(
r"<span style='color:{};font-weight:bold;'>$word</span>",
color_hm
.get("bold_green")
.expect("Error getting bold green text color"),
),
),
);
hash.insert(
4,
(
// Bold Yellow
r"(?P<by>\[1;33m)(?P<word>[[:alnum:][:space:][:punct:]]*)",
format!(
r"<span style='color:{};font-weight:bold;'>$word</span>",
color_hm
.get("bold_yellow")
.expect("Error getting bold yellow text color"),
),
),
);
hash.insert(
5,
(
// Bold Blue
r"(?P<bu>\[1;34m)(?P<word>[[:alnum:][:space:][:punct:]]*)",
format!(
r"<span style='color:{};font-weight:bold;'>$word</span>",
color_hm
.get("bold_blue")
.expect("Error getting bold blue text color"),
),
),
);
hash.insert(
6,
(
// Bold Magenta
r"(?P<bm>\[1;35m)(?P<word>[[:alnum:][:space:][:punct:]]*)",
format!(
r"<span style='color:{};font-weight:bold;'>$word</span>",
color_hm
.get("bold_magenta")
.expect("Error getting bold magenta text color"),
),
),
);
hash.insert(
7,
(
// Bold Cyan
r"(?P<bc>\[1;36m)(?P<word>[[:alnum:][:space:][:punct:]]*)",
format!(
r"<span style='color:{};font-weight:bold;'>$word</span>",
color_hm
.get("bold_cyan")
.expect("Error getting bold cyan text color"),
),
),
);
hash.insert(
8,
(
// Bold White
// Let's change this to black since the html background
// is white. White on white = no bueno.
r"(?P<bw>\[1;37m)(?P<word>[[:alnum:][:space:][:punct:]]*)",
format!(
r"<span style='color:{};font-weight:bold;'>$word</span>",
color_hm
.get("foreground")
.expect("Error getting bold bold white text color"),
),
),
);
// All the normal colors
hash.insert(
9,
(
// Black
r"(?P<b>\[30m)(?P<word>[[:alnum:][:space:][:punct:]]*)",
format!(
r"<span style='color:{};'>$word</span>",
color_hm
.get("foreground")
.expect("Error getting black text color"),
),
),
);
hash.insert(
10,
(
// Red
r"(?P<r>\[31m)(?P<word>[[:alnum:][:space:][:punct:]]*)",
format!(
r"<span style='color:{};'>$word</span>",
color_hm.get("red").expect("Error getting red text color"),
),
),
);
hash.insert(
11,
(
// Green
r"(?P<g>\[32m)(?P<word>[[:alnum:][:space:][:punct:]]*)",
format!(
r"<span style='color:{};'>$word</span>",
color_hm
.get("green")
.expect("Error getting green text color"),
),
),
);
hash.insert(
12,
(
// Yellow
r"(?P<y>\[33m)(?P<word>[[:alnum:][:space:][:punct:]]*)",
format!(
r"<span style='color:{};'>$word</span>",
color_hm
.get("yellow")
.expect("Error getting yellow text color"),
),
),
);
hash.insert(
13,
(
// Blue
r"(?P<u>\[34m)(?P<word>[[:alnum:][:space:][:punct:]]*)",
format!(
r"<span style='color:{};'>$word</span>",
color_hm.get("blue").expect("Error getting blue text color"),
),
),
);
hash.insert(
14,
(
// Magenta
r"(?P<m>\[35m)(?P<word>[[:alnum:][:space:][:punct:]]*)",
format!(
r"<span style='color:{};'>$word</span>",
color_hm
.get("magenta")
.expect("Error getting magenta text color"),
),
),
);
hash.insert(
15,
(
// Cyan
r"(?P<c>\[36m)(?P<word>[[:alnum:][:space:][:punct:]]*)",
format!(
r"<span style='color:{};'>$word</span>",
color_hm.get("cyan").expect("Error getting cyan text color"),
),
),
);
hash.insert(
16,
(
// White
// Let's change this to black since the html background
// is white. White on white = no bueno.
r"(?P<w>\[37m)(?P<word>[[:alnum:][:space:][:punct:]]*)",
format!(
r"<span style='color:{};'>$word</span>",
color_hm
.get("foreground")
.expect("Error getting white text color"),
),
),
);
}
fn setup_no_color_regexes(hash: &mut HashMap<u32, (&'static str, String)>) {
// We can just use one regex here because we're just removing ansi sequences
// and not replacing them with html colors.
// attribution: https://stackoverflow.com/questions/14693701/how-can-i-remove-the-ansi-escape-sequences-from-a-string-in-python
hash.insert(
0,
(
r"(?:\x1B[@-Z\\-_]|[\x80-\x9A\x9C-\x9F]|(?:\x1B\[|\x9B)[0-?]*[ -/]*[@-~])",
r"$name_group_doesnt_exist".to_string(),
),
);
}
fn run_regexes(hash: &HashMap<u32, (&'static str, String)>, contents: &str) -> String {
let mut working_string = contents.to_owned();
let hash_count: u32 = hash.len() as u32;
for n in 0..hash_count {
let value = hash.get(&n).expect("error getting hash at index");
//println!("{},{}", value.0, value.1);
let re = Regex::new(value.0).expect("problem with color regex");
let after = re.replace_all(&working_string, &value.1[..]).to_string();
working_string = after.clone();
}
working_string
}
#[cfg(test)]
mod tests {
use super::ShellError;
use super::*;
#[test]
fn examples_work_as_expected() -> Result<(), ShellError> {
use crate::examples::test as test_examples;
test_examples(ToHtml {})
}
}

View File

@ -0,0 +1,258 @@
use crate::prelude::*;
use nu_engine::WholeStreamCommand;
use nu_errors::{CoerceInto, ShellError};
use nu_protocol::{Primitive, Signature, SyntaxShape, UnspannedPathMember, UntaggedValue, Value};
use serde::Serialize;
use serde_json::json;
pub struct ToJson;
impl WholeStreamCommand for ToJson {
fn name(&self) -> &str {
"to json"
}
fn signature(&self) -> Signature {
Signature::build("to json").named(
"pretty",
SyntaxShape::Int,
"Formats the JSON text with the provided indentation setting",
Some('p'),
)
}
fn usage(&self) -> &str {
"Converts table data into JSON text."
}
fn run(&self, args: CommandArgs) -> Result<OutputStream, ShellError> {
to_json(args)
}
fn examples(&self) -> Vec<Example> {
vec![
Example {
description:
"Outputs an unformatted JSON string representing the contents of this table",
example: "echo [1 2 3] | to json",
result: Some(vec![Value::from("[1,2,3]")]),
},
Example {
description:
"Outputs a formatted JSON string representing the contents of this table with an indentation setting of 2 spaces",
example: "echo [1 2 3] | to json --pretty 2",
result: Some(vec![Value::from("[\n 1,\n 2,\n 3\n]")]),
},
]
}
}
pub fn value_to_json_value(v: &Value) -> Result<serde_json::Value, ShellError> {
Ok(match &v.value {
UntaggedValue::Primitive(Primitive::Boolean(b)) => serde_json::Value::Bool(*b),
UntaggedValue::Primitive(Primitive::Filesize(b)) => serde_json::Value::Number(
serde_json::Number::from(b.to_u64().expect("What about really big numbers")),
),
UntaggedValue::Primitive(Primitive::Duration(i)) => {
serde_json::Value::String(i.to_string())
}
UntaggedValue::Primitive(Primitive::Date(d)) => serde_json::Value::String(d.to_string()),
UntaggedValue::Primitive(Primitive::EndOfStream) => serde_json::Value::Null,
UntaggedValue::Primitive(Primitive::BeginningOfStream) => serde_json::Value::Null,
UntaggedValue::Primitive(Primitive::Decimal(f)) => {
if let Some(f) = f.to_f64() {
if let Some(num) = serde_json::Number::from_f64(
f.to_f64().expect("TODO: What about really big decimals?"),
) {
serde_json::Value::Number(num)
} else {
return Err(ShellError::labeled_error(
"Could not convert value to decimal number",
"could not convert to decimal",
&v.tag,
));
}
} else {
return Err(ShellError::labeled_error(
"Could not convert value to decimal number",
"could not convert to decimal",
&v.tag,
));
}
}
UntaggedValue::Primitive(Primitive::Int(i)) => {
serde_json::Value::Number(serde_json::Number::from(*i))
}
UntaggedValue::Primitive(Primitive::BigInt(i)) => {
serde_json::Value::Number(serde_json::Number::from(CoerceInto::<i64>::coerce_into(
i.tagged(&v.tag),
"converting to JSON number",
)?))
}
UntaggedValue::Primitive(Primitive::Nothing) => serde_json::Value::Null,
UntaggedValue::Primitive(Primitive::GlobPattern(s)) => serde_json::Value::String(s.clone()),
UntaggedValue::Primitive(Primitive::String(s)) => serde_json::Value::String(s.clone()),
UntaggedValue::Primitive(Primitive::ColumnPath(path)) => serde_json::Value::Array(
path.iter()
.map(|x| match &x.unspanned {
UnspannedPathMember::String(string) => {
Ok(serde_json::Value::String(string.clone()))
}
UnspannedPathMember::Int(int) => {
Ok(serde_json::Value::Number(serde_json::Number::from(*int)))
}
})
.collect::<Result<Vec<serde_json::Value>, ShellError>>()?,
),
UntaggedValue::Primitive(Primitive::FilePath(s)) => {
serde_json::Value::String(s.display().to_string())
}
UntaggedValue::Table(l) => serde_json::Value::Array(json_list(l)?),
UntaggedValue::Error(e) => return Err(e.clone()),
UntaggedValue::Block(_) | UntaggedValue::Primitive(Primitive::Range(_)) => {
serde_json::Value::Null
}
#[cfg(feature = "dataframe")]
UntaggedValue::DataFrame(_) => serde_json::Value::Null,
UntaggedValue::Primitive(Primitive::Binary(b)) => serde_json::Value::Array(
b.iter()
.map(|x| {
serde_json::Number::from_f64(*x as f64).ok_or_else(|| {
ShellError::labeled_error(
"Can not convert number from floating point",
"can not convert to number",
&v.tag,
)
})
})
.collect::<Result<Vec<serde_json::Number>, ShellError>>()?
.into_iter()
.map(serde_json::Value::Number)
.collect(),
),
UntaggedValue::Row(o) => {
let mut m = serde_json::Map::new();
for (k, v) in o.entries.iter() {
m.insert(k.clone(), value_to_json_value(v)?);
}
serde_json::Value::Object(m)
}
})
}
fn json_list(input: &[Value]) -> Result<Vec<serde_json::Value>, ShellError> {
let mut out = vec![];
for value in input {
out.push(value_to_json_value(value)?);
}
Ok(out)
}
fn to_json(args: CommandArgs) -> Result<OutputStream, ShellError> {
let name_tag = args.call_info.name_tag.clone();
let pretty: Option<Value> = args.get_flag("pretty")?;
let name_span = name_tag.span;
let input: Vec<Value> = args.input.collect();
let to_process_input = match input.len() {
x if x > 1 => {
let tag = input[0].tag.clone();
vec![Value {
value: UntaggedValue::Table(input),
tag,
}]
}
1 => input,
_ => vec![],
};
Ok((to_process_input
.into_iter()
.map(move |value| match value_to_json_value(&value) {
Ok(json_value) => {
let value_span = value.tag.span;
match serde_json::to_string(&json_value) {
Ok(mut serde_json_string) => {
if let Some(pretty_value) = &pretty {
let mut pretty_format_failed = true;
if let Ok(pretty_u64) = pretty_value.as_u64() {
if let Ok(serde_json_value) =
serde_json::from_str::<serde_json::Value>(
serde_json_string.as_str(),
)
{
let indentation_string = " ".repeat(pretty_u64 as usize);
let serde_formatter =
serde_json::ser::PrettyFormatter::with_indent(
indentation_string.as_bytes(),
);
let serde_buffer = Vec::new();
let mut serde_serializer =
serde_json::Serializer::with_formatter(
serde_buffer,
serde_formatter,
);
let serde_json_object = json!(serde_json_value);
if let Ok(()) =
serde_json_object.serialize(&mut serde_serializer)
{
if let Ok(ser_json_string) =
String::from_utf8(serde_serializer.into_inner())
{
pretty_format_failed = false;
serde_json_string = ser_json_string
}
}
}
}
if pretty_format_failed {
return Value::error(ShellError::labeled_error(
"Pretty formatting failed",
"failed",
pretty_value.tag(),
));
}
}
UntaggedValue::Primitive(Primitive::String(serde_json_string))
.into_value(&value.tag)
}
_ => Value::error(ShellError::labeled_error_with_secondary(
"Expected a table with JSON-compatible structure.tag() from pipeline",
"requires JSON-compatible input",
name_span,
"originates from here".to_string(),
value_span,
)),
}
}
_ => Value::error(ShellError::labeled_error(
"Expected a table with JSON-compatible structure from pipeline",
"requires JSON-compatible input",
&name_tag,
)),
}))
.into_output_stream())
}
#[cfg(test)]
mod tests {
use super::ShellError;
use super::ToJson;
#[test]
fn examples_work_as_expected() -> Result<(), ShellError> {
use crate::examples::test as test_examples;
test_examples(ToJson {})
}
}

View File

@ -0,0 +1,380 @@
use crate::prelude::*;
use nu_data::value::format_leaf;
use nu_engine::WholeStreamCommand;
use nu_errors::ShellError;
use nu_protocol::{Signature, UntaggedValue, Value};
pub struct Command;
#[derive(Deserialize)]
pub struct Arguments {
pretty: bool,
#[serde(rename = "per-element")]
per_element: bool,
}
impl WholeStreamCommand for Command {
fn name(&self) -> &str {
"to md"
}
fn signature(&self) -> Signature {
Signature::build("to md")
.switch(
"pretty",
"Formats the Markdown table to vertically align items",
Some('p'),
)
.switch(
"per-element",
"treat each row as markdown syntax element",
Some('e'),
)
}
fn usage(&self) -> &str {
"Convert table into simple Markdown"
}
fn run(&self, args: CommandArgs) -> Result<OutputStream, ShellError> {
to_md(args)
}
fn examples(&self) -> Vec<Example> {
vec![
Example {
description: "Outputs an unformatted table markdown string (default)",
example: "ls | to md",
result: Some(vec![Value::from(one(r#"
|name|type|chickens|modified|
|-|-|-|-|
|Andres.txt|File|10|1 year ago|
|Jonathan|Dir|5|1 year ago|
|Darren.txt|File|20|1 year ago|
|Yehuda|Dir|4|1 year ago|
"#))]),
},
Example {
description: "Optionally, output a formatted markdown string",
example: "ls | to md --pretty",
result: Some(vec![Value::from(one(r#"
| name | type | chickens | modified |
| ---------- | ---- | -------- | ---------- |
| Andres.txt | File | 10 | 1 year ago |
| Jonathan | Dir | 5 | 1 year ago |
| Darren.txt | File | 20 | 1 year ago |
| Yehuda | Dir | 4 | 1 year ago |
"#))]),
},
Example {
description: "Treat each row as a markdown element",
example: "echo [[H1]; [\"Welcome to Nushell\"]] | append (ls | first 2) | to md --per-element --pretty",
result: Some(vec![Value::from(one(r#"
# Welcome to Nushell
| name | type | chickens | modified |
| ---------- | ---- | -------- | ---------- |
| Andres.txt | File | 10 | 1 year ago |
| Jonathan | Dir | 5 | 1 year ago |
"#))]),
}
]
}
}
fn to_md(args: CommandArgs) -> Result<OutputStream, ShellError> {
let name_tag = args.call_info.name_tag.clone();
let arguments = Arguments {
per_element: args.has_flag("per-element"),
pretty: args.has_flag("pretty"),
};
let input: Vec<Value> = args.input.collect();
Ok(OutputStream::one(
UntaggedValue::string(process(&input, arguments)).into_value(if input.is_empty() {
name_tag
} else {
input[0].tag()
}),
))
}
fn process(
input: &[Value],
Arguments {
pretty,
per_element,
}: Arguments,
) -> String {
if per_element {
input
.iter()
.map(|v| match &v.value {
UntaggedValue::Table(values) => table(values, pretty),
_ => fragment(v, pretty),
})
.collect::<String>()
} else {
table(input, pretty)
}
}
fn fragment(input: &Value, pretty: bool) -> String {
let headers = input.data_descriptors();
let mut out = String::new();
if headers.len() == 1 {
let markup = match (&headers[0]).to_ascii_lowercase().as_ref() {
"h1" => "# ".to_string(),
"h2" => "## ".to_string(),
"h3" => "### ".to_string(),
"blockquote" => "> ".to_string(),
_ => return table(&[input.clone()], pretty),
};
out.push_str(&markup);
out.push_str(&format_leaf(input.get_data(&headers[0]).borrow()).plain_string(100_000));
} else if input.is_row() {
let string = match input.row_entries().next() {
Some(value) => value.1.as_string().unwrap_or_default(),
None => String::from(""),
};
out = format_leaf(&UntaggedValue::from(string)).plain_string(100_000)
} else {
out = format_leaf(&input.value).plain_string(100_000)
}
out.push('\n');
out
}
fn collect_headers(headers: &[String]) -> (Vec<String>, Vec<usize>) {
let mut escaped_headers: Vec<String> = Vec::new();
let mut column_widths: Vec<usize> = Vec::new();
if !headers.is_empty() && (headers.len() > 1 || !headers[0].is_empty()) {
for header in headers {
let escaped_header_string = htmlescape::encode_minimal(header);
column_widths.push(escaped_header_string.len());
escaped_headers.push(escaped_header_string);
}
} else {
column_widths = vec![0; headers.len()]
}
(escaped_headers, column_widths)
}
fn table(input: &[Value], pretty: bool) -> String {
let headers = nu_protocol::merge_descriptors(input);
let (escaped_headers, mut column_widths) = collect_headers(&headers);
let mut escaped_rows: Vec<Vec<String>> = Vec::new();
for row in input {
let mut escaped_row: Vec<String> = Vec::new();
match row.value.clone() {
UntaggedValue::Row(row) => {
for i in 0..headers.len() {
let data = row.get_data(&headers[i]);
let value_string = format_leaf(data.borrow()).plain_string(100_000);
let new_column_width = value_string.len();
escaped_row.push(value_string);
if column_widths[i] < new_column_width {
column_widths[i] = new_column_width;
}
}
}
p => {
let value_string =
htmlescape::encode_minimal(&format_leaf(&p).plain_string(100_000));
escaped_row.push(value_string);
}
}
escaped_rows.push(escaped_row);
}
let output_string = if (column_widths.is_empty() || column_widths.iter().all(|x| *x == 0))
&& escaped_rows.is_empty()
{
String::from("")
} else {
get_output_string(&escaped_headers, &escaped_rows, &column_widths, pretty)
.trim()
.to_string()
};
output_string
}
fn get_output_string(
headers: &[String],
rows: &[Vec<String>],
column_widths: &[usize],
pretty: bool,
) -> String {
let mut output_string = String::new();
if !headers.is_empty() {
output_string.push('|');
for i in 0..headers.len() {
if pretty {
output_string.push(' ');
output_string.push_str(&get_padded_string(
headers[i].clone(),
column_widths[i],
' ',
));
output_string.push(' ');
} else {
output_string.push_str(headers[i].as_str());
}
output_string.push('|');
}
output_string.push_str("\n|");
#[allow(clippy::needless_range_loop)]
for i in 0..headers.len() {
if pretty {
output_string.push(' ');
output_string.push_str(&get_padded_string(
String::from("-"),
column_widths[i],
'-',
));
output_string.push(' ');
} else {
output_string.push('-');
}
output_string.push('|');
}
output_string.push('\n');
}
for row in rows {
if !headers.is_empty() {
output_string.push('|');
}
for i in 0..row.len() {
if pretty {
output_string.push(' ');
output_string.push_str(&get_padded_string(row[i].clone(), column_widths[i], ' '));
output_string.push(' ');
} else {
output_string.push_str(row[i].as_str());
}
if !headers.is_empty() {
output_string.push('|');
}
}
output_string.push('\n');
}
output_string
}
fn get_padded_string(text: String, desired_length: usize, padding_character: char) -> String {
let repeat_length = if text.len() > desired_length {
0
} else {
desired_length - text.len()
};
format!(
"{}{}",
text,
padding_character.to_string().repeat(repeat_length)
)
}
fn one(string: &str) -> String {
string
.lines()
.skip(1)
.map(|line| line.trim())
.collect::<Vec<&str>>()
.join("\n")
.trim_end()
.to_string()
}
#[cfg(test)]
mod tests {
use super::{fragment, one, table};
use nu_protocol::{row, Value};
#[test]
fn render_h1() {
let value = row! {"H1".into() => Value::from("Ecuador")};
assert_eq!(fragment(&value, false), "# Ecuador\n");
}
#[test]
fn render_h2() {
let value = row! {"H2".into() => Value::from("Ecuador")};
assert_eq!(fragment(&value, false), "## Ecuador\n");
}
#[test]
fn render_h3() {
let value = row! {"H3".into() => Value::from("Ecuador")};
assert_eq!(fragment(&value, false), "### Ecuador\n");
}
#[test]
fn render_blockquote() {
let value = row! {"BLOCKQUOTE".into() => Value::from("Ecuador")};
assert_eq!(fragment(&value, false), "> Ecuador\n");
}
#[test]
fn render_table() {
let value = vec![
row! { "country".into() => Value::from("Ecuador")},
row! { "country".into() => Value::from("New Zealand")},
row! { "country".into() => Value::from("USA")},
];
assert_eq!(
table(&value, false),
one(r#"
|country|
|-|
|Ecuador|
|New Zealand|
|USA|
"#)
);
assert_eq!(
table(&value, true),
one(r#"
| country |
| ----------- |
| Ecuador |
| New Zealand |
| USA |
"#)
);
}
}

View File

@ -0,0 +1,22 @@
mod command;
pub(crate) mod csv;
pub(crate) mod delimited;
mod html;
mod json;
mod md;
pub(crate) mod toml;
mod tsv;
pub(crate) mod url;
mod xml;
mod yaml;
pub use self::csv::ToCsv;
pub use self::toml::ToToml;
pub use self::url::ToUrl;
pub use command::To;
pub use html::ToHtml;
pub use json::ToJson;
pub use md::Command as ToMarkdown;
pub use tsv::ToTsv;
pub use xml::ToXml;
pub use yaml::ToYaml;

View File

@ -0,0 +1,240 @@
use crate::prelude::*;
use nu_engine::WholeStreamCommand;
use nu_errors::{CoerceInto, ShellError};
use nu_protocol::{Primitive, Signature, UnspannedPathMember, UntaggedValue, Value};
pub struct ToToml;
impl WholeStreamCommand for ToToml {
fn name(&self) -> &str {
"to toml"
}
fn signature(&self) -> Signature {
Signature::build("to toml")
}
fn usage(&self) -> &str {
"Convert table into .toml text"
}
fn run(&self, args: CommandArgs) -> Result<OutputStream, ShellError> {
to_toml(args)
}
}
// Helper method to recursively convert nu_protocol::Value -> toml::Value
// This shouldn't be called at the top-level
fn helper(v: &Value) -> Result<toml::Value, ShellError> {
Ok(match &v.value {
UntaggedValue::Primitive(Primitive::Boolean(b)) => toml::Value::Boolean(*b),
UntaggedValue::Primitive(Primitive::Filesize(b)) => {
if let Some(value) = b.to_i64() {
toml::Value::Integer(value)
} else {
return Err(ShellError::labeled_error(
"Value too large to write to toml",
"value too large for toml",
v.tag.span,
));
}
}
UntaggedValue::Primitive(Primitive::Duration(i)) => toml::Value::String(i.to_string()),
UntaggedValue::Primitive(Primitive::Date(d)) => toml::Value::String(d.to_string()),
UntaggedValue::Primitive(Primitive::EndOfStream) => {
toml::Value::String("<End of Stream>".to_string())
}
UntaggedValue::Primitive(Primitive::BeginningOfStream) => {
toml::Value::String("<Beginning of Stream>".to_string())
}
UntaggedValue::Primitive(Primitive::Decimal(f)) => {
toml::Value::Float(f.tagged(&v.tag).coerce_into("converting to TOML float")?)
}
UntaggedValue::Primitive(Primitive::Int(i)) => toml::Value::Integer(*i),
UntaggedValue::Primitive(Primitive::BigInt(i)) => {
toml::Value::Integer(i.tagged(&v.tag).coerce_into("converting to TOML integer")?)
}
UntaggedValue::Primitive(Primitive::Nothing) => {
toml::Value::String("<Nothing>".to_string())
}
UntaggedValue::Primitive(Primitive::GlobPattern(s)) => toml::Value::String(s.clone()),
UntaggedValue::Primitive(Primitive::String(s)) => toml::Value::String(s.clone()),
UntaggedValue::Primitive(Primitive::FilePath(s)) => {
toml::Value::String(s.display().to_string())
}
UntaggedValue::Primitive(Primitive::ColumnPath(path)) => toml::Value::Array(
path.iter()
.map(|x| match &x.unspanned {
UnspannedPathMember::String(string) => Ok(toml::Value::String(string.clone())),
UnspannedPathMember::Int(int) => Ok(toml::Value::Integer(*int)),
})
.collect::<Result<Vec<toml::Value>, ShellError>>()?,
),
UntaggedValue::Table(l) => toml::Value::Array(collect_values(l)?),
UntaggedValue::Error(e) => return Err(e.clone()),
UntaggedValue::Block(_) => toml::Value::String("<Block>".to_string()),
#[cfg(feature = "dataframe")]
UntaggedValue::DataFrame(_) => toml::Value::String("<Data>".to_string()),
UntaggedValue::Primitive(Primitive::Range(_)) => toml::Value::String("<Range>".to_string()),
UntaggedValue::Primitive(Primitive::Binary(b)) => {
toml::Value::Array(b.iter().map(|x| toml::Value::Integer(*x as i64)).collect())
}
UntaggedValue::Row(o) => {
let mut m = toml::map::Map::new();
for (k, v) in o.entries.iter() {
m.insert(k.clone(), helper(v)?);
}
toml::Value::Table(m)
}
})
}
/// Converts a nu_protocol::Value into a toml::Value
/// Will return a Shell Error, if the Nu Value is not a valid top-level TOML Value
pub fn value_to_toml_value(v: &Value) -> Result<toml::Value, ShellError> {
match &v.value {
UntaggedValue::Row(o) => {
let mut m = toml::map::Map::new();
for (k, v) in o.entries.iter() {
m.insert(k.clone(), helper(v)?);
}
Ok(toml::Value::Table(m))
}
UntaggedValue::Primitive(Primitive::String(s)) => {
// Attempt to de-serialize the String
toml::de::from_str(s).map_err(|_| {
ShellError::labeled_error(
format!("{:?} unable to de-serialize string to TOML", s),
"invalid TOML",
v.tag(),
)
})
}
_ => Err(ShellError::labeled_error(
format!("{:?} is not a valid top-level TOML", v.value),
"invalid TOML",
v.tag(),
)),
}
}
fn collect_values(input: &[Value]) -> Result<Vec<toml::Value>, ShellError> {
let mut out = vec![];
for value in input {
out.push(helper(value)?);
}
Ok(out)
}
fn to_toml(args: CommandArgs) -> Result<OutputStream, ShellError> {
let name_tag = args.name_tag();
let name_span = name_tag.span;
let input: Vec<Value> = args.input.collect();
let to_process_input = match input.len() {
x if x > 1 => {
let tag = input[0].tag.clone();
vec![Value {
value: UntaggedValue::Table(input),
tag,
}]
}
1 => input,
_ => vec![],
};
Ok((to_process_input.into_iter().map(move |value| {
let value_span = value.tag.span;
match value_to_toml_value(&value) {
Ok(toml_value) => match toml::to_string(&toml_value) {
Ok(x) => UntaggedValue::Primitive(Primitive::String(x)).into_value(&name_tag),
_ => Value::error(ShellError::labeled_error_with_secondary(
"Expected a table with TOML-compatible structure.tag() from pipeline",
"requires TOML-compatible input",
name_span,
"originates from here".to_string(),
value_span,
)),
},
_ => Value::error(ShellError::labeled_error(
"Expected a table with TOML-compatible structure from pipeline",
"requires TOML-compatible input",
&name_tag,
)),
}
}))
.into_output_stream())
}
#[cfg(test)]
mod tests {
use super::ShellError;
use super::*;
use nu_protocol::Dictionary;
#[test]
fn examples_work_as_expected() -> Result<(), ShellError> {
use crate::examples::test as test_examples;
test_examples(ToToml {})
}
#[test]
fn test_value_to_toml_value() {
//
// Positive Tests
//
// Dictionary -> What we do in "crates/nu-cli/src/data/config.rs" to write the config file
let mut m = indexmap::IndexMap::new();
m.insert("rust".to_owned(), Value::from("editor"));
m.insert("is".to_owned(), Value::nothing());
m.insert(
"features".to_owned(),
UntaggedValue::Table(vec![
UntaggedValue::string("hello").into_untagged_value(),
UntaggedValue::string("array").into_untagged_value(),
])
.into_untagged_value(),
);
let tv = value_to_toml_value(&UntaggedValue::Row(Dictionary::new(m)).into_untagged_value())
.expect("Expected Ok from valid TOML dictionary");
assert_eq!(
tv.get("features"),
Some(&toml::Value::Array(vec![
toml::Value::String("hello".to_owned()),
toml::Value::String("array".to_owned())
]))
);
// TOML string
let tv = value_to_toml_value(&Value::from(
r#"
title = "TOML Example"
[owner]
name = "Tom Preston-Werner"
dob = 1979-05-27T07:32:00-08:00 # First class dates
[dependencies]
rustyline = "4.1.0"
sysinfo = "0.8.4"
chrono = { version = "0.4.6", features = ["serde"] }
"#,
))
.expect("Expected Ok from valid TOML string");
assert_eq!(
tv.get("title").unwrap(),
&toml::Value::String("TOML Example".to_owned())
);
//
// Negative Tests
//
value_to_toml_value(&Value::from("not_valid"))
.expect_err("Expected non-valid toml (String) to cause error!");
value_to_toml_value(&UntaggedValue::Table(vec![Value::from("1")]).into_untagged_value())
.expect_err("Expected non-valid toml (Table) to cause error!");
}
}

View File

@ -0,0 +1,49 @@
use crate::commands::formats::to::delimited::to_delimited_data;
use crate::prelude::*;
use nu_engine::WholeStreamCommand;
use nu_errors::ShellError;
use nu_protocol::Signature;
pub struct ToTsv;
impl WholeStreamCommand for ToTsv {
fn name(&self) -> &str {
"to tsv"
}
fn signature(&self) -> Signature {
Signature::build("to tsv").switch(
"noheaders",
"do not output the column names as the first row",
Some('n'),
)
}
fn usage(&self) -> &str {
"Convert table into .tsv text"
}
fn run(&self, args: CommandArgs) -> Result<OutputStream, ShellError> {
to_tsv(args)
}
}
fn to_tsv(args: CommandArgs) -> Result<OutputStream, ShellError> {
let name = args.call_info.name_tag.clone();
let noheaders = args.has_flag("noheaders");
to_delimited_data(noheaders, '\t', "TSV", args.input, name)
}
#[cfg(test)]
mod tests {
use super::ShellError;
use super::ToTsv;
#[test]
fn examples_work_as_expected() -> Result<(), ShellError> {
use crate::examples::test as test_examples;
test_examples(ToTsv {})
}
}

View File

@ -0,0 +1,85 @@
use crate::prelude::*;
use nu_engine::WholeStreamCommand;
use nu_errors::ShellError;
use nu_protocol::{Signature, UntaggedValue, Value};
pub struct ToUrl;
impl WholeStreamCommand for ToUrl {
fn name(&self) -> &str {
"to url"
}
fn signature(&self) -> Signature {
Signature::build("to url")
}
fn usage(&self) -> &str {
"Convert table into url-encoded text"
}
fn run(&self, args: CommandArgs) -> Result<OutputStream, ShellError> {
to_url(args)
}
}
fn to_url(args: CommandArgs) -> Result<OutputStream, ShellError> {
let tag = args.name_tag();
let input = args.input;
Ok(input
.map(move |value| match value {
Value {
value: UntaggedValue::Row(row),
..
} => {
let mut row_vec = vec![];
for (k, v) in row.entries {
match v.as_string() {
Ok(s) => {
row_vec.push((k.clone(), s.to_string()));
}
_ => {
return Value::error(ShellError::labeled_error_with_secondary(
"Expected table with string values",
"requires table with strings",
&tag,
"value originates from here",
v.tag,
));
}
}
}
match serde_urlencoded::to_string(row_vec) {
Ok(s) => UntaggedValue::string(s).into_value(&tag),
_ => Value::error(ShellError::labeled_error(
"Failed to convert to url-encoded",
"cannot url-encode",
&tag,
)),
}
}
Value { tag: value_tag, .. } => Value::error(ShellError::labeled_error_with_secondary(
"Expected a table from pipeline",
"requires table input",
&tag,
"value originates from here",
value_tag.span,
)),
})
.into_output_stream())
}
#[cfg(test)]
mod tests {
use super::ShellError;
use super::ToUrl;
#[test]
fn examples_work_as_expected() -> Result<(), ShellError> {
use crate::examples::test as test_examples;
test_examples(ToUrl {})
}
}

View File

@ -0,0 +1,196 @@
use crate::prelude::*;
use indexmap::IndexMap;
use nu_engine::WholeStreamCommand;
use nu_errors::ShellError;
use nu_protocol::{Primitive, Signature, SyntaxShape, UntaggedValue, Value};
use quick_xml::events::{BytesEnd, BytesStart, BytesText, Event};
use std::collections::HashSet;
use std::io::Cursor;
use std::io::Write;
pub struct ToXml;
impl WholeStreamCommand for ToXml {
fn name(&self) -> &str {
"to xml"
}
fn signature(&self) -> Signature {
Signature::build("to xml").named(
"pretty",
SyntaxShape::Int,
"Formats the XML text with the provided indentation setting",
Some('p'),
)
}
fn usage(&self) -> &str {
"Convert table into .xml text"
}
fn run(&self, args: CommandArgs) -> Result<OutputStream, ShellError> {
to_xml(args)
}
}
pub fn add_attributes<'a>(
element: &mut quick_xml::events::BytesStart<'a>,
attributes: &'a IndexMap<String, String>,
) {
for (k, v) in attributes.iter() {
element.push_attribute((k.as_str(), v.as_str()));
}
}
pub fn get_attributes(row: &Value) -> Option<IndexMap<String, String>> {
if let UntaggedValue::Row(r) = &row.value {
if let Some(v) = r.entries.get("attributes") {
if let UntaggedValue::Row(a) = &v.value {
let mut h = IndexMap::new();
for (k, v) in a.entries.iter() {
h.insert(k.clone(), v.convert_to_string());
}
return Some(h);
}
}
}
None
}
pub fn get_children(row: &Value) -> Option<&Vec<Value>> {
if let UntaggedValue::Row(r) = &row.value {
if let Some(v) = r.entries.get("children") {
if let UntaggedValue::Table(t) = &v.value {
return Some(t);
}
}
}
None
}
pub fn is_xml_row(row: &Value) -> bool {
if let UntaggedValue::Row(r) = &row.value {
let keys: HashSet<&String> = r.keys().collect();
let children: String = "children".to_string();
let attributes: String = "attributes".to_string();
return keys.contains(&children) && keys.contains(&attributes) && keys.len() == 2;
}
false
}
pub fn write_xml_events<W: Write>(
current: &Value,
writer: &mut quick_xml::Writer<W>,
) -> Result<(), ShellError> {
match &current.value {
UntaggedValue::Row(o) => {
for (k, v) in o.entries.iter() {
let mut e = BytesStart::owned(k.as_bytes(), k.len());
if !is_xml_row(v) {
return Err(ShellError::labeled_error(
"Expected a row with 'children' and 'attributes' columns",
"missing 'children' and 'attributes' columns ",
&current.tag,
));
}
let a = get_attributes(v);
if let Some(ref a) = a {
add_attributes(&mut e, a);
}
writer
.write_event(Event::Start(e))
.expect("Couldn't open XML node");
let c = get_children(v);
if let Some(c) = c {
for v in c {
write_xml_events(v, writer)?;
}
}
writer
.write_event(Event::End(BytesEnd::borrowed(k.as_bytes())))
.expect("Couldn't close XML node");
}
}
UntaggedValue::Table(t) => {
for v in t {
write_xml_events(v, writer)?;
}
}
_ => {
let s = current.convert_to_string();
writer
.write_event(Event::Text(BytesText::from_plain_str(s.as_str())))
.expect("Couldn't write XML text");
}
}
Ok(())
}
fn to_xml(args: CommandArgs) -> Result<OutputStream, ShellError> {
let name_tag = args.call_info.name_tag.clone();
let name_span = name_tag.span;
let pretty: Option<Value> = args.get_flag("pretty")?;
let input: Vec<Value> = args.input.collect();
let to_process_input = match input.len() {
x if x > 1 => {
let tag = input[0].tag.clone();
vec![Value {
value: UntaggedValue::Table(input),
tag,
}]
}
1 => input,
_ => vec![],
};
Ok((to_process_input.into_iter().map(move |value| {
let mut w = pretty.as_ref().map_or_else(
|| quick_xml::Writer::new(Cursor::new(Vec::new())),
|p| {
quick_xml::Writer::new_with_indent(
Cursor::new(Vec::new()),
b' ',
p.value.expect_int() as usize,
)
},
);
let value_span = value.tag.span;
match write_xml_events(&value, &mut w) {
Ok(_) => {
let b = w.into_inner().into_inner();
let s = if let Ok(s) = String::from_utf8(b) {
s
} else {
return Value::error(ShellError::untagged_runtime_error(
"Could not convert a string to utf-8",
));
};
UntaggedValue::Primitive(Primitive::String(s)).into_value(&name_tag)
}
Err(_) => Value::error(ShellError::labeled_error_with_secondary(
"Expected a table with XML-compatible structure from pipeline",
"requires XML-compatible input",
name_span,
"originates from here".to_string(),
value_span,
)),
}
}))
.into_output_stream())
}
#[cfg(test)]
mod tests {
use super::ShellError;
use super::ToXml;
#[test]
fn examples_work_as_expected() -> Result<(), ShellError> {
use crate::examples::test as test_examples;
test_examples(ToXml {})
}
}

View File

@ -0,0 +1,172 @@
use crate::prelude::*;
use nu_engine::WholeStreamCommand;
use nu_errors::{CoerceInto, ShellError};
use nu_protocol::{Primitive, Signature, UnspannedPathMember, UntaggedValue, Value};
pub struct ToYaml;
impl WholeStreamCommand for ToYaml {
fn name(&self) -> &str {
"to yaml"
}
fn signature(&self) -> Signature {
Signature::build("to yaml")
}
fn usage(&self) -> &str {
"Convert table into .yaml/.yml text"
}
fn run(&self, args: CommandArgs) -> Result<OutputStream, ShellError> {
to_yaml(args)
}
}
pub fn value_to_yaml_value(v: &Value) -> Result<serde_yaml::Value, ShellError> {
Ok(match &v.value {
UntaggedValue::Primitive(Primitive::Boolean(b)) => serde_yaml::Value::Bool(*b),
UntaggedValue::Primitive(Primitive::Filesize(b)) => {
serde_yaml::Value::Number(serde_yaml::Number::from(b.to_f64().ok_or_else(|| {
ShellError::labeled_error(
"Could not convert to bytes",
"could not convert to bytes",
&v.tag,
)
})?))
}
UntaggedValue::Primitive(Primitive::Duration(i)) => {
serde_yaml::Value::String(i.to_string())
}
UntaggedValue::Primitive(Primitive::Date(d)) => serde_yaml::Value::String(d.to_string()),
UntaggedValue::Primitive(Primitive::EndOfStream) => serde_yaml::Value::Null,
UntaggedValue::Primitive(Primitive::BeginningOfStream) => serde_yaml::Value::Null,
UntaggedValue::Primitive(Primitive::Decimal(f)) => {
serde_yaml::Value::Number(serde_yaml::Number::from(f.to_f64().ok_or_else(|| {
ShellError::labeled_error(
"Could not convert to decimal",
"could not convert to decimal",
&v.tag,
)
})?))
}
UntaggedValue::Primitive(Primitive::Int(i)) => {
serde_yaml::Value::Number(serde_yaml::Number::from(*i))
}
UntaggedValue::Primitive(Primitive::BigInt(i)) => {
serde_yaml::Value::Number(serde_yaml::Number::from(CoerceInto::<i64>::coerce_into(
i.tagged(&v.tag),
"converting to YAML number",
)?))
}
UntaggedValue::Primitive(Primitive::Nothing) => serde_yaml::Value::Null,
UntaggedValue::Primitive(Primitive::GlobPattern(s)) => serde_yaml::Value::String(s.clone()),
UntaggedValue::Primitive(Primitive::String(s)) => serde_yaml::Value::String(s.clone()),
UntaggedValue::Primitive(Primitive::ColumnPath(path)) => {
let mut out = vec![];
for member in path.iter() {
match &member.unspanned {
UnspannedPathMember::String(string) => {
out.push(serde_yaml::Value::String(string.clone()))
}
UnspannedPathMember::Int(int) => {
out.push(serde_yaml::Value::Number(serde_yaml::Number::from(*int)))
}
}
}
serde_yaml::Value::Sequence(out)
}
UntaggedValue::Primitive(Primitive::FilePath(s)) => {
serde_yaml::Value::String(s.display().to_string())
}
UntaggedValue::Table(l) => {
let mut out = vec![];
for value in l {
out.push(value_to_yaml_value(value)?);
}
serde_yaml::Value::Sequence(out)
}
UntaggedValue::Error(e) => return Err(e.clone()),
UntaggedValue::Block(_) | UntaggedValue::Primitive(Primitive::Range(_)) => {
serde_yaml::Value::Null
}
#[cfg(feature = "dataframe")]
UntaggedValue::DataFrame(_) => serde_yaml::Value::Null,
UntaggedValue::Primitive(Primitive::Binary(b)) => serde_yaml::Value::Sequence(
b.iter()
.map(|x| serde_yaml::Value::Number(serde_yaml::Number::from(*x)))
.collect(),
),
UntaggedValue::Row(o) => {
let mut m = serde_yaml::Mapping::new();
for (k, v) in o.entries.iter() {
m.insert(
serde_yaml::Value::String(k.clone()),
value_to_yaml_value(v)?,
);
}
serde_yaml::Value::Mapping(m)
}
})
}
fn to_yaml(args: CommandArgs) -> Result<OutputStream, ShellError> {
let name_tag = args.name_tag();
let name_span = name_tag.span;
let input: Vec<Value> = args.input.collect();
let to_process_input = match input.len() {
x if x > 1 => {
let tag = input[0].tag.clone();
vec![Value {
value: UntaggedValue::Table(input),
tag,
}]
}
1 => input,
_ => vec![],
};
Ok((to_process_input.into_iter().map(move |value| {
let value_span = value.tag.span;
match value_to_yaml_value(&value) {
Ok(yaml_value) => match serde_yaml::to_string(&yaml_value) {
Ok(x) => UntaggedValue::Primitive(Primitive::String(x)).into_value(&name_tag),
_ => Value::error(ShellError::labeled_error_with_secondary(
"Expected a table with YAML-compatible structure from pipeline",
"requires YAML-compatible input",
name_span,
"originates from here".to_string(),
value_span,
)),
},
_ => Value::error(ShellError::labeled_error(
"Expected a table with YAML-compatible structure from pipeline",
"requires YAML-compatible input",
&name_tag,
)),
}
}))
.into_output_stream())
}
#[cfg(test)]
mod tests {
use super::ShellError;
use super::ToYaml;
#[test]
fn examples_work_as_expected() -> Result<(), ShellError> {
use crate::examples::test as test_examples;
test_examples(ToYaml {})
}
}