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