mirror of
https://github.com/nushell/nushell
synced 2025-01-13 13:49:21 +00:00
Merge pull request #830 from t-hart/pull-req/from-master
[DRAFT] Adds `from-ssv` command.
This commit is contained in:
commit
bd6d8189f8
5 changed files with 247 additions and 0 deletions
|
@ -284,6 +284,7 @@ Nu adheres closely to a set of goals that make up its design philosophy. As feat
|
||||||
| from-ini | Parse text as .ini and create table |
|
| from-ini | Parse text as .ini and create table |
|
||||||
| from-json | Parse text as .json and create table |
|
| from-json | Parse text as .json and create table |
|
||||||
| from-sqlite | Parse binary data as sqlite .db and create table |
|
| from-sqlite | Parse binary data as sqlite .db and create table |
|
||||||
|
| from-ssv | Parse text as whitespace-separated values and create table|
|
||||||
| from-toml | Parse text as .toml and create table |
|
| from-toml | Parse text as .toml and create table |
|
||||||
| from-tsv | Parse text as .tsv and create table |
|
| from-tsv | Parse text as .tsv and create table |
|
||||||
| from-url | Parse urlencoded string and create a table |
|
| from-url | Parse urlencoded string and create a table |
|
||||||
|
|
|
@ -282,6 +282,7 @@ pub async fn cli() -> Result<(), Box<dyn Error>> {
|
||||||
whole_stream_command(Env),
|
whole_stream_command(Env),
|
||||||
whole_stream_command(FromCSV),
|
whole_stream_command(FromCSV),
|
||||||
whole_stream_command(FromTSV),
|
whole_stream_command(FromTSV),
|
||||||
|
whole_stream_command(FromSSV),
|
||||||
whole_stream_command(FromINI),
|
whole_stream_command(FromINI),
|
||||||
whole_stream_command(FromBSON),
|
whole_stream_command(FromBSON),
|
||||||
whole_stream_command(FromJSON),
|
whole_stream_command(FromJSON),
|
||||||
|
|
|
@ -22,6 +22,7 @@ pub(crate) mod from_csv;
|
||||||
pub(crate) mod from_ini;
|
pub(crate) mod from_ini;
|
||||||
pub(crate) mod from_json;
|
pub(crate) mod from_json;
|
||||||
pub(crate) mod from_sqlite;
|
pub(crate) mod from_sqlite;
|
||||||
|
pub(crate) mod from_ssv;
|
||||||
pub(crate) mod from_toml;
|
pub(crate) mod from_toml;
|
||||||
pub(crate) mod from_tsv;
|
pub(crate) mod from_tsv;
|
||||||
pub(crate) mod from_url;
|
pub(crate) mod from_url;
|
||||||
|
@ -92,6 +93,7 @@ pub(crate) use from_ini::FromINI;
|
||||||
pub(crate) use from_json::FromJSON;
|
pub(crate) use from_json::FromJSON;
|
||||||
pub(crate) use from_sqlite::FromDB;
|
pub(crate) use from_sqlite::FromDB;
|
||||||
pub(crate) use from_sqlite::FromSQLite;
|
pub(crate) use from_sqlite::FromSQLite;
|
||||||
|
pub(crate) use from_ssv::FromSSV;
|
||||||
pub(crate) use from_toml::FromTOML;
|
pub(crate) use from_toml::FromTOML;
|
||||||
pub(crate) use from_tsv::FromTSV;
|
pub(crate) use from_tsv::FromTSV;
|
||||||
pub(crate) use from_url::FromURL;
|
pub(crate) use from_url::FromURL;
|
||||||
|
|
187
src/commands/from_ssv.rs
Normal file
187
src/commands/from_ssv.rs
Normal file
|
@ -0,0 +1,187 @@
|
||||||
|
use crate::commands::WholeStreamCommand;
|
||||||
|
use crate::data::{Primitive, TaggedDictBuilder, Value};
|
||||||
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
pub struct FromSSV;
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct FromSSVArgs {
|
||||||
|
headerless: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
const STRING_REPRESENTATION: &str = "from-ssv";
|
||||||
|
|
||||||
|
impl WholeStreamCommand for FromSSV {
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
STRING_REPRESENTATION
|
||||||
|
}
|
||||||
|
|
||||||
|
fn signature(&self) -> Signature {
|
||||||
|
Signature::build(STRING_REPRESENTATION).switch("headerless")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn usage(&self) -> &str {
|
||||||
|
"Parse text as whitespace-separated values and create a table."
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(
|
||||||
|
&self,
|
||||||
|
args: CommandArgs,
|
||||||
|
registry: &CommandRegistry,
|
||||||
|
) -> Result<OutputStream, ShellError> {
|
||||||
|
args.process(registry, from_ssv)?.run()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn string_to_table(s: &str, headerless: bool) -> Option<Vec<Vec<(String, String)>>> {
|
||||||
|
let mut lines = s.lines().filter(|l| !l.trim().is_empty());
|
||||||
|
|
||||||
|
let headers = lines
|
||||||
|
.next()?
|
||||||
|
.split_whitespace()
|
||||||
|
.map(|s| s.to_owned())
|
||||||
|
.collect::<Vec<String>>();
|
||||||
|
|
||||||
|
let header_row = if headerless {
|
||||||
|
(1..=headers.len())
|
||||||
|
.map(|i| format!("Column{}", i))
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
} else {
|
||||||
|
headers
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(
|
||||||
|
lines
|
||||||
|
.map(|l| {
|
||||||
|
header_row
|
||||||
|
.iter()
|
||||||
|
.zip(l.split_whitespace())
|
||||||
|
.map(|(a, b)| (String::from(a), String::from(b)))
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_ssv_string_to_value(
|
||||||
|
s: &str,
|
||||||
|
headerless: bool,
|
||||||
|
tag: impl Into<Tag>,
|
||||||
|
) -> Option<Tagged<Value>> {
|
||||||
|
let tag = tag.into();
|
||||||
|
let rows = string_to_table(s, headerless)?
|
||||||
|
.iter()
|
||||||
|
.map(|row| {
|
||||||
|
let mut tagged_dict = TaggedDictBuilder::new(&tag);
|
||||||
|
for (col, entry) in row {
|
||||||
|
tagged_dict.insert_tagged(
|
||||||
|
col,
|
||||||
|
Value::Primitive(Primitive::String(String::from(entry))).tagged(&tag),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
tagged_dict.into_tagged_value()
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Some(Value::Table(rows).tagged(&tag))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_ssv(
|
||||||
|
FromSSVArgs { headerless }: FromSSVArgs,
|
||||||
|
RunnableContext { input, name, .. }: RunnableContext,
|
||||||
|
) -> Result<OutputStream, ShellError> {
|
||||||
|
let stream = async_stream! {
|
||||||
|
let values: Vec<Tagged<Value>> = input.values.collect().await;
|
||||||
|
let mut concat_string = String::new();
|
||||||
|
let mut latest_tag: Option<Tag> = None;
|
||||||
|
|
||||||
|
for value in values {
|
||||||
|
let value_tag = value.tag();
|
||||||
|
latest_tag = Some(value_tag.clone());
|
||||||
|
match value.item {
|
||||||
|
Value::Primitive(Primitive::String(s)) => {
|
||||||
|
concat_string.push_str(&s);
|
||||||
|
}
|
||||||
|
_ => yield Err(ShellError::labeled_error_with_secondary (
|
||||||
|
"Expected a string from pipeline",
|
||||||
|
"requires string input",
|
||||||
|
&name,
|
||||||
|
"value originates from here",
|
||||||
|
&value_tag
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match from_ssv_string_to_value(&concat_string, headerless, name.clone()) {
|
||||||
|
Some(x) => match x {
|
||||||
|
Tagged { item: Value::Table(list), ..} => {
|
||||||
|
for l in list { yield ReturnSuccess::value(l) }
|
||||||
|
}
|
||||||
|
x => yield ReturnSuccess::value(x)
|
||||||
|
},
|
||||||
|
None => if let Some(tag) = latest_tag {
|
||||||
|
yield Err(ShellError::labeled_error_with_secondary(
|
||||||
|
"Could not parse as SSV",
|
||||||
|
"input cannot be parsed ssv",
|
||||||
|
&name,
|
||||||
|
"value originates from here",
|
||||||
|
&tag,
|
||||||
|
))
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(stream.to_output_stream())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
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);
|
||||||
|
assert_eq!(
|
||||||
|
result,
|
||||||
|
Some(vec![
|
||||||
|
vec![owned("a", "1"), owned("b", "2")],
|
||||||
|
vec![owned("a", "3"), owned("b", "4")]
|
||||||
|
])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn it_ignores_headers_when_headerless() {
|
||||||
|
let input = r#"
|
||||||
|
a b
|
||||||
|
1 2
|
||||||
|
3 4
|
||||||
|
"#;
|
||||||
|
let result = string_to_table(input, true);
|
||||||
|
assert_eq!(
|
||||||
|
result,
|
||||||
|
Some(vec![
|
||||||
|
vec![owned("Column1", "1"), owned("Column2", "2")],
|
||||||
|
vec![owned("Column1", "3"), owned("Column2", "4")]
|
||||||
|
])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn it_returns_none_given_an_empty_string() {
|
||||||
|
let input = "";
|
||||||
|
let result = string_to_table(input, true);
|
||||||
|
assert_eq!(result, None);
|
||||||
|
}
|
||||||
|
}
|
|
@ -355,6 +355,62 @@ fn converts_from_tsv_text_skipping_headers_to_structured_table() {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn converts_from_ssv_text_to_structured_table() {
|
||||||
|
Playground::setup("filter_from_ssv_test_1", |dirs, sandbox| {
|
||||||
|
sandbox.with_files(vec![FileWithContentToBeTrimmed(
|
||||||
|
"oc_get_svc.txt",
|
||||||
|
r#"
|
||||||
|
NAME LABELS SELECTOR IP PORT(S)
|
||||||
|
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 actual = nu!(
|
||||||
|
cwd: dirs.test(), h::pipeline(
|
||||||
|
r#"
|
||||||
|
open oc_get_svc.txt
|
||||||
|
| from-ssv
|
||||||
|
| nth 0
|
||||||
|
| get IP
|
||||||
|
| echo $it
|
||||||
|
"#
|
||||||
|
));
|
||||||
|
|
||||||
|
assert_eq!(actual, "172.30.78.158");
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn converts_from_ssv_text_skipping_headers_to_structured_table() {
|
||||||
|
Playground::setup("filter_from_ssv_test_2", |dirs, sandbox| {
|
||||||
|
sandbox.with_files(vec![FileWithContentToBeTrimmed(
|
||||||
|
"oc_get_svc.txt",
|
||||||
|
r#"
|
||||||
|
NAME LABELS SELECTOR IP PORT(S)
|
||||||
|
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 actual = nu!(
|
||||||
|
cwd: dirs.test(), h::pipeline(
|
||||||
|
r#"
|
||||||
|
open oc_get_svc.txt
|
||||||
|
| from-ssv --headerless
|
||||||
|
| nth 2
|
||||||
|
| get Column2
|
||||||
|
| echo $it
|
||||||
|
"#
|
||||||
|
));
|
||||||
|
|
||||||
|
assert_eq!(actual, "component=apiserver,provider=kubernetes");
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn can_convert_table_to_bson_and_back_into_table() {
|
fn can_convert_table_to_bson_and_back_into_table() {
|
||||||
let actual = nu!(
|
let actual = nu!(
|
||||||
|
|
Loading…
Reference in a new issue