forked from extern/nushell
Begin directory contrib docs and split commands (#3650)
* Begin directory contrib docs and split commands * Fix unused import warning
This commit is contained in:
39
crates/nu-command/src/commands/formats/from/command.rs
Normal file
39
crates/nu-command/src/commands/formats/from/command.rs
Normal 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 {})
|
||||
}
|
||||
}
|
107
crates/nu-command/src/commands/formats/from/csv.rs
Normal file
107
crates/nu-command/src/commands/formats/from/csv.rs
Normal 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 {})
|
||||
}
|
||||
}
|
112
crates/nu-command/src/commands/formats/from/delimited.rs
Normal file
112
crates/nu-command/src/commands/formats/from/delimited.rs
Normal 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,
|
||||
}
|
||||
}
|
125
crates/nu-command/src/commands/formats/from/eml.rs
Normal file
125
crates/nu-command/src/commands/formats/from/eml.rs
Normal 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 {})
|
||||
}
|
||||
}
|
250
crates/nu-command/src/commands/formats/from/ics.rs
Normal file
250
crates/nu-command/src/commands/formats/from/ics.rs
Normal 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 {})
|
||||
}
|
||||
}
|
96
crates/nu-command/src/commands/formats/from/ini.rs
Normal file
96
crates/nu-command/src/commands/formats/from/ini.rs
Normal 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 {})
|
||||
}
|
||||
}
|
140
crates/nu-command/src/commands/formats/from/json.rs
Normal file
140
crates/nu-command/src/commands/formats/from/json.rs
Normal 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 {})
|
||||
}
|
||||
}
|
32
crates/nu-command/src/commands/formats/from/mod.rs
Normal file
32
crates/nu-command/src/commands/formats/from/mod.rs
Normal 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};
|
120
crates/nu-command/src/commands/formats/from/ods.rs
Normal file
120
crates/nu-command/src/commands/formats/from/ods.rs
Normal 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 {})
|
||||
}
|
||||
}
|
480
crates/nu-command/src/commands/formats/from/ssv.rs
Normal file
480
crates/nu-command/src/commands/formats/from/ssv.rs
Normal 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 {})
|
||||
}
|
||||
}
|
101
crates/nu-command/src/commands/formats/from/toml.rs
Normal file
101
crates/nu-command/src/commands/formats/from/toml.rs
Normal 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 {})
|
||||
}
|
||||
}
|
50
crates/nu-command/src/commands/formats/from/tsv.rs
Normal file
50
crates/nu-command/src/commands/formats/from/tsv.rs
Normal 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 {})
|
||||
}
|
||||
}
|
65
crates/nu-command/src/commands/formats/from/url.rs
Normal file
65
crates/nu-command/src/commands/formats/from/url.rs
Normal 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 {})
|
||||
}
|
||||
}
|
106
crates/nu-command/src/commands/formats/from/vcf.rs
Normal file
106
crates/nu-command/src/commands/formats/from/vcf.rs
Normal 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 {})
|
||||
}
|
||||
}
|
127
crates/nu-command/src/commands/formats/from/xlsx.rs
Normal file
127
crates/nu-command/src/commands/formats/from/xlsx.rs
Normal 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 {})
|
||||
}
|
||||
}
|
302
crates/nu-command/src/commands/formats/from/xml.rs
Normal file
302
crates/nu-command/src/commands/formats/from/xml.rs
Normal 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 {})
|
||||
}
|
||||
}
|
205
crates/nu-command/src/commands/formats/from/yaml.rs
Normal file
205
crates/nu-command/src/commands/formats/from/yaml.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
5
crates/nu-command/src/commands/formats/mod.rs
Normal file
5
crates/nu-command/src/commands/formats/mod.rs
Normal file
@ -0,0 +1,5 @@
|
||||
mod from;
|
||||
mod to;
|
||||
|
||||
pub use from::*;
|
||||
pub use to::*;
|
40
crates/nu-command/src/commands/formats/to/command.rs
Normal file
40
crates/nu-command/src/commands/formats/to/command.rs
Normal 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 {})
|
||||
}
|
||||
}
|
80
crates/nu-command/src/commands/formats/to/csv.rs
Normal file
80
crates/nu-command/src/commands/formats/to/csv.rs
Normal 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 {})
|
||||
}
|
||||
}
|
217
crates/nu-command/src/commands/formats/to/delimited.rs
Normal file
217
crates/nu-command/src/commands/formats/to/delimited.rs
Normal 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())
|
||||
}
|
742
crates/nu-command/src/commands/formats/to/html.rs
Normal file
742
crates/nu-command/src/commands/formats/to/html.rs
Normal 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(®ex_hm, &output_string);
|
||||
} else if no_color {
|
||||
setup_no_color_regexes(&mut regex_hm);
|
||||
output_string = run_regexes(®ex_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 {})
|
||||
}
|
||||
}
|
258
crates/nu-command/src/commands/formats/to/json.rs
Normal file
258
crates/nu-command/src/commands/formats/to/json.rs
Normal 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 {})
|
||||
}
|
||||
}
|
380
crates/nu-command/src/commands/formats/to/md.rs
Normal file
380
crates/nu-command/src/commands/formats/to/md.rs
Normal 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 |
|
||||
"#)
|
||||
);
|
||||
}
|
||||
}
|
22
crates/nu-command/src/commands/formats/to/mod.rs
Normal file
22
crates/nu-command/src/commands/formats/to/mod.rs
Normal 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;
|
240
crates/nu-command/src/commands/formats/to/toml.rs
Normal file
240
crates/nu-command/src/commands/formats/to/toml.rs
Normal 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!");
|
||||
}
|
||||
}
|
49
crates/nu-command/src/commands/formats/to/tsv.rs
Normal file
49
crates/nu-command/src/commands/formats/to/tsv.rs
Normal 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 {})
|
||||
}
|
||||
}
|
85
crates/nu-command/src/commands/formats/to/url.rs
Normal file
85
crates/nu-command/src/commands/formats/to/url.rs
Normal 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 {})
|
||||
}
|
||||
}
|
196
crates/nu-command/src/commands/formats/to/xml.rs
Normal file
196
crates/nu-command/src/commands/formats/to/xml.rs
Normal 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 ¤t.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 ",
|
||||
¤t.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 {})
|
||||
}
|
||||
}
|
172
crates/nu-command/src/commands/formats/to/yaml.rs
Normal file
172
crates/nu-command/src/commands/formats/to/yaml.rs
Normal 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 {})
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user