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)?)
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)?)?;
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 value_to_bytes(value: Value) -> Result<Vec<u8>, ShellError> {
335 match value {
336 Value::String { val, .. } => Ok(val.into_bytes()),
337 Value::Binary { val, .. } => Ok(val),
338 Value::List { vals, .. } => {
339 let val = vals
340 .into_iter()
341 .map(Value::coerce_into_string)
342 .collect::<Result<Vec<String>, ShellError>>()?
343 .join("\n")
344 + "\n";
345
346 Ok(val.into_bytes())
347 }
348 Value::Error { error, .. } => Err(*error),
350 other => Ok(other.coerce_into_string()?.into_bytes()),
351 }
352}
353
354fn prepare_path(
357 path: &Spanned<PathBuf>,
358 append: bool,
359 force: bool,
360) -> Result<(&Path, Span), ShellError> {
361 let span = path.span;
362 let path = &path.item;
363
364 if !(force || append) && path.exists() {
365 Err(ShellError::Generic(
366 GenericError::new(
367 "Destination file already exists",
368 format!(
369 "Destination file '{}' already exists",
370 path.to_string_lossy()
371 ),
372 span,
373 )
374 .with_help("you can use -f, --force to force overwriting the destination"),
375 ))
376 } else {
377 Ok((path, span))
378 }
379}
380
381fn open_file(
382 engine_state: &EngineState,
383 path: &Path,
384 span: Span,
385 append: bool,
386) -> Result<File, ShellError> {
387 let file: std::io::Result<File> = match (append, path.exists() || is_windows_device_path(path))
388 {
389 (true, true) => std::fs::OpenOptions::new().append(true).open(path),
390 _ => {
391 #[cfg(target_os = "windows")]
394 if path.is_dir() {
395 #[allow(
396 deprecated,
397 reason = "we don't get a IsADirectory error, so we need to provide it"
398 )]
399 Err(std::io::ErrorKind::IsADirectory.into())
400 } else {
401 std::fs::File::create(path)
402 }
403 #[cfg(not(target_os = "windows"))]
404 std::fs::File::create(path)
405 }
406 };
407
408 match file {
409 Ok(file) => Ok(file),
410 Err(err) => {
411 if err.kind() == std::io::ErrorKind::NotFound
414 && let Some(missing_component) =
415 path.ancestors().skip(1).filter(|dir| !dir.exists()).last()
416 {
417 let components_to_remove = path
420 .strip_prefix(missing_component)
421 .expect("Stripping ancestor from a path should never fail")
422 .as_os_str()
423 .as_encoded_bytes();
424
425 return Err(ShellError::Io(IoError::new(
426 ErrorKind::DirectoryNotFound,
427 engine_state
428 .span_match_postfix(span, components_to_remove)
429 .map(|(pre, _post)| pre)
430 .unwrap_or(span),
431 PathBuf::from(missing_component),
432 )));
433 }
434
435 Err(ShellError::Io(IoError::new(err, span, PathBuf::from(path))))
436 }
437 }
438}
439
440fn get_files(
442 engine_state: &EngineState,
443 path: &Spanned<PathBuf>,
444 stderr_path: Option<&Spanned<PathBuf>>,
445 append: bool,
446 force: bool,
447) -> Result<(File, Option<File>), ShellError> {
448 let (path, path_span) = prepare_path(path, append, force)?;
450 let stderr_path_and_span = stderr_path
451 .as_ref()
452 .map(|stderr_path| prepare_path(stderr_path, append, force))
453 .transpose()?;
454
455 let file = open_file(engine_state, path, path_span, append)?;
457
458 let stderr_file = stderr_path_and_span
459 .map(|(stderr_path, stderr_path_span)| {
460 if path == stderr_path {
461 Err(ShellError::Generic(
462 GenericError::new(
463 "input and stderr input to same file",
464 "can't save both input and stderr input to the same file",
465 stderr_path_span,
466 )
467 .with_help("you should use `o+e> file` instead"),
468 ))
469 } else {
470 open_file(engine_state, stderr_path, stderr_path_span, append)
471 }
472 })
473 .transpose()?;
474
475 Ok((file, stderr_file))
476}
477
478fn write_or_consume_stderr(
479 stderr: ChildPipe,
480 file: Option<File>,
481 span: Span,
482 signals: &Signals,
483 progress: bool,
484) -> Result<(), ShellError> {
485 if let Some(file) = file {
486 match stderr {
487 ChildPipe::Pipe(pipe) => stream_to_file(pipe, None, signals, file, span, progress),
488 ChildPipe::Tee(tee) => stream_to_file(tee, None, signals, file, span, progress),
489 }?
490 } else {
491 match stderr {
492 ChildPipe::Pipe(mut pipe) => io::copy(&mut pipe, &mut io::stderr()),
493 ChildPipe::Tee(mut tee) => io::copy(&mut tee, &mut io::stderr()),
494 }
495 .map_err(|err| IoError::new(err, span, None))?;
496 }
497 Ok(())
498}
499
500struct ByteStreamSaveContext<'a> {
501 metadata: Option<&'a PipelineMetadata>,
502 path: &'a Spanned<PathBuf>,
503 stderr_path: Option<&'a Spanned<PathBuf>>,
504 engine_state: &'a EngineState,
505 append: bool,
506 force: bool,
507 span: Span,
508 progress: bool,
509}
510
511fn stream_byte_stream_to_file(
512 stream: ByteStream,
513 context: ByteStreamSaveContext<'_>,
514) -> Result<PipelineData, ShellError> {
515 let from_io_error = IoError::factory(context.span, context.path.item.as_path());
516 let span = context.span;
517 let progress = context.progress;
518
519 check_saving_to_source_file(context.metadata, context.path, context.stderr_path)?;
520
521 let (file, stderr_file) = get_files(
522 context.engine_state,
523 context.path,
524 context.stderr_path,
525 context.append,
526 context.force,
527 )?;
528
529 let size = stream.known_size();
530 let signals = context.engine_state.signals();
531
532 match stream.into_source() {
533 ByteStreamSource::Read(read) => {
534 stream_to_file(read, size, signals, file, span, progress)?;
535 }
536 ByteStreamSource::File(source) => {
537 stream_to_file(source, size, signals, file, span, progress)?;
538 }
539 #[cfg(feature = "os")]
540 ByteStreamSource::Child(mut child) => {
541 match (child.stdout.take(), child.stderr.take()) {
542 (Some(stdout), stderr) => {
543 let handler = stderr
544 .map(|stderr| {
545 let signals = signals.clone();
546 thread::Builder::new()
547 .name("stderr saver".into())
548 .spawn(move || {
549 write_or_consume_stderr(
550 stderr,
551 stderr_file,
552 span,
553 &signals,
554 progress,
555 )
556 })
557 })
558 .transpose()
559 .map_err(&from_io_error)?;
560
561 let res = match stdout {
562 ChildPipe::Pipe(pipe) => {
563 stream_to_file(pipe, None, signals, file, span, progress)
564 }
565 ChildPipe::Tee(tee) => {
566 stream_to_file(tee, None, signals, file, span, progress)
567 }
568 };
569 if let Some(h) = handler {
570 h.join().map_err(|err| ShellError::ExternalCommand {
571 label: "Fail to receive external commands stderr message".to_string(),
572 help: format!("{err:?}"),
573 span,
574 })??;
575 }
576 res?;
577 }
578 (None, Some(stderr)) => {
579 write_or_consume_stderr(stderr, stderr_file, span, signals, progress)?;
580 }
581 (None, None) => {}
582 };
583
584 child.wait()?;
585 }
586 }
587
588 Ok(PipelineData::empty())
589}
590
591fn stream_to_file(
592 source: impl Read,
593 known_size: Option<u64>,
594 signals: &Signals,
595 mut file: File,
596 span: Span,
597 progress: bool,
598) -> Result<(), ShellError> {
599 let from_io_error = IoError::factory(span, None);
601
602 if progress {
604 let mut bytes_processed = 0;
605
606 let mut bar = progress_bar::NuProgressBar::new(known_size);
607
608 let mut last_update = Instant::now();
609
610 let mut reader = BufReader::new(source);
611
612 let res = loop {
613 if let Err(err) = signals.check(&span) {
614 bar.abandoned_msg("# Cancelled #");
615 return Err(err);
616 }
617
618 match reader.fill_buf() {
619 Ok(&[]) => break Ok(()),
620 Ok(buf) => {
621 file.write_all(buf).map_err(&from_io_error)?;
622 let len = buf.len();
623 reader.consume(len);
624 bytes_processed += len as u64;
625 if last_update.elapsed() >= Duration::from_millis(75) {
626 bar.update_bar(bytes_processed);
627 last_update = Instant::now();
628 }
629 }
630 Err(e) if e.kind() == io::ErrorKind::Interrupted => continue,
631 Err(e) => break Err(e),
632 }
633 };
634
635 if let Err(err) = res {
637 let _ = file.flush();
638 bar.abandoned_msg("# Error while saving #");
639 Err(from_io_error(err).into())
640 } else {
641 file.flush().map_err(&from_io_error)?;
642 Ok(())
643 }
644 } else {
645 copy_with_signals(source, file, span, signals)?;
646 Ok(())
647 }
648}