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    ByteStreamSource, DataSource, OutDest, PipelineMetadata, Signals, ast,
8    byte_stream::copy_with_signals, process::ChildPipe, shell_error::io::IoError,
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, 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            Example {
267                description: "Show the extensions for which the `save` command will automatically serialize",
268                example: r#"scope commands
269    | where name starts-with "to "
270    | insert extension { get name | str replace -r "^to " "" | $"*.($in)" }
271    | select extension name
272    | rename extension command
273"#,
274                result: None,
275            },
276        ]
277    }
278
279    fn pipe_redirection(&self) -> (Option<OutDest>, Option<OutDest>) {
280        (Some(OutDest::PipeSeparate), Some(OutDest::PipeSeparate))
281    }
282}
283
284fn saving_to_source_file_error(dest: &Spanned<PathBuf>) -> ShellError {
285    ShellError::GenericError {
286        error: "pipeline input and output are the same file".into(),
287        msg: format!(
288            "can't save output to '{}' while it's being read",
289            dest.item.display()
290        ),
291        span: Some(dest.span),
292        help: Some(
293            "insert a `collect` command in the pipeline before `save` (see `help collect`).".into(),
294        ),
295        inner: vec![],
296    }
297}
298
299fn check_saving_to_source_file(
300    metadata: Option<&PipelineMetadata>,
301    dest: &Spanned<PathBuf>,
302    stderr_dest: Option<&Spanned<PathBuf>>,
303) -> Result<(), ShellError> {
304    let Some(DataSource::FilePath(source)) = metadata.map(|meta| &meta.data_source) else {
305        return Ok(());
306    };
307
308    if &dest.item == source {
309        return Err(saving_to_source_file_error(dest));
310    }
311
312    if let Some(dest) = stderr_dest {
313        if &dest.item == source {
314            return Err(saving_to_source_file_error(dest));
315        }
316    }
317
318    Ok(())
319}
320
321/// Convert [`PipelineData`] bytes to write in file, possibly converting
322/// to format of output file
323fn input_to_bytes(
324    input: PipelineData,
325    path: &Path,
326    raw: bool,
327    engine_state: &EngineState,
328    stack: &mut Stack,
329    span: Span,
330) -> Result<Vec<u8>, ShellError> {
331    let ext = if raw {
332        None
333    } else if let PipelineData::ByteStream(..) = input {
334        None
335    } else if let PipelineData::Value(Value::String { .. }, ..) = input {
336        None
337    } else {
338        path.extension()
339            .map(|name| name.to_string_lossy().to_string())
340    };
341
342    let input = if let Some(ext) = ext {
343        convert_to_extension(engine_state, &ext, stack, input, span)?
344    } else {
345        input
346    };
347
348    value_to_bytes(input.into_value(span)?)
349}
350
351/// Convert given data into content of file of specified extension if
352/// corresponding `to` command exists. Otherwise attempt to convert
353/// data to bytes as is
354fn convert_to_extension(
355    engine_state: &EngineState,
356    extension: &str,
357    stack: &mut Stack,
358    input: PipelineData,
359    span: Span,
360) -> Result<PipelineData, ShellError> {
361    if let Some(decl_id) = engine_state.find_decl(format!("to {extension}").as_bytes(), &[]) {
362        let decl = engine_state.get_decl(decl_id);
363        if let Some(block_id) = decl.block_id() {
364            let block = engine_state.get_block(block_id);
365            let eval_block = get_eval_block(engine_state);
366            eval_block(engine_state, stack, block, input)
367        } else {
368            let call = ast::Call::new(span);
369            decl.run(engine_state, stack, &(&call).into(), input)
370        }
371    } else {
372        Ok(input)
373    }
374}
375
376/// Convert [`Value::String`] [`Value::Binary`] or [`Value::List`] into [`Vec`] of bytes
377///
378/// Propagates [`Value::Error`] and creates error otherwise
379fn value_to_bytes(value: Value) -> Result<Vec<u8>, ShellError> {
380    match value {
381        Value::String { val, .. } => Ok(val.into_bytes()),
382        Value::Binary { val, .. } => Ok(val),
383        Value::List { vals, .. } => {
384            let val = vals
385                .into_iter()
386                .map(Value::coerce_into_string)
387                .collect::<Result<Vec<String>, ShellError>>()?
388                .join("\n")
389                + "\n";
390
391            Ok(val.into_bytes())
392        }
393        // Propagate errors by explicitly matching them before the final case.
394        Value::Error { error, .. } => Err(*error),
395        other => Ok(other.coerce_into_string()?.into_bytes()),
396    }
397}
398
399/// Convert string path to [`Path`] and [`Span`] and check if this path
400/// can be used with given flags
401fn prepare_path(
402    path: &Spanned<PathBuf>,
403    append: bool,
404    force: bool,
405) -> Result<(&Path, Span), ShellError> {
406    let span = path.span;
407    let path = &path.item;
408
409    if !(force || append) && path.exists() {
410        Err(ShellError::GenericError {
411            error: "Destination file already exists".into(),
412            msg: format!(
413                "Destination file '{}' already exists",
414                path.to_string_lossy()
415            ),
416            span: Some(span),
417            help: Some("you can use -f, --force to force overwriting the destination".into()),
418            inner: vec![],
419        })
420    } else {
421        Ok((path, span))
422    }
423}
424
425fn open_file(path: &Path, span: Span, append: bool) -> Result<File, ShellError> {
426    let file: Result<File, nu_protocol::shell_error::io::ErrorKind> = match (append, path.exists())
427    {
428        (true, true) => std::fs::OpenOptions::new()
429            .append(true)
430            .open(path)
431            .map_err(|err| err.into()),
432        _ => {
433            // This is a temporary solution until `std::fs::File::create` is fixed on Windows (rust-lang/rust#134893)
434            // A TOCTOU problem exists here, which may cause wrong error message to be shown
435            #[cfg(target_os = "windows")]
436            if path.is_dir() {
437                #[allow(
438                    deprecated,
439                    reason = "we don't get a IsADirectory error, so we need to provide it"
440                )]
441                Err(nu_protocol::shell_error::io::ErrorKind::from_std(
442                    std::io::ErrorKind::IsADirectory,
443                ))
444            } else {
445                std::fs::File::create(path).map_err(|err| err.into())
446            }
447            #[cfg(not(target_os = "windows"))]
448            std::fs::File::create(path).map_err(|err| err.into())
449        }
450    };
451
452    file.map_err(|err_kind| ShellError::Io(IoError::new(err_kind, span, PathBuf::from(path))))
453}
454
455/// Get output file and optional stderr file
456fn get_files(
457    path: &Spanned<PathBuf>,
458    stderr_path: Option<&Spanned<PathBuf>>,
459    append: bool,
460    force: bool,
461) -> Result<(File, Option<File>), ShellError> {
462    // First check both paths
463    let (path, path_span) = prepare_path(path, append, force)?;
464    let stderr_path_and_span = stderr_path
465        .as_ref()
466        .map(|stderr_path| prepare_path(stderr_path, append, force))
467        .transpose()?;
468
469    // Only if both files can be used open and possibly truncate them
470    let file = open_file(path, path_span, append)?;
471
472    let stderr_file = stderr_path_and_span
473        .map(|(stderr_path, stderr_path_span)| {
474            if path == stderr_path {
475                Err(ShellError::GenericError {
476                    error: "input and stderr input to same file".into(),
477                    msg: "can't save both input and stderr input to the same file".into(),
478                    span: Some(stderr_path_span),
479                    help: Some("you should use `o+e> file` instead".into()),
480                    inner: vec![],
481                })
482            } else {
483                open_file(stderr_path, stderr_path_span, append)
484            }
485        })
486        .transpose()?;
487
488    Ok((file, stderr_file))
489}
490
491fn stream_to_file(
492    source: impl Read,
493    known_size: Option<u64>,
494    signals: &Signals,
495    mut file: File,
496    span: Span,
497    progress: bool,
498) -> Result<(), ShellError> {
499    // TODO: maybe we can get a path in here
500    let from_io_error = IoError::factory(span, None);
501
502    // https://github.com/nushell/nushell/pull/9377 contains the reason for not using `BufWriter`
503    if progress {
504        let mut bytes_processed = 0;
505
506        let mut bar = progress_bar::NuProgressBar::new(known_size);
507
508        let mut last_update = Instant::now();
509
510        let mut reader = BufReader::new(source);
511
512        let res = loop {
513            if let Err(err) = signals.check(span) {
514                bar.abandoned_msg("# Cancelled #".to_owned());
515                return Err(err);
516            }
517
518            match reader.fill_buf() {
519                Ok(&[]) => break Ok(()),
520                Ok(buf) => {
521                    file.write_all(buf).map_err(&from_io_error)?;
522                    let len = buf.len();
523                    reader.consume(len);
524                    bytes_processed += len as u64;
525                    if last_update.elapsed() >= Duration::from_millis(75) {
526                        bar.update_bar(bytes_processed);
527                        last_update = Instant::now();
528                    }
529                }
530                Err(e) if e.kind() == io::ErrorKind::Interrupted => continue,
531                Err(e) => break Err(e),
532            }
533        };
534
535        // If the process failed, stop the progress bar with an error message.
536        if let Err(err) = res {
537            let _ = file.flush();
538            bar.abandoned_msg("# Error while saving #".to_owned());
539            Err(from_io_error(err).into())
540        } else {
541            file.flush().map_err(&from_io_error)?;
542            Ok(())
543        }
544    } else {
545        copy_with_signals(source, file, span, signals)?;
546        Ok(())
547    }
548}