nu_command/filesystem/
save.rs

1use crate::progress_bar;
2use nu_engine::get_eval_block;
3#[allow(deprecated)]
4use nu_engine::{command_prelude::*, current_dir};
5use nu_path::expand_path_with;
6use nu_protocol::{
7    ast, byte_stream::copy_with_signals, process::ChildPipe, shell_error::io::IoError,
8    ByteStreamSource, DataSource, OutDest, PipelineMetadata, Signals,
9};
10use std::{
11    fs::File,
12    io::{self, BufRead, BufReader, Read, Write},
13    path::{Path, PathBuf},
14    thread,
15    time::{Duration, Instant},
16};
17
18#[derive(Clone)]
19pub struct Save;
20
21impl Command for Save {
22    fn name(&self) -> &str {
23        "save"
24    }
25
26    fn description(&self) -> &str {
27        "Save a file."
28    }
29
30    fn search_terms(&self) -> Vec<&str> {
31        vec![
32            "write",
33            "write_file",
34            "append",
35            "redirection",
36            "file",
37            "io",
38            ">",
39            ">>",
40        ]
41    }
42
43    fn signature(&self) -> nu_protocol::Signature {
44        Signature::build("save")
45            .input_output_types(vec![(Type::Any, Type::Nothing)])
46            .required("filename", SyntaxShape::Filepath, "The filename to use.")
47            .named(
48                "stderr",
49                SyntaxShape::Filepath,
50                "the filename used to save stderr, only works with `-r` flag",
51                Some('e'),
52            )
53            .switch("raw", "save file as raw binary", Some('r'))
54            .switch("append", "append input to the end of the file", Some('a'))
55            .switch("force", "overwrite the destination", Some('f'))
56            .switch("progress", "enable progress bar", Some('p'))
57            .category(Category::FileSystem)
58    }
59
60    fn run(
61        &self,
62        engine_state: &EngineState,
63        stack: &mut Stack,
64        call: &Call,
65        input: PipelineData,
66    ) -> Result<PipelineData, ShellError> {
67        let raw = call.has_flag(engine_state, stack, "raw")?;
68        let append = call.has_flag(engine_state, stack, "append")?;
69        let force = call.has_flag(engine_state, stack, "force")?;
70        let progress = call.has_flag(engine_state, stack, "progress")?;
71
72        let span = call.head;
73        #[allow(deprecated)]
74        let cwd = current_dir(engine_state, stack)?;
75
76        let path_arg = call.req::<Spanned<PathBuf>>(engine_state, stack, 0)?;
77        let path = Spanned {
78            item: expand_path_with(path_arg.item, &cwd, true),
79            span: path_arg.span,
80        };
81
82        let stderr_path = call
83            .get_flag::<Spanned<PathBuf>>(engine_state, stack, "stderr")?
84            .map(|arg| Spanned {
85                item: expand_path_with(arg.item, cwd, true),
86                span: arg.span,
87            });
88
89        let from_io_error = IoError::factory(span, path.item.as_path());
90        match input {
91            PipelineData::ByteStream(stream, metadata) => {
92                check_saving_to_source_file(metadata.as_ref(), &path, stderr_path.as_ref())?;
93
94                let (file, stderr_file) = get_files(&path, stderr_path.as_ref(), append, force)?;
95
96                let size = stream.known_size();
97                let signals = engine_state.signals();
98
99                match stream.into_source() {
100                    ByteStreamSource::Read(read) => {
101                        stream_to_file(read, size, signals, file, span, progress)?;
102                    }
103                    ByteStreamSource::File(source) => {
104                        stream_to_file(source, size, signals, file, span, progress)?;
105                    }
106                    #[cfg(feature = "os")]
107                    ByteStreamSource::Child(mut child) => {
108                        fn write_or_consume_stderr(
109                            stderr: ChildPipe,
110                            file: Option<File>,
111                            span: Span,
112                            signals: &Signals,
113                            progress: bool,
114                        ) -> Result<(), ShellError> {
115                            if let Some(file) = file {
116                                match stderr {
117                                    ChildPipe::Pipe(pipe) => {
118                                        stream_to_file(pipe, None, signals, file, span, progress)
119                                    }
120                                    ChildPipe::Tee(tee) => {
121                                        stream_to_file(tee, None, signals, file, span, progress)
122                                    }
123                                }?
124                            } else {
125                                match stderr {
126                                    ChildPipe::Pipe(mut pipe) => {
127                                        io::copy(&mut pipe, &mut io::stderr())
128                                    }
129                                    ChildPipe::Tee(mut tee) => {
130                                        io::copy(&mut tee, &mut io::stderr())
131                                    }
132                                }
133                                .map_err(|err| IoError::new(err.kind(), span, None))?;
134                            }
135                            Ok(())
136                        }
137
138                        match (child.stdout.take(), child.stderr.take()) {
139                            (Some(stdout), stderr) => {
140                                // delegate a thread to redirect stderr to result.
141                                let handler = stderr
142                                    .map(|stderr| {
143                                        let signals = signals.clone();
144                                        thread::Builder::new().name("stderr saver".into()).spawn(
145                                            move || {
146                                                write_or_consume_stderr(
147                                                    stderr,
148                                                    stderr_file,
149                                                    span,
150                                                    &signals,
151                                                    progress,
152                                                )
153                                            },
154                                        )
155                                    })
156                                    .transpose()
157                                    .map_err(&from_io_error)?;
158
159                                let res = match stdout {
160                                    ChildPipe::Pipe(pipe) => {
161                                        stream_to_file(pipe, None, signals, file, span, progress)
162                                    }
163                                    ChildPipe::Tee(tee) => {
164                                        stream_to_file(tee, None, signals, file, span, progress)
165                                    }
166                                };
167                                if let Some(h) = handler {
168                                    h.join().map_err(|err| ShellError::ExternalCommand {
169                                        label: "Fail to receive external commands stderr message"
170                                            .to_string(),
171                                        help: format!("{err:?}"),
172                                        span,
173                                    })??;
174                                }
175                                res?;
176                            }
177                            (None, Some(stderr)) => {
178                                write_or_consume_stderr(
179                                    stderr,
180                                    stderr_file,
181                                    span,
182                                    signals,
183                                    progress,
184                                )?;
185                            }
186                            (None, None) => {}
187                        };
188
189                        child.wait()?;
190                    }
191                }
192
193                Ok(PipelineData::Empty)
194            }
195            PipelineData::ListStream(ls, pipeline_metadata)
196                if raw || prepare_path(&path, append, force)?.0.extension().is_none() =>
197            {
198                check_saving_to_source_file(
199                    pipeline_metadata.as_ref(),
200                    &path,
201                    stderr_path.as_ref(),
202                )?;
203
204                let (mut file, _) = get_files(&path, stderr_path.as_ref(), append, force)?;
205                for val in ls {
206                    file.write_all(&value_to_bytes(val)?)
207                        .map_err(&from_io_error)?;
208                    file.write_all("\n".as_bytes()).map_err(&from_io_error)?;
209                }
210                file.flush().map_err(&from_io_error)?;
211
212                Ok(PipelineData::empty())
213            }
214            input => {
215                // It's not necessary to check if we are saving to the same file if this is a
216                // collected value, and not a stream
217                if !matches!(input, PipelineData::Value(..) | PipelineData::Empty) {
218                    check_saving_to_source_file(
219                        input.metadata().as_ref(),
220                        &path,
221                        stderr_path.as_ref(),
222                    )?;
223                }
224
225                let bytes =
226                    input_to_bytes(input, Path::new(&path.item), raw, engine_state, stack, span)?;
227
228                // Only open file after successful conversion
229                let (mut file, _) = get_files(&path, stderr_path.as_ref(), append, force)?;
230
231                file.write_all(&bytes).map_err(&from_io_error)?;
232                file.flush().map_err(&from_io_error)?;
233
234                Ok(PipelineData::empty())
235            }
236        }
237    }
238
239    fn examples(&self) -> Vec<Example> {
240        vec![
241            Example {
242                description: "Save a string to foo.txt in the current directory",
243                example: r#"'save me' | save foo.txt"#,
244                result: None,
245            },
246            Example {
247                description: "Append a string to the end of foo.txt",
248                example: r#"'append me' | save --append foo.txt"#,
249                result: None,
250            },
251            Example {
252                description: "Save a record to foo.json in the current directory",
253                example: r#"{ a: 1, b: 2 } | save foo.json"#,
254                result: None,
255            },
256            Example {
257                description: "Save a running program's stderr to foo.txt",
258                example: r#"do -i {} | save foo.txt --stderr foo.txt"#,
259                result: None,
260            },
261            Example {
262                description: "Save a running program's stderr to separate file",
263                example: r#"do -i {} | save foo.txt --stderr bar.txt"#,
264                result: None,
265            },
266        ]
267    }
268
269    fn pipe_redirection(&self) -> (Option<OutDest>, Option<OutDest>) {
270        (Some(OutDest::PipeSeparate), Some(OutDest::PipeSeparate))
271    }
272}
273
274fn saving_to_source_file_error(dest: &Spanned<PathBuf>) -> ShellError {
275    ShellError::GenericError {
276        error: "pipeline input and output are the same file".into(),
277        msg: format!(
278            "can't save output to '{}' while it's being read",
279            dest.item.display()
280        ),
281        span: Some(dest.span),
282        help: Some(
283            "insert a `collect` command in the pipeline before `save` (see `help collect`).".into(),
284        ),
285        inner: vec![],
286    }
287}
288
289fn check_saving_to_source_file(
290    metadata: Option<&PipelineMetadata>,
291    dest: &Spanned<PathBuf>,
292    stderr_dest: Option<&Spanned<PathBuf>>,
293) -> Result<(), ShellError> {
294    let Some(DataSource::FilePath(source)) = metadata.map(|meta| &meta.data_source) else {
295        return Ok(());
296    };
297
298    if &dest.item == source {
299        return Err(saving_to_source_file_error(dest));
300    }
301
302    if let Some(dest) = stderr_dest {
303        if &dest.item == source {
304            return Err(saving_to_source_file_error(dest));
305        }
306    }
307
308    Ok(())
309}
310
311/// Convert [`PipelineData`] bytes to write in file, possibly converting
312/// to format of output file
313fn input_to_bytes(
314    input: PipelineData,
315    path: &Path,
316    raw: bool,
317    engine_state: &EngineState,
318    stack: &mut Stack,
319    span: Span,
320) -> Result<Vec<u8>, ShellError> {
321    let ext = if raw {
322        None
323    } else if let PipelineData::ByteStream(..) = input {
324        None
325    } else if let PipelineData::Value(Value::String { .. }, ..) = input {
326        None
327    } else {
328        path.extension()
329            .map(|name| name.to_string_lossy().to_string())
330    };
331
332    let input = if let Some(ext) = ext {
333        convert_to_extension(engine_state, &ext, stack, input, span)?
334    } else {
335        input
336    };
337
338    value_to_bytes(input.into_value(span)?)
339}
340
341/// Convert given data into content of file of specified extension if
342/// corresponding `to` command exists. Otherwise attempt to convert
343/// data to bytes as is
344fn convert_to_extension(
345    engine_state: &EngineState,
346    extension: &str,
347    stack: &mut Stack,
348    input: PipelineData,
349    span: Span,
350) -> Result<PipelineData, ShellError> {
351    if let Some(decl_id) = engine_state.find_decl(format!("to {extension}").as_bytes(), &[]) {
352        let decl = engine_state.get_decl(decl_id);
353        if let Some(block_id) = decl.block_id() {
354            let block = engine_state.get_block(block_id);
355            let eval_block = get_eval_block(engine_state);
356            eval_block(engine_state, stack, block, input)
357        } else {
358            let call = ast::Call::new(span);
359            decl.run(engine_state, stack, &(&call).into(), input)
360        }
361    } else {
362        Ok(input)
363    }
364}
365
366/// Convert [`Value::String`] [`Value::Binary`] or [`Value::List`] into [`Vec`] of bytes
367///
368/// Propagates [`Value::Error`] and creates error otherwise
369fn value_to_bytes(value: Value) -> Result<Vec<u8>, ShellError> {
370    match value {
371        Value::String { val, .. } => Ok(val.into_bytes()),
372        Value::Binary { val, .. } => Ok(val),
373        Value::List { vals, .. } => {
374            let val = vals
375                .into_iter()
376                .map(Value::coerce_into_string)
377                .collect::<Result<Vec<String>, ShellError>>()?
378                .join("\n")
379                + "\n";
380
381            Ok(val.into_bytes())
382        }
383        // Propagate errors by explicitly matching them before the final case.
384        Value::Error { error, .. } => Err(*error),
385        other => Ok(other.coerce_into_string()?.into_bytes()),
386    }
387}
388
389/// Convert string path to [`Path`] and [`Span`] and check if this path
390/// can be used with given flags
391fn prepare_path(
392    path: &Spanned<PathBuf>,
393    append: bool,
394    force: bool,
395) -> Result<(&Path, Span), ShellError> {
396    let span = path.span;
397    let path = &path.item;
398
399    if !(force || append) && path.exists() {
400        Err(ShellError::GenericError {
401            error: "Destination file already exists".into(),
402            msg: format!(
403                "Destination file '{}' already exists",
404                path.to_string_lossy()
405            ),
406            span: Some(span),
407            help: Some("you can use -f, --force to force overwriting the destination".into()),
408            inner: vec![],
409        })
410    } else {
411        Ok((path, span))
412    }
413}
414
415fn open_file(path: &Path, span: Span, append: bool) -> Result<File, ShellError> {
416    let file: Result<File, nu_protocol::shell_error::io::ErrorKind> = match (append, path.exists())
417    {
418        (true, true) => std::fs::OpenOptions::new()
419            .append(true)
420            .open(path)
421            .map_err(|err| err.kind().into()),
422        _ => {
423            // This is a temporary solution until `std::fs::File::create` is fixed on Windows (rust-lang/rust#134893)
424            // A TOCTOU problem exists here, which may cause wrong error message to be shown
425            #[cfg(target_os = "windows")]
426            if path.is_dir() {
427                Err(nu_protocol::shell_error::io::ErrorKind::Std(
428                    std::io::ErrorKind::IsADirectory,
429                ))
430            } else {
431                std::fs::File::create(path).map_err(|err| err.kind().into())
432            }
433            #[cfg(not(target_os = "windows"))]
434            std::fs::File::create(path).map_err(|err| err.kind().into())
435        }
436    };
437
438    file.map_err(|err_kind| ShellError::Io(IoError::new(err_kind, span, PathBuf::from(path))))
439}
440
441/// Get output file and optional stderr file
442fn get_files(
443    path: &Spanned<PathBuf>,
444    stderr_path: Option<&Spanned<PathBuf>>,
445    append: bool,
446    force: bool,
447) -> Result<(File, Option<File>), ShellError> {
448    // First check both paths
449    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    // Only if both files can be used open and possibly truncate them
456    let file = open_file(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::GenericError {
462                    error: "input and stderr input to same file".into(),
463                    msg: "can't save both input and stderr input to the same file".into(),
464                    span: Some(stderr_path_span),
465                    help: Some("you should use `o+e> file` instead".into()),
466                    inner: vec![],
467                })
468            } else {
469                open_file(stderr_path, stderr_path_span, append)
470            }
471        })
472        .transpose()?;
473
474    Ok((file, stderr_file))
475}
476
477fn stream_to_file(
478    source: impl Read,
479    known_size: Option<u64>,
480    signals: &Signals,
481    mut file: File,
482    span: Span,
483    progress: bool,
484) -> Result<(), ShellError> {
485    // TODO: maybe we can get a path in here
486    let from_io_error = IoError::factory(span, None);
487
488    // https://github.com/nushell/nushell/pull/9377 contains the reason for not using `BufWriter`
489    if progress {
490        let mut bytes_processed = 0;
491
492        let mut bar = progress_bar::NuProgressBar::new(known_size);
493
494        let mut last_update = Instant::now();
495
496        let mut reader = BufReader::new(source);
497
498        let res = loop {
499            if let Err(err) = signals.check(span) {
500                bar.abandoned_msg("# Cancelled #".to_owned());
501                return Err(err);
502            }
503
504            match reader.fill_buf() {
505                Ok(&[]) => break Ok(()),
506                Ok(buf) => {
507                    file.write_all(buf).map_err(&from_io_error)?;
508                    let len = buf.len();
509                    reader.consume(len);
510                    bytes_processed += len as u64;
511                    if last_update.elapsed() >= Duration::from_millis(75) {
512                        bar.update_bar(bytes_processed);
513                        last_update = Instant::now();
514                    }
515                }
516                Err(e) if e.kind() == io::ErrorKind::Interrupted => continue,
517                Err(e) => break Err(e),
518            }
519        };
520
521        // If the process failed, stop the progress bar with an error message.
522        if let Err(err) = res {
523            let _ = file.flush();
524            bar.abandoned_msg("# Error while saving #".to_owned());
525            Err(from_io_error(err).into())
526        } else {
527            file.flush().map_err(&from_io_error)?;
528            Ok(())
529        }
530    } else {
531        copy_with_signals(source, file, span, signals)?;
532        Ok(())
533    }
534}