mirror of
https://github.com/nushell/nushell
synced 2025-01-10 04:09:09 +00:00
319930a1b9
* Add streaming support to save for ExternalStream data Prior to this change, save would collect data from an ExternalStream (data originating from externals) consuming memory for the full amount of data piped to it, This change adds streaming support for ExternalStream allowing saving of arbitrarily large files and bounding memory usage. * Remove broken save test This test passes but not for the right reasons, since this test was written filename has become a required parameter. The parser outputs an error but the test still passes as is checking the original un-mutated file assuming save has re-written the contents. This change removes the test. ``` running 1 test === stderr Error: nu::parser::missing_positional (https://docs.rs/nu-parser/0.60.0/nu-parser/enum.ParseError.html#variant.MissingPositional) × Missing required positional argument. ╭─[source:1:1] 1 │ open save_test_1/cargo_sample.toml | save · ▲ · ╰── missing filename ╰──── help: Usage: save {flags} <filename> test commands::save::figures_out_intelligently_where_to_write_out_with_metadata ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 515 filtered out; finished in 0.10s ```
211 lines
7.3 KiB
Rust
211 lines
7.3 KiB
Rust
use nu_engine::CallExt;
|
|
use nu_protocol::ast::Call;
|
|
use nu_protocol::engine::{Command, EngineState, Stack};
|
|
use nu_protocol::{
|
|
Category, Example, PipelineData, ShellError, Signature, Spanned, SyntaxShape, Value,
|
|
};
|
|
use std::io::{BufWriter, Write};
|
|
|
|
use std::path::Path;
|
|
|
|
#[derive(Clone)]
|
|
pub struct Save;
|
|
|
|
impl Command for Save {
|
|
fn name(&self) -> &str {
|
|
"save"
|
|
}
|
|
|
|
fn usage(&self) -> &str {
|
|
"Save a file."
|
|
}
|
|
|
|
fn signature(&self) -> nu_protocol::Signature {
|
|
Signature::build("save")
|
|
.required("filename", SyntaxShape::Filepath, "the filename to use")
|
|
.switch("raw", "save file as raw binary", Some('r'))
|
|
.switch("append", "append input to the end of the file", None)
|
|
.category(Category::FileSystem)
|
|
}
|
|
|
|
fn run(
|
|
&self,
|
|
engine_state: &EngineState,
|
|
stack: &mut Stack,
|
|
call: &Call,
|
|
input: PipelineData,
|
|
) -> Result<nu_protocol::PipelineData, nu_protocol::ShellError> {
|
|
let raw = call.has_flag("raw");
|
|
let append = call.has_flag("append");
|
|
|
|
let span = call.head;
|
|
|
|
let path = call.req::<Spanned<String>>(engine_state, stack, 0)?;
|
|
let arg_span = path.span;
|
|
let path = Path::new(&path.item);
|
|
|
|
let file = match (append, path.exists()) {
|
|
(true, true) => std::fs::OpenOptions::new()
|
|
.write(true)
|
|
.append(true)
|
|
.open(path),
|
|
_ => std::fs::File::create(path),
|
|
};
|
|
|
|
let mut file = match file {
|
|
Ok(file) => file,
|
|
Err(err) => {
|
|
return Ok(PipelineData::Value(
|
|
Value::Error {
|
|
error: ShellError::SpannedLabeledError(
|
|
"Permission denied".into(),
|
|
err.to_string(),
|
|
arg_span,
|
|
),
|
|
},
|
|
None,
|
|
));
|
|
}
|
|
};
|
|
|
|
let ext = if raw {
|
|
None
|
|
} else {
|
|
path.extension()
|
|
.map(|name| name.to_string_lossy().to_string())
|
|
};
|
|
|
|
if let Some(ext) = ext {
|
|
let output = match engine_state.find_decl(format!("to {}", ext).as_bytes()) {
|
|
Some(converter_id) => {
|
|
let output = engine_state.get_decl(converter_id).run(
|
|
engine_state,
|
|
stack,
|
|
&Call::new(span),
|
|
input,
|
|
)?;
|
|
|
|
output.into_value(span)
|
|
}
|
|
None => input.into_value(span),
|
|
};
|
|
|
|
match output {
|
|
Value::String { val, .. } => {
|
|
if let Err(err) = file.write_all(val.as_bytes()) {
|
|
return Err(ShellError::IOError(err.to_string()));
|
|
}
|
|
|
|
Ok(PipelineData::new(span))
|
|
}
|
|
Value::Binary { val, .. } => {
|
|
if let Err(err) = file.write_all(&val) {
|
|
return Err(ShellError::IOError(err.to_string()));
|
|
}
|
|
|
|
Ok(PipelineData::new(span))
|
|
}
|
|
Value::List { vals, .. } => {
|
|
let val = vals
|
|
.into_iter()
|
|
.map(|it| it.as_string())
|
|
.collect::<Result<Vec<String>, ShellError>>()?
|
|
.join("\n")
|
|
+ "\n";
|
|
|
|
if let Err(err) = file.write_all(val.as_bytes()) {
|
|
return Err(ShellError::IOError(err.to_string()));
|
|
}
|
|
|
|
Ok(PipelineData::new(span))
|
|
}
|
|
v => Err(ShellError::UnsupportedInput(
|
|
format!("{:?} not supported", v.get_type()),
|
|
span,
|
|
)),
|
|
}
|
|
} else {
|
|
match input {
|
|
PipelineData::ExternalStream { stdout: None, .. } => Ok(PipelineData::new(span)),
|
|
PipelineData::ExternalStream {
|
|
stdout: Some(mut stream),
|
|
..
|
|
} => {
|
|
let mut writer = BufWriter::new(file);
|
|
|
|
stream
|
|
.try_for_each(move |result| {
|
|
let buf = match result {
|
|
Ok(v) => match v {
|
|
Value::String { val, .. } => val.into_bytes(),
|
|
Value::Binary { val, .. } => val,
|
|
_ => {
|
|
return Err(ShellError::UnsupportedInput(
|
|
format!("{:?} not supported", v.get_type()),
|
|
v.span()?,
|
|
));
|
|
}
|
|
},
|
|
Err(err) => return Err(err),
|
|
};
|
|
|
|
if let Err(err) = writer.write(&buf) {
|
|
return Err(ShellError::IOError(err.to_string()));
|
|
}
|
|
Ok(())
|
|
})
|
|
.map(|_| PipelineData::new(span))
|
|
}
|
|
input => match input.into_value(span) {
|
|
Value::String { val, .. } => {
|
|
if let Err(err) = file.write_all(val.as_bytes()) {
|
|
return Err(ShellError::IOError(err.to_string()));
|
|
}
|
|
|
|
Ok(PipelineData::new(span))
|
|
}
|
|
Value::Binary { val, .. } => {
|
|
if let Err(err) = file.write_all(&val) {
|
|
return Err(ShellError::IOError(err.to_string()));
|
|
}
|
|
|
|
Ok(PipelineData::new(span))
|
|
}
|
|
Value::List { vals, .. } => {
|
|
let val = vals
|
|
.into_iter()
|
|
.map(|it| it.as_string())
|
|
.collect::<Result<Vec<String>, ShellError>>()?
|
|
.join("\n")
|
|
+ "\n";
|
|
|
|
if let Err(err) = file.write_all(val.as_bytes()) {
|
|
return Err(ShellError::IOError(err.to_string()));
|
|
}
|
|
|
|
Ok(PipelineData::new(span))
|
|
}
|
|
v => Err(ShellError::UnsupportedInput(
|
|
format!("{:?} not supported", v.get_type()),
|
|
span,
|
|
)),
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
fn examples(&self) -> Vec<Example> {
|
|
vec![
|
|
Example {
|
|
description: "Save a string to foo.txt in current directory",
|
|
example: r#"echo 'save me' | save foo.txt"#,
|
|
result: None,
|
|
},
|
|
Example {
|
|
description: "Save a record to foo.json in current directory",
|
|
example: r#"echo { a: 1, b: 2 } | save foo.json"#,
|
|
result: None,
|
|
},
|
|
]
|
|
}
|
|
}
|