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, is_windows_device_path};
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 borrow::Cow,
12 fs::File,
13 io::{self, BufRead, BufReader, Read, Write},
14 path::{Path, PathBuf},
15 thread,
16 time::{Duration, Instant},
17};
18
19#[derive(Clone)]
20pub struct Save;
21
22impl Command for Save {
23 fn name(&self) -> &str {
24 "save"
25 }
26
27 fn description(&self) -> &str {
28 "Save a file."
29 }
30
31 fn search_terms(&self) -> Vec<&str> {
32 vec![
33 "write",
34 "write_file",
35 "append",
36 "redirection",
37 "file",
38 "io",
39 ">",
40 ">>",
41 ]
42 }
43
44 fn signature(&self) -> nu_protocol::Signature {
45 Signature::build("save")
46 .input_output_types(vec![(Type::Any, Type::Nothing)])
47 .required("filename", SyntaxShape::Filepath, "The filename to use.")
48 .named(
49 "stderr",
50 SyntaxShape::Filepath,
51 "the filename used to save stderr, only works with `-r` flag",
52 Some('e'),
53 )
54 .switch("raw", "save file as raw binary", Some('r'))
55 .switch("append", "append input to the end of the file", Some('a'))
56 .switch("force", "overwrite the destination", Some('f'))
57 .switch("progress", "enable progress bar", Some('p'))
58 .category(Category::FileSystem)
59 }
60
61 fn run(
62 &self,
63 engine_state: &EngineState,
64 stack: &mut Stack,
65 call: &Call,
66 input: PipelineData,
67 ) -> Result<PipelineData, ShellError> {
68 let raw = call.has_flag(engine_state, stack, "raw")?;
69 let append = call.has_flag(engine_state, stack, "append")?;
70 let force = call.has_flag(engine_state, stack, "force")?;
71 let progress = call.has_flag(engine_state, stack, "progress")?;
72
73 let span = call.head;
74 #[allow(deprecated)]
75 let cwd = current_dir(engine_state, stack)?;
76
77 let path_arg = call.req::<Spanned<PathBuf>>(engine_state, stack, 0)?;
78 let path = Spanned {
79 item: expand_path_with(path_arg.item, &cwd, true),
80 span: path_arg.span,
81 };
82
83 let stderr_path = call
84 .get_flag::<Spanned<PathBuf>>(engine_state, stack, "stderr")?
85 .map(|arg| Spanned {
86 item: expand_path_with(arg.item, cwd, true),
87 span: arg.span,
88 });
89
90 let from_io_error = IoError::factory(span, path.item.as_path());
91 match input {
92 PipelineData::ByteStream(stream, metadata) => {
93 check_saving_to_source_file(metadata.as_ref(), &path, stderr_path.as_ref())?;
94
95 let (file, stderr_file) =
96 get_files(engine_state, &path, stderr_path.as_ref(), append, force)?;
97
98 let size = stream.known_size();
99 let signals = engine_state.signals();
100
101 match stream.into_source() {
102 ByteStreamSource::Read(read) => {
103 stream_to_file(read, size, signals, file, span, progress)?;
104 }
105 ByteStreamSource::File(source) => {
106 stream_to_file(source, size, signals, file, span, progress)?;
107 }
108 #[cfg(feature = "os")]
109 ByteStreamSource::Child(mut child) => {
110 fn write_or_consume_stderr(
111 stderr: ChildPipe,
112 file: Option<File>,
113 span: Span,
114 signals: &Signals,
115 progress: bool,
116 ) -> Result<(), ShellError> {
117 if let Some(file) = file {
118 match stderr {
119 ChildPipe::Pipe(pipe) => {
120 stream_to_file(pipe, None, signals, file, span, progress)
121 }
122 ChildPipe::Tee(tee) => {
123 stream_to_file(tee, None, signals, file, span, progress)
124 }
125 }?
126 } else {
127 match stderr {
128 ChildPipe::Pipe(mut pipe) => {
129 io::copy(&mut pipe, &mut io::stderr())
130 }
131 ChildPipe::Tee(mut tee) => {
132 io::copy(&mut tee, &mut io::stderr())
133 }
134 }
135 .map_err(|err| IoError::new(err, span, None))?;
136 }
137 Ok(())
138 }
139
140 match (child.stdout.take(), child.stderr.take()) {
141 (Some(stdout), stderr) => {
142 let handler = stderr
144 .map(|stderr| {
145 let signals = signals.clone();
146 thread::Builder::new().name("stderr saver".into()).spawn(
147 move || {
148 write_or_consume_stderr(
149 stderr,
150 stderr_file,
151 span,
152 &signals,
153 progress,
154 )
155 },
156 )
157 })
158 .transpose()
159 .map_err(&from_io_error)?;
160
161 let res = match stdout {
162 ChildPipe::Pipe(pipe) => {
163 stream_to_file(pipe, None, signals, file, span, progress)
164 }
165 ChildPipe::Tee(tee) => {
166 stream_to_file(tee, None, signals, file, span, progress)
167 }
168 };
169 if let Some(h) = handler {
170 h.join().map_err(|err| ShellError::ExternalCommand {
171 label: "Fail to receive external commands stderr message"
172 .to_string(),
173 help: format!("{err:?}"),
174 span,
175 })??;
176 }
177 res?;
178 }
179 (None, Some(stderr)) => {
180 write_or_consume_stderr(
181 stderr,
182 stderr_file,
183 span,
184 signals,
185 progress,
186 )?;
187 }
188 (None, None) => {}
189 };
190
191 child.wait()?;
192 }
193 }
194
195 Ok(PipelineData::empty())
196 }
197 PipelineData::ListStream(ls, pipeline_metadata)
198 if raw || prepare_path(&path, append, force)?.0.extension().is_none() =>
199 {
200 check_saving_to_source_file(
201 pipeline_metadata.as_ref(),
202 &path,
203 stderr_path.as_ref(),
204 )?;
205
206 let (mut file, _) =
207 get_files(engine_state, &path, stderr_path.as_ref(), append, force)?;
208 for val in ls {
209 file.write_all(&value_to_bytes(val)?)
210 .map_err(&from_io_error)?;
211 file.write_all("\n".as_bytes()).map_err(&from_io_error)?;
212 }
213 file.flush().map_err(&from_io_error)?;
214
215 Ok(PipelineData::empty())
216 }
217 input => {
218 if !matches!(input, PipelineData::Value(..) | PipelineData::Empty) {
221 check_saving_to_source_file(
222 input.metadata().as_ref(),
223 &path,
224 stderr_path.as_ref(),
225 )?;
226 }
227
228 let ext = extract_extension(&input, &path.item, raw);
230 let converted = match ext {
231 None => input,
232 Some(ext) => convert_to_extension(engine_state, &ext, stack, input, span)?,
233 };
234
235 if let PipelineData::Value(v @ Value::Custom { .. }, ..) = converted {
237 let val_span = v.span();
238 let val = v.into_custom_value()?;
239 return val
240 .save(
241 Spanned {
242 item: &path.item,
243 span: path.span,
244 },
245 val_span,
246 span,
247 )
248 .map(|()| PipelineData::empty());
249 }
250
251 let bytes = value_to_bytes(converted.into_value(span)?)?;
252
253 let (mut file, _) =
255 get_files(engine_state, &path, stderr_path.as_ref(), append, force)?;
256
257 file.write_all(&bytes).map_err(&from_io_error)?;
258 file.flush().map_err(&from_io_error)?;
259
260 Ok(PipelineData::empty())
261 }
262 }
263 }
264
265 fn examples(&self) -> Vec<Example<'_>> {
266 vec![
267 Example {
268 description: "Save a string to foo.txt in the current directory",
269 example: r#"'save me' | save foo.txt"#,
270 result: None,
271 },
272 Example {
273 description: "Append a string to the end of foo.txt",
274 example: r#"'append me' | save --append foo.txt"#,
275 result: None,
276 },
277 Example {
278 description: "Save a record to foo.json in the current directory",
279 example: r#"{ a: 1, b: 2 } | save foo.json"#,
280 result: None,
281 },
282 Example {
283 description: "Save a running program's stderr to foo.txt",
284 example: r#"do -i {} | save foo.txt --stderr foo.txt"#,
285 result: None,
286 },
287 Example {
288 description: "Save a running program's stderr to separate file",
289 example: r#"do -i {} | save foo.txt --stderr bar.txt"#,
290 result: None,
291 },
292 Example {
293 description: "Show the extensions for which the `save` command will automatically serialize",
294 example: r#"scope commands
295 | where name starts-with "to "
296 | insert extension { get name | str replace -r "^to " "" | $"*.($in)" }
297 | select extension name
298 | rename extension command
299"#,
300 result: None,
301 },
302 ]
303 }
304
305 fn pipe_redirection(&self) -> (Option<OutDest>, Option<OutDest>) {
306 (Some(OutDest::PipeSeparate), Some(OutDest::PipeSeparate))
307 }
308}
309
310fn saving_to_source_file_error(dest: &Spanned<PathBuf>) -> ShellError {
311 ShellError::GenericError {
312 error: "pipeline input and output are the same file".into(),
313 msg: format!(
314 "can't save output to '{}' while it's being read",
315 dest.item.display()
316 ),
317 span: Some(dest.span),
318 help: Some(
319 "insert a `collect` command in the pipeline before `save` (see `help collect`).".into(),
320 ),
321 inner: vec![],
322 }
323}
324
325fn check_saving_to_source_file(
326 metadata: Option<&PipelineMetadata>,
327 dest: &Spanned<PathBuf>,
328 stderr_dest: Option<&Spanned<PathBuf>>,
329) -> Result<(), ShellError> {
330 let Some(DataSource::FilePath(source)) = metadata.map(|meta| &meta.data_source) else {
331 return Ok(());
332 };
333
334 if &dest.item == source {
335 return Err(saving_to_source_file_error(dest));
336 }
337
338 if let Some(dest) = stderr_dest
339 && &dest.item == source
340 {
341 return Err(saving_to_source_file_error(dest));
342 }
343
344 Ok(())
345}
346
347fn extract_extension<'e>(input: &PipelineData, path: &'e Path, raw: bool) -> Option<Cow<'e, str>> {
349 match (raw, input) {
350 (true, _)
351 | (_, PipelineData::ByteStream(..))
352 | (_, PipelineData::Value(Value::String { .. }, ..)) => None,
353 _ => path.extension().map(|name| name.to_string_lossy()),
354 }
355}
356
357fn convert_to_extension(
361 engine_state: &EngineState,
362 extension: &str,
363 stack: &mut Stack,
364 input: PipelineData,
365 span: Span,
366) -> Result<PipelineData, ShellError> {
367 if let Some(decl_id) = engine_state.find_decl(format!("to {extension}").as_bytes(), &[]) {
368 let decl = engine_state.get_decl(decl_id);
369 if let Some(block_id) = decl.block_id() {
370 let block = engine_state.get_block(block_id);
371 let eval_block = get_eval_block(engine_state);
372 eval_block(engine_state, stack, block, input).map(|p| p.body)
373 } else {
374 let call = ast::Call::new(span);
375 decl.run(engine_state, stack, &(&call).into(), input)
376 }
377 } else {
378 Ok(input)
379 }
380}
381
382fn value_to_bytes(value: Value) -> Result<Vec<u8>, ShellError> {
386 match value {
387 Value::String { val, .. } => Ok(val.into_bytes()),
388 Value::Binary { val, .. } => Ok(val),
389 Value::List { vals, .. } => {
390 let val = vals
391 .into_iter()
392 .map(Value::coerce_into_string)
393 .collect::<Result<Vec<String>, ShellError>>()?
394 .join("\n")
395 + "\n";
396
397 Ok(val.into_bytes())
398 }
399 Value::Error { error, .. } => Err(*error),
401 other => Ok(other.coerce_into_string()?.into_bytes()),
402 }
403}
404
405fn prepare_path(
408 path: &Spanned<PathBuf>,
409 append: bool,
410 force: bool,
411) -> Result<(&Path, Span), ShellError> {
412 let span = path.span;
413 let path = &path.item;
414
415 if !(force || append) && path.exists() {
416 Err(ShellError::GenericError {
417 error: "Destination file already exists".into(),
418 msg: format!(
419 "Destination file '{}' already exists",
420 path.to_string_lossy()
421 ),
422 span: Some(span),
423 help: Some("you can use -f, --force to force overwriting the destination".into()),
424 inner: vec![],
425 })
426 } else {
427 Ok((path, span))
428 }
429}
430
431fn open_file(
432 engine_state: &EngineState,
433 path: &Path,
434 span: Span,
435 append: bool,
436) -> Result<File, ShellError> {
437 let file: std::io::Result<File> = match (append, path.exists() || is_windows_device_path(path))
438 {
439 (true, true) => std::fs::OpenOptions::new().append(true).open(path),
440 _ => {
441 #[cfg(target_os = "windows")]
444 if path.is_dir() {
445 #[allow(
446 deprecated,
447 reason = "we don't get a IsADirectory error, so we need to provide it"
448 )]
449 Err(std::io::ErrorKind::IsADirectory.into())
450 } else {
451 std::fs::File::create(path)
452 }
453 #[cfg(not(target_os = "windows"))]
454 std::fs::File::create(path)
455 }
456 };
457
458 match file {
459 Ok(file) => Ok(file),
460 Err(err) => {
461 if err.kind() == std::io::ErrorKind::NotFound
464 && let Some(missing_component) =
465 path.ancestors().skip(1).filter(|dir| !dir.exists()).last()
466 {
467 let components_to_remove = path
470 .strip_prefix(missing_component)
471 .expect("Stripping ancestor from a path should never fail")
472 .as_os_str()
473 .as_encoded_bytes();
474
475 return Err(ShellError::Io(IoError::new(
476 ErrorKind::DirectoryNotFound,
477 engine_state
478 .span_match_postfix(span, components_to_remove)
479 .map(|(pre, _post)| pre)
480 .unwrap_or(span),
481 PathBuf::from(missing_component),
482 )));
483 }
484
485 Err(ShellError::Io(IoError::new(err, span, PathBuf::from(path))))
486 }
487 }
488}
489
490fn get_files(
492 engine_state: &EngineState,
493 path: &Spanned<PathBuf>,
494 stderr_path: Option<&Spanned<PathBuf>>,
495 append: bool,
496 force: bool,
497) -> Result<(File, Option<File>), ShellError> {
498 let (path, path_span) = prepare_path(path, append, force)?;
500 let stderr_path_and_span = stderr_path
501 .as_ref()
502 .map(|stderr_path| prepare_path(stderr_path, append, force))
503 .transpose()?;
504
505 let file = open_file(engine_state, path, path_span, append)?;
507
508 let stderr_file = stderr_path_and_span
509 .map(|(stderr_path, stderr_path_span)| {
510 if path == stderr_path {
511 Err(ShellError::GenericError {
512 error: "input and stderr input to same file".into(),
513 msg: "can't save both input and stderr input to the same file".into(),
514 span: Some(stderr_path_span),
515 help: Some("you should use `o+e> file` instead".into()),
516 inner: vec![],
517 })
518 } else {
519 open_file(engine_state, stderr_path, stderr_path_span, append)
520 }
521 })
522 .transpose()?;
523
524 Ok((file, stderr_file))
525}
526
527fn stream_to_file(
528 source: impl Read,
529 known_size: Option<u64>,
530 signals: &Signals,
531 mut file: File,
532 span: Span,
533 progress: bool,
534) -> Result<(), ShellError> {
535 let from_io_error = IoError::factory(span, None);
537
538 if progress {
540 let mut bytes_processed = 0;
541
542 let mut bar = progress_bar::NuProgressBar::new(known_size);
543
544 let mut last_update = Instant::now();
545
546 let mut reader = BufReader::new(source);
547
548 let res = loop {
549 if let Err(err) = signals.check(&span) {
550 bar.abandoned_msg("# Cancelled #".to_owned());
551 return Err(err);
552 }
553
554 match reader.fill_buf() {
555 Ok(&[]) => break Ok(()),
556 Ok(buf) => {
557 file.write_all(buf).map_err(&from_io_error)?;
558 let len = buf.len();
559 reader.consume(len);
560 bytes_processed += len as u64;
561 if last_update.elapsed() >= Duration::from_millis(75) {
562 bar.update_bar(bytes_processed);
563 last_update = Instant::now();
564 }
565 }
566 Err(e) if e.kind() == io::ErrorKind::Interrupted => continue,
567 Err(e) => break Err(e),
568 }
569 };
570
571 if let Err(err) = res {
573 let _ = file.flush();
574 bar.abandoned_msg("# Error while saving #".to_owned());
575 Err(from_io_error(err).into())
576 } else {
577 file.flush().map_err(&from_io_error)?;
578 Ok(())
579 }
580 } else {
581 copy_with_signals(source, file, span, signals)?;
582 Ok(())
583 }
584}