Files
nushell/crates/nu-command/src/formats/to/csv.rs
132ikl f4136aa3f4 Add pipeline span to metadata (#16014)
# Description

This PR makes the span of a pipeline accessible through `metadata`,
meaning it's possible to get the span of a pipeline without collecting
it.

Examples:
```nushell
ls | metadata
# => ╭────────┬────────────────────╮
# => │        │ ╭───────┬────────╮ │
# => │ span   │ │ start │ 170218 │ │
# => │        │ │ end   │ 170220 │ │
# => │        │ ╰───────┴────────╯ │
# => │ source │ ls                 │
# => ╰────────┴────────────────────╯
```

```nushell
ls | metadata access {|meta|
  error make {msg: "error", label: {text: "here", span: $meta.span}}
}
# => Error:   × error
# =>    ╭─[entry #7:1:1]
# =>  1 │ ls | metadata access {|meta|
# =>    · ─┬
# =>    ·  ╰── here
# =>  2 │   error make {msg: "error", label: {text: "here", span: $meta.span}}
# =>    ╰────
```

Here's an example that wouldn't be possible before, since you would have
to use `metadata $in` to get the span, collecting the (infinite) stream

```nushell
generate {|x=0| {out: 0, next: 0} } | metadata access {|meta|
  # do whatever with stream
  error make {msg: "error", label: {text: "here", span: $meta.span}}
}
# => Error:   × error
# =>    ╭─[entry #16:1:1]
# =>  1 │ generate {|x=0| {out: 0, next: 0} } | metadata access {|meta|
# =>    · ────┬───
# =>    ·     ╰── here
# =>  2 │   # do whatever with stream
# =>    ╰────
```

I haven't done the tests or anything yet since I'm not sure how we feel
about having this as part of the normal metadata, rather than a new
command like `metadata span` or something. We could also have a
`metadata access` like functionality for that with an optional closure
argument potentially.

# User-Facing Changes

* The span of a pipeline is now available through `metadata` and
`metadata access` without collecting a stream.

# Tests + Formatting

TODO

# After Submitting

N/A
2025-06-30 23:17:43 +02:00

181 lines
5.4 KiB
Rust

use std::sync::Arc;
use crate::formats::to::delimited::to_delimited_data;
use nu_engine::command_prelude::*;
use nu_protocol::Config;
use super::delimited::ToDelimitedDataArgs;
#[derive(Clone)]
pub struct ToCsv;
impl Command for ToCsv {
fn name(&self) -> &str {
"to csv"
}
fn signature(&self) -> Signature {
Signature::build("to csv")
.input_output_types(vec![
(Type::record(), Type::String),
(Type::table(), Type::String),
])
.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'),
)
.named(
"columns",
SyntaxShape::List(SyntaxShape::String.into()),
"the names (in order) of the columns to use",
None,
)
.category(Category::Formats)
}
fn examples(&self) -> Vec<Example> {
vec![
Example {
description: "Outputs a CSV string representing the contents of this table",
example: "[[foo bar]; [1 2]] | to csv",
result: Some(Value::test_string("foo,bar\n1,2\n")),
},
Example {
description: "Outputs a CSV string representing the contents of this table",
example: "[[foo bar]; [1 2]] | to csv --separator ';' ",
result: Some(Value::test_string("foo;bar\n1;2\n")),
},
Example {
description: "Outputs a CSV string representing the contents of this record",
example: "{a: 1 b: 2} | to csv",
result: Some(Value::test_string("a,b\n1,2\n")),
},
Example {
description: "Outputs a CSV stream with column names pre-determined",
example: "[[foo bar baz]; [1 2 3]] | to csv --columns [baz foo]",
result: Some(Value::test_string("baz,foo\n3,1\n")),
},
]
}
fn description(&self) -> &str {
"Convert table into .csv text ."
}
fn run(
&self,
engine_state: &EngineState,
stack: &mut Stack,
call: &Call,
input: PipelineData,
) -> Result<PipelineData, ShellError> {
let head = call.head;
let noheaders = call.has_flag(engine_state, stack, "noheaders")?;
let separator: Option<Spanned<String>> = call.get_flag(engine_state, stack, "separator")?;
let columns: Option<Vec<String>> = call.get_flag(engine_state, stack, "columns")?;
let config = engine_state.config.clone();
to_csv(input, noheaders, separator, columns, head, config)
}
}
fn to_csv(
input: PipelineData,
noheaders: bool,
separator: Option<Spanned<String>>,
columns: Option<Vec<String>>,
head: Span,
config: Arc<Config>,
) -> Result<PipelineData, ShellError> {
let sep = match separator {
Some(Spanned { item: s, span, .. }) => {
if s == r"\t" {
Spanned { item: '\t', span }
} else {
let vec_s: Vec<char> = s.chars().collect();
if vec_s.len() != 1 {
return Err(ShellError::TypeMismatch {
err_message: "Expected a single separator char from --separator"
.to_string(),
span,
});
};
Spanned {
item: vec_s[0],
span: head,
}
}
}
_ => Spanned {
item: ',',
span: head,
},
};
to_delimited_data(
ToDelimitedDataArgs {
noheaders,
separator: sep,
columns,
format_name: "CSV",
input,
head,
content_type: Some(mime::TEXT_CSV.to_string()),
},
config,
)
}
#[cfg(test)]
mod test {
use nu_cmd_lang::eval_pipeline_without_terminal_expression;
use crate::{Get, Metadata};
use super::*;
#[test]
fn test_examples() {
use crate::test_examples;
test_examples(ToCsv {})
}
#[test]
fn test_content_type_metadata() {
let mut engine_state = Box::new(EngineState::new());
let delta = {
// Base functions that are needed for testing
// Try to keep this working set small to keep tests running as fast as possible
let mut working_set = StateWorkingSet::new(&engine_state);
working_set.add_decl(Box::new(ToCsv {}));
working_set.add_decl(Box::new(Metadata {}));
working_set.add_decl(Box::new(Get {}));
working_set.render()
};
engine_state
.merge_delta(delta)
.expect("Error merging delta");
let cmd = "{a: 1 b: 2} | to csv | metadata | get content_type | $in";
let result = eval_pipeline_without_terminal_expression(
cmd,
std::env::temp_dir().as_ref(),
&mut engine_state,
);
assert_eq!(
Value::test_string("text/csv"),
result.expect("There should be a result")
);
}
}