1use crate::formats::{preserve_toml_document, read_toml_source_from_metadata};
2use crate::progress_bar;
3use nu_engine::{command_prelude::*, get_eval_block};
4use nu_path::{expand_path_with, is_windows_device_path};
5use nu_protocol::{
6 ByteStreamSource, DataSource, OutDest, PipelineMetadata, Signals, ast,
7 byte_stream::copy_with_signals, process::ChildPipe, shell_error::generic::GenericError,
8 shell_error::io::IoError,
9};
10use std::{
11 borrow::Cow,
12 fs::File,
13 io::{self, BufRead, BufReader, Read, Write},
14 path::{Path, PathBuf},
15 thread,
16 time::Duration,
17};
18
19use nu_utils::time::Instant;
20
21#[derive(Clone)]
22pub struct Save;
23
24impl Command for Save {
25 fn name(&self) -> &str {
26 "save"
27 }
28
29 fn description(&self) -> &str {
30 "Save a file."
31 }
32
33 fn search_terms(&self) -> Vec<&str> {
34 vec![
35 "write",
36 "write_file",
37 "append",
38 "redirection",
39 "file",
40 "io",
41 ">",
42 ">>",
43 ]
44 }
45
46 fn signature(&self) -> nu_protocol::Signature {
47 Signature::build("save")
48 .input_output_types(vec![(Type::Any, Type::Nothing)])
49 .required("filename", SyntaxShape::Filepath, "The filename to use.")
50 .named(
51 "stderr",
52 SyntaxShape::Filepath,
53 "The filename used to save stderr, only works with `-r` flag.",
54 Some('e'),
55 )
56 .switch("raw", "Save file as raw binary.", Some('r'))
57 .switch("append", "Append input to the end of the file.", Some('a'))
58 .switch("force", "Overwrite the destination.", Some('f'))
59 .switch("progress", "Enable progress bar.", Some('p'))
60 .category(Category::FileSystem)
61 }
62
63 fn run(
64 &self,
65 engine_state: &EngineState,
66 stack: &mut Stack,
67 call: &Call,
68 input: PipelineData,
69 ) -> Result<PipelineData, ShellError> {
70 let raw = call.has_flag(engine_state, stack, "raw")?;
71 let append = call.has_flag(engine_state, stack, "append")?;
72 let force = call.has_flag(engine_state, stack, "force")?;
73 let progress = call.has_flag(engine_state, stack, "progress")?;
74
75 let span = call.head;
76 let cwd = engine_state.cwd(Some(stack))?.into_std_path_buf();
77
78 let path = call
79 .req::<Spanned<PathBuf>>(engine_state, stack, 0)?
80 .map(|p| expand_path_with(p, &cwd, true));
81
82 let stderr_path = call
83 .get_flag::<Spanned<PathBuf>>(engine_state, stack, "stderr")?
84 .map(|arg| arg.map(|p| expand_path_with(p, cwd, true)));
85
86 let from_io_error = IoError::factory(span, path.item.as_path());
87 let save_byte_stream = |stream, metadata| {
88 stream_byte_stream_to_file(
89 stream,
90 ByteStreamSaveContext {
91 metadata,
92 path: &path,
93 stderr_path: stderr_path.as_ref(),
94 engine_state,
95 append,
96 force,
97 span,
98 progress,
99 },
100 )
101 };
102
103 match input {
104 PipelineData::ByteStream(stream, metadata) => {
105 save_byte_stream(stream, metadata.as_ref())
106 }
107 PipelineData::ListStream(ls, pipeline_metadata)
108 if raw || prepare_path(&path, append, force)?.0.extension().is_none() =>
109 {
110 check_saving_to_source_file(
111 pipeline_metadata.as_ref(),
112 &path,
113 stderr_path.as_ref(),
114 )?;
115
116 let (mut file, _) =
117 get_files(engine_state, &path, stderr_path.as_ref(), append, force)?;
118 for val in ls {
119 file.write_all(&value_to_bytes(val, span)?)
120 .map_err(&from_io_error)?;
121 file.write_all("\n".as_bytes()).map_err(&from_io_error)?;
122 }
123 file.flush().map_err(&from_io_error)?;
124
125 Ok(PipelineData::empty())
126 }
127 input => {
128 if !matches!(input, PipelineData::Value(..) | PipelineData::Empty) {
131 check_saving_to_source_file(input.metadata_ref(), &path, stderr_path.as_ref())?;
132 }
133
134 if let Some(bytes) =
135 preserve_toml_output(engine_state, &input, &path.item, raw, append, span)?
136 {
137 let (mut file, _) =
138 get_files(engine_state, &path, stderr_path.as_ref(), append, force)?;
139
140 file.write_all(&bytes).map_err(&from_io_error)?;
141 file.flush().map_err(&from_io_error)?;
142
143 return Ok(PipelineData::empty());
144 }
145
146 let ext = extract_extension(&input, &path.item, raw);
148 let converted = match ext {
149 None => input,
150 Some(ext) => convert_to_extension(engine_state, &ext, stack, input, span)?,
151 };
152
153 if let PipelineData::Value(v @ Value::Custom { .. }, ..) = converted {
155 let val_span = v.span();
156 let val = v.into_custom_value()?;
157 return val
158 .save(path.as_deref(), val_span, span)
159 .map(|()| PipelineData::empty());
160 }
161
162 if let PipelineData::ByteStream(stream, metadata) = converted {
165 return save_byte_stream(stream, metadata.as_ref());
166 }
167
168 let bytes = value_to_bytes(converted.into_value(span)?, span)?;
169
170 let (mut file, _) =
172 get_files(engine_state, &path, stderr_path.as_ref(), append, force)?;
173
174 file.write_all(&bytes).map_err(&from_io_error)?;
175 file.flush().map_err(&from_io_error)?;
176
177 Ok(PipelineData::empty())
178 }
179 }
180 }
181
182 fn examples(&self) -> Vec<Example<'_>> {
183 vec![
184 Example {
185 description: "Save a string to foo.txt in the current directory.",
186 example: "'save me' | save foo.txt",
187 result: None,
188 },
189 Example {
190 description: "Append a string to the end of foo.txt.",
191 example: "'append me' | save --append foo.txt",
192 result: None,
193 },
194 Example {
195 description: "Save a record to foo.json in the current directory.",
196 example: "{ a: 1, b: 2 } | save foo.json",
197 result: None,
198 },
199 Example {
200 description: "Save a running program's stderr to foo.txt.",
201 example: "do -i {} | save foo.txt --stderr foo.txt",
202 result: None,
203 },
204 Example {
205 description: "Save a running program's stderr to separate file.",
206 example: "do -i {} | save foo.txt --stderr bar.txt",
207 result: None,
208 },
209 Example {
210 description: "Show the extensions for which the `save` command will automatically serialize.",
211 example: r#"scope commands
212 | where name starts-with "to "
213 | insert extension { get name | str replace -r "^to " "" | $"*.($in)" }
214 | select extension name
215 | rename extension command
216"#,
217 result: None,
218 },
219 ]
220 }
221
222 fn pipe_redirection(&self) -> (Option<OutDest>, Option<OutDest>) {
223 (Some(OutDest::PipeSeparate), Some(OutDest::PipeSeparate))
224 }
225}
226
227fn saving_to_source_file_error(dest: &Spanned<PathBuf>) -> ShellError {
228 ShellError::Generic(
229 GenericError::new(
230 "pipeline input and output are the same file",
231 format!(
232 "can't save output to '{}' while it's being read",
233 dest.item.display()
234 ),
235 dest.span,
236 )
237 .with_help(
238 "insert a `collect` command in the pipeline before `save` (see `help collect`).",
239 ),
240 )
241}
242
243fn check_saving_to_source_file(
244 metadata: Option<&PipelineMetadata>,
245 dest: &Spanned<PathBuf>,
246 stderr_dest: Option<&Spanned<PathBuf>>,
247) -> Result<(), ShellError> {
248 let Some(DataSource::FilePath(source)) = metadata.map(|meta| &meta.data_source) else {
249 return Ok(());
250 };
251
252 if &dest.item == source {
253 return Err(saving_to_source_file_error(dest));
254 }
255
256 if let Some(dest) = stderr_dest
257 && &dest.item == source
258 {
259 return Err(saving_to_source_file_error(dest));
260 }
261
262 Ok(())
263}
264
265fn preserve_toml_output(
266 engine_state: &EngineState,
267 input: &PipelineData,
268 path: &Path,
269 raw: bool,
270 append: bool,
271 span: Span,
272) -> Result<Option<Vec<u8>>, ShellError> {
273 if raw
274 || append
275 || path
276 .extension()
277 .is_none_or(|extension| extension.to_string_lossy() != "toml")
278 {
279 return Ok(None);
280 }
281
282 let PipelineData::Value(value, metadata) = input else {
283 return Ok(None);
284 };
285 let Some(original_source) = read_toml_source_from_metadata(metadata.as_ref()) else {
286 return Ok(None);
287 };
288
289 match value {
290 Value::Record { .. } => preserve_toml_document(engine_state, value, &original_source, span)
291 .map(|document| Some(document.into_bytes())),
292 _ => Ok(None),
293 }
294}
295
296fn extract_extension<'e>(input: &PipelineData, path: &'e Path, raw: bool) -> Option<Cow<'e, str>> {
298 match (raw, input) {
299 (true, _)
300 | (_, PipelineData::ByteStream(..))
301 | (_, PipelineData::Value(Value::String { .. }, ..)) => None,
302 _ => path.extension().map(|name| name.to_string_lossy()),
303 }
304}
305
306fn convert_to_extension(
310 engine_state: &EngineState,
311 extension: &str,
312 stack: &mut Stack,
313 input: PipelineData,
314 span: Span,
315) -> Result<PipelineData, ShellError> {
316 if let Some(decl_id) = engine_state.find_decl(format!("to {extension}").as_bytes(), &[]) {
317 let decl = engine_state.get_decl(decl_id);
318 if let Some(block_id) = decl.block_id() {
319 let block = engine_state.get_block(block_id);
320 let eval_block = get_eval_block(engine_state);
321 eval_block(engine_state, stack, block, input).map(|p| p.body)
322 } else {
323 let call = ast::Call::new(span);
324 decl.run(engine_state, stack, &(&call).into(), input)
325 }
326 } else {
327 Ok(input)
328 }
329}
330
331fn cant_serialize_to_file(from_type: Type, value_span: Span, call_span: Span) -> ShellError {
341 ShellError::UnsupportedInput {
342 msg: format!(
343 "cannot save {from_type} to this file: no `to` converter matches the file's \
344 extension. Serialize it first, e.g. `... | to json | save <file>`, or save \
345 the rendered table with ansi escape sequences with `... | table | save <file>`"
346 ),
347 input: "value originates from here".into(),
348 msg_span: call_span,
349 input_span: value_span,
350 }
351}
352
353fn value_to_bytes(value: Value, span: Span) -> Result<Vec<u8>, ShellError> {
359 match value {
360 Value::String { val, .. } => Ok(val.into_bytes()),
361 Value::Binary { val, .. } => Ok(val.into_owned()),
362 Value::List { vals, .. } => {
363 let val = vals
364 .into_iter()
365 .map(|val| {
366 let (ty, val_span) = (val.get_type(), val.span());
367 val.coerce_into_string()
368 .map_err(|_| cant_serialize_to_file(ty, val_span, span))
369 })
370 .collect::<Result<Vec<String>, ShellError>>()?
371 .join("\n")
372 + "\n";
373
374 Ok(val.into_bytes())
375 }
376 Value::Error { error, .. } => Err(*error),
378 other => {
379 let (ty, val_span) = (other.get_type(), other.span());
380 other
381 .coerce_into_string()
382 .map(String::into_bytes)
383 .map_err(|_| cant_serialize_to_file(ty, val_span, span))
384 }
385 }
386}
387
388fn prepare_path(
391 path: &Spanned<PathBuf>,
392 append: bool,
393 force: bool,
394) -> Result<(&Path, Span), ShellError> {
395 let span = path.span;
396 let path = &path.item;
397
398 if !(force || append) && path.exists() {
399 Err(ShellError::Generic(
400 GenericError::new(
401 "Destination file already exists",
402 format!(
403 "Destination file '{}' already exists",
404 path.to_string_lossy()
405 ),
406 span,
407 )
408 .with_help("you can use -f, --force to force overwriting the destination"),
409 ))
410 } else {
411 Ok((path, span))
412 }
413}
414
415fn open_file(
416 engine_state: &EngineState,
417 path: &Path,
418 span: Span,
419 append: bool,
420) -> Result<File, ShellError> {
421 let file: std::io::Result<File> = match (append, path.exists() || is_windows_device_path(path))
422 {
423 (true, true) => std::fs::OpenOptions::new().append(true).open(path),
424 _ => {
425 #[cfg(target_os = "windows")]
428 if path.is_dir() {
429 #[allow(
430 deprecated,
431 reason = "we don't get a IsADirectory error, so we need to provide it"
432 )]
433 Err(std::io::ErrorKind::IsADirectory.into())
434 } else {
435 std::fs::File::create(path)
436 }
437 #[cfg(not(target_os = "windows"))]
438 std::fs::File::create(path)
439 }
440 };
441
442 match file {
443 Ok(file) => Ok(file),
444 Err(err) => {
445 if err.kind() == std::io::ErrorKind::NotFound
448 && let Some(missing_component) =
449 path.ancestors().skip(1).filter(|dir| !dir.exists()).last()
450 {
451 let components_to_remove = path
454 .strip_prefix(missing_component)
455 .expect("Stripping ancestor from a path should never fail")
456 .as_os_str()
457 .as_encoded_bytes();
458
459 return Err(ShellError::Io(IoError::new(
460 ErrorKind::DirectoryNotFound,
461 engine_state
462 .span_match_postfix(span, components_to_remove)
463 .map(|(pre, _post)| pre)
464 .unwrap_or(span),
465 PathBuf::from(missing_component),
466 )));
467 }
468
469 Err(ShellError::Io(IoError::new(err, span, PathBuf::from(path))))
470 }
471 }
472}
473
474fn get_files(
476 engine_state: &EngineState,
477 path: &Spanned<PathBuf>,
478 stderr_path: Option<&Spanned<PathBuf>>,
479 append: bool,
480 force: bool,
481) -> Result<(File, Option<File>), ShellError> {
482 let (path, path_span) = prepare_path(path, append, force)?;
484 let stderr_path_and_span = stderr_path
485 .as_ref()
486 .map(|stderr_path| prepare_path(stderr_path, append, force))
487 .transpose()?;
488
489 let file = open_file(engine_state, path, path_span, append)?;
491
492 let stderr_file = stderr_path_and_span
493 .map(|(stderr_path, stderr_path_span)| {
494 if path == stderr_path {
495 Err(ShellError::Generic(
496 GenericError::new(
497 "input and stderr input to same file",
498 "can't save both input and stderr input to the same file",
499 stderr_path_span,
500 )
501 .with_help("you should use `o+e> file` instead"),
502 ))
503 } else {
504 open_file(engine_state, stderr_path, stderr_path_span, append)
505 }
506 })
507 .transpose()?;
508
509 Ok((file, stderr_file))
510}
511
512fn write_or_consume_stderr(
513 stderr: ChildPipe,
514 file: Option<File>,
515 span: Span,
516 signals: &Signals,
517 progress: bool,
518) -> Result<(), ShellError> {
519 if let Some(file) = file {
520 match stderr {
521 ChildPipe::Pipe(pipe) => stream_to_file(pipe, None, signals, file, span, progress),
522 ChildPipe::Tee(tee) => stream_to_file(tee, None, signals, file, span, progress),
523 }?
524 } else {
525 match stderr {
526 ChildPipe::Pipe(mut pipe) => io::copy(&mut pipe, &mut io::stderr()),
527 ChildPipe::Tee(mut tee) => io::copy(&mut tee, &mut io::stderr()),
528 }
529 .map_err(|err| IoError::new(err, span, None))?;
530 }
531 Ok(())
532}
533
534struct ByteStreamSaveContext<'a> {
535 metadata: Option<&'a PipelineMetadata>,
536 path: &'a Spanned<PathBuf>,
537 stderr_path: Option<&'a Spanned<PathBuf>>,
538 engine_state: &'a EngineState,
539 append: bool,
540 force: bool,
541 span: Span,
542 progress: bool,
543}
544
545fn stream_byte_stream_to_file(
546 stream: ByteStream,
547 context: ByteStreamSaveContext<'_>,
548) -> Result<PipelineData, ShellError> {
549 let from_io_error = IoError::factory(context.span, context.path.item.as_path());
550 let span = context.span;
551 let progress = context.progress;
552
553 check_saving_to_source_file(context.metadata, context.path, context.stderr_path)?;
554
555 let (file, stderr_file) = get_files(
556 context.engine_state,
557 context.path,
558 context.stderr_path,
559 context.append,
560 context.force,
561 )?;
562
563 let size = stream.known_size();
564 let signals = context.engine_state.signals();
565
566 match stream.into_source() {
567 ByteStreamSource::Read(read) => {
568 stream_to_file(read, size, signals, file, span, progress)?;
569 }
570 ByteStreamSource::File(source) => {
571 stream_to_file(source, size, signals, file, span, progress)?;
572 }
573 #[cfg(feature = "os")]
574 ByteStreamSource::Child(mut child) => {
575 match (child.stdout.take(), child.stderr.take()) {
576 (Some(stdout), stderr) => {
577 let handler = stderr
578 .map(|stderr| {
579 let signals = signals.clone();
580 thread::Builder::new()
581 .name("stderr saver".into())
582 .spawn(move || {
583 write_or_consume_stderr(
584 stderr,
585 stderr_file,
586 span,
587 &signals,
588 progress,
589 )
590 })
591 })
592 .transpose()
593 .map_err(&from_io_error)?;
594
595 let res = match stdout {
596 ChildPipe::Pipe(pipe) => {
597 stream_to_file(pipe, None, signals, file, span, progress)
598 }
599 ChildPipe::Tee(tee) => {
600 stream_to_file(tee, None, signals, file, span, progress)
601 }
602 };
603 if let Some(h) = handler {
604 h.join().map_err(|err| ShellError::ExternalCommand {
605 label: "Fail to receive external commands stderr message".to_string(),
606 help: format!("{err:?}"),
607 span,
608 })??;
609 }
610 res?;
611 }
612 (None, Some(stderr)) => {
613 write_or_consume_stderr(stderr, stderr_file, span, signals, progress)?;
614 }
615 (None, None) => {}
616 };
617
618 child.wait()?;
619 }
620 }
621
622 Ok(PipelineData::empty())
623}
624
625fn stream_to_file(
626 source: impl Read,
627 known_size: Option<u64>,
628 signals: &Signals,
629 mut file: File,
630 span: Span,
631 progress: bool,
632) -> Result<(), ShellError> {
633 let from_io_error = IoError::factory(span, None);
635
636 if progress {
638 let mut bytes_processed = 0;
639
640 let mut bar = progress_bar::NuProgressBar::new(known_size);
641
642 let mut last_update = Instant::now();
643
644 let mut reader = BufReader::new(source);
645
646 let res = loop {
647 if let Err(err) = signals.check(&span) {
648 bar.abandoned_msg("# Cancelled #");
649 return Err(err);
650 }
651
652 match reader.fill_buf() {
653 Ok(&[]) => break Ok(()),
654 Ok(buf) => {
655 file.write_all(buf).map_err(&from_io_error)?;
656 let len = buf.len();
657 reader.consume(len);
658 bytes_processed += len as u64;
659 if last_update.elapsed() >= Duration::from_millis(75) {
660 bar.update_bar(bytes_processed);
661 last_update = Instant::now();
662 }
663 }
664 Err(e) if e.kind() == io::ErrorKind::Interrupted => continue,
665 Err(e) => break Err(e),
666 }
667 };
668
669 if let Err(err) = res {
671 let _ = file.flush();
672 bar.abandoned_msg("# Error while saving #");
673 Err(from_io_error(err).into())
674 } else {
675 file.flush().map_err(&from_io_error)?;
676 Ok(())
677 }
678 } else {
679 copy_with_signals(source, file, span, signals)?;
680 Ok(())
681 }
682}