Merge pull request #943 from drmason13/from_csv-add-separator-arg

Add --separator argument to from_csv
This commit is contained in:
Jonathan Turner 2019-11-09 15:09:32 -08:00 committed by GitHub
commit 9043970e97
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 122 additions and 1 deletions

View file

@ -3,7 +3,9 @@
Converts csv data into table. Use this when nushell cannot dertermine the input file extension. Converts csv data into table. Use this when nushell cannot dertermine the input file extension.
## Example ## Example
Let's say we have the following file : Let's say we have the following file :
```shell ```shell
> cat pets.txt > cat pets.txt
animal, name, age animal, name, age
@ -36,6 +38,7 @@ To get a table from `pets.txt` we need to use the `from-csv` command :
``` ```
To ignore the csv headers use `--headerless` : To ignore the csv headers use `--headerless` :
```shell ```shell
━━━┯━━━━━━━━━━━┯━━━━━━━━━┯━━━━━━━━━ ━━━┯━━━━━━━━━━━┯━━━━━━━━━┯━━━━━━━━━
# │ Column1 │ Column2 │ Column3 # │ Column1 │ Column2 │ Column3
@ -45,3 +48,65 @@ To ignore the csv headers use `--headerless` :
━━━┷━━━━━━━━━━━┷━━━━━━━━━┷━━━━━━━━━ ━━━┷━━━━━━━━━━━┷━━━━━━━━━┷━━━━━━━━━
``` ```
To split on a character other than ',' use `--separator` :
```shell
> open pets.txt
animal; name; age
cat; Tom; 7
dog; Alfred; 10
chameleon; Linda; 1
```
```shell
> open pets.txt | from-csv --separator ';'
━━━┯━━━━━━━━━━━┯━━━━━━━━━┯━━━━━━
# │ animal │ name │ age
───┼───────────┼─────────┼──────
0 │ cat │ Tom │ 7
1 │ dog │ Alfred │ 10
2 │ chameleon │ Linda │ 1
━━━┷━━━━━━━━━━━┷━━━━━━━━━┷━━━━━━
```
To use this command to open a csv with separators other than a comma, use the `--raw` switch of `open` to open the csv, othewise the csv will enter `from-csv` as a table split on commas rather than raw text.
```shell
> mv pets.txt pets.csv
> open pets.csv | from-csv --separator ';'
error: Expected a string from pipeline
- shell:1:16
1 | open pets.csv | from-csv --separator ';'
| ^^^^^^^^ requires string input
- shell:1:0
1 | open pets.csv | from-csv --separator ';'
| value originates from here
> open pets.csv --raw | from-csv --separator ';'
━━━┯━━━━━━━━━━━┯━━━━━━━━━┯━━━━━━
# │ animal │ name │ age
───┼───────────┼─────────┼──────
0 │ cat │ Tom │ 7
1 │ dog │ Alfred │ 10
2 │ chameleon │ Linda │ 1
━━━┷━━━━━━━━━━━┷━━━━━━━━━┷━━━━━━
```
Note that separators are currently provided as strings and need to be wrapped in quotes.
```shell
> open pets.csv --raw | from-csv --separator ;
- shell:1:43
1 | open pets.csv --raw | from-csv --separator ;
| ^
```
It is also considered an error to use a separator greater than one char :
```shell
> open pets.txt | from-csv --separator '123'
error: Expected a single separator char from --separator
- shell:1:37
1 | open pets.txt | from-csv --separator '123'
| ^^^^^ requires a single character string input
```

View file

@ -8,6 +8,7 @@ pub struct FromCSV;
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct FromCSVArgs { pub struct FromCSVArgs {
headerless: bool, headerless: bool,
separator: Option<Tagged<Value>>,
} }
impl WholeStreamCommand for FromCSV { impl WholeStreamCommand for FromCSV {
@ -17,6 +18,11 @@ impl WholeStreamCommand for FromCSV {
fn signature(&self) -> Signature { fn signature(&self) -> Signature {
Signature::build("from-csv") Signature::build("from-csv")
.named(
"separator",
SyntaxShape::String,
"a character to separate columns, defaults to ','",
)
.switch("headerless", "don't treat the first row as column names") .switch("headerless", "don't treat the first row as column names")
} }
@ -36,10 +42,12 @@ impl WholeStreamCommand for FromCSV {
pub fn from_csv_string_to_value( pub fn from_csv_string_to_value(
s: String, s: String,
headerless: bool, headerless: bool,
separator: char,
tag: impl Into<Tag>, tag: impl Into<Tag>,
) -> Result<Tagged<Value>, csv::Error> { ) -> Result<Tagged<Value>, csv::Error> {
let mut reader = ReaderBuilder::new() let mut reader = ReaderBuilder::new()
.has_headers(false) .has_headers(false)
.delimiter(separator as u8)
.from_reader(s.as_bytes()); .from_reader(s.as_bytes());
let tag = tag.into(); let tag = tag.into();
@ -84,10 +92,29 @@ pub fn from_csv_string_to_value(
fn from_csv( fn from_csv(
FromCSVArgs { FromCSVArgs {
headerless: skip_headers, headerless: skip_headers,
separator,
}: FromCSVArgs, }: FromCSVArgs,
RunnableContext { input, name, .. }: RunnableContext, RunnableContext { input, name, .. }: RunnableContext,
) -> Result<OutputStream, ShellError> { ) -> Result<OutputStream, ShellError> {
let name_tag = name; let name_tag = name;
let sep = match separator {
Some(Tagged {
item: Value::Primitive(Primitive::String(s)),
tag,
..
}) => {
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]
}
_ => ',',
};
let stream = async_stream! { let stream = async_stream! {
let values: Vec<Tagged<Value>> = input.values.collect().await; let values: Vec<Tagged<Value>> = input.values.collect().await;
@ -114,7 +141,7 @@ fn from_csv(
} }
} }
match from_csv_string_to_value(concat_string, skip_headers, name_tag.clone()) { match from_csv_string_to_value(concat_string, skip_headers, sep, name_tag.clone()) {
Ok(x) => match x { Ok(x) => match x {
Tagged { item: Value::Table(list), .. } => { Tagged { item: Value::Table(list), .. } => {
for l in list { for l in list {

View file

@ -100,6 +100,35 @@ fn converts_from_csv_text_to_structured_table() {
}) })
} }
#[test]
fn converts_from_csv_text_with_separator_to_structured_table() {
Playground::setup("filter_from_csv_test_1", |dirs, sandbox| {
sandbox.with_files(vec![FileWithContentToBeTrimmed(
"los_tres_caballeros.txt",
r#"
first_name;last_name;rusty_luck
Andrés;Robalino;1
Jonathan;Turner;1
Yehuda;Katz;1
"#,
)]);
let actual = nu!(
cwd: dirs.test(), h::pipeline(
r#"
open los_tres_caballeros.txt
| from-csv --separator ';'
| get rusty_luck
| str --to-int
| sum
| echo $it
"#
));
assert_eq!(actual, "3");
})
}
#[test] #[test]
fn converts_from_csv_text_skipping_headers_to_structured_table() { fn converts_from_csv_text_skipping_headers_to_structured_table() {
Playground::setup("filter_from_csv_test_2", |dirs, sandbox| { Playground::setup("filter_from_csv_test_2", |dirs, sandbox| {