1use crate::progress_bar;
2use nu_engine::{command_prelude::*, get_eval_block};
3use nu_path::{expand_path_with, is_windows_device_path};
4use nu_protocol::{
5 ByteStreamSource, DataSource, OutDest, PipelineMetadata, Signals, ast,
6 byte_stream::copy_with_signals, process::ChildPipe, shell_error::generic::GenericError,
7 shell_error::io::IoError,
8};
9use std::{
10 borrow::Cow,
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 let cwd = engine_state.cwd(Some(stack))?.into_std_path_buf();
74
75 let path_arg = call.req::<Spanned<PathBuf>>(engine_state, stack, 0)?;
76 let path = Spanned {
77 item: expand_path_with(path_arg.item, &cwd, true),
78 span: path_arg.span,
79 };
80
81 let stderr_path = call
82 .get_flag::<Spanned<PathBuf>>(engine_state, stack, "stderr")?
83 .map(|arg| Spanned {
84 item: expand_path_with(arg.item, cwd, true),
85 span: arg.span,
86 });
87
88 let from_io_error = IoError::factory(span, path.item.as_path());
89 match input {
90 PipelineData::ByteStream(stream, metadata) => {
91 check_saving_to_source_file(metadata.as_ref(), &path, stderr_path.as_ref())?;
92
93 let (file, stderr_file) =
94 get_files(engine_state, &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 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, _) =
205 get_files(engine_state, &path, stderr_path.as_ref(), append, force)?;
206 for val in ls {
207 file.write_all(&value_to_bytes(val)?)
208 .map_err(&from_io_error)?;
209 file.write_all("\n".as_bytes()).map_err(&from_io_error)?;
210 }
211 file.flush().map_err(&from_io_error)?;
212
213 Ok(PipelineData::empty())
214 }
215 input => {
216 if !matches!(input, PipelineData::Value(..) | PipelineData::Empty) {
219 check_saving_to_source_file(
220 input.metadata().as_ref(),
221 &path,
222 stderr_path.as_ref(),
223 )?;
224 }
225
226 let ext = extract_extension(&input, &path.item, raw);
228 let converted = match ext {
229 None => input,
230 Some(ext) => convert_to_extension(engine_state, &ext, stack, input, span)?,
231 };
232
233 if let PipelineData::Value(v @ Value::Custom { .. }, ..) = converted {
235 let val_span = v.span();
236 let val = v.into_custom_value()?;
237 return val
238 .save(
239 Spanned {
240 item: &path.item,
241 span: path.span,
242 },
243 val_span,
244 span,
245 )
246 .map(|()| PipelineData::empty());
247 }
248
249 let bytes = value_to_bytes(converted.into_value(span)?)?;
250
251 let (mut file, _) =
253 get_files(engine_state, &path, stderr_path.as_ref(), append, force)?;
254
255 file.write_all(&bytes).map_err(&from_io_error)?;
256 file.flush().map_err(&from_io_error)?;
257
258 Ok(PipelineData::empty())
259 }
260 }
261 }
262
263 fn examples(&self) -> Vec<Example<'_>> {
264 vec![
265 Example {
266 description: "Save a string to foo.txt in the current directory.",
267 example: "'save me' | save foo.txt",
268 result: None,
269 },
270 Example {
271 description: "Append a string to the end of foo.txt.",
272 example: "'append me' | save --append foo.txt",
273 result: None,
274 },
275 Example {
276 description: "Save a record to foo.json in the current directory.",
277 example: "{ a: 1, b: 2 } | save foo.json",
278 result: None,
279 },
280 Example {
281 description: "Save a running program's stderr to foo.txt.",
282 example: "do -i {} | save foo.txt --stderr foo.txt",
283 result: None,
284 },
285 Example {
286 description: "Save a running program's stderr to separate file.",
287 example: "do -i {} | save foo.txt --stderr bar.txt",
288 result: None,
289 },
290 Example {
291 description: "Show the extensions for which the `save` command will automatically serialize.",
292 example: r#"scope commands
293 | where name starts-with "to "
294 | insert extension { get name | str replace -r "^to " "" | $"*.($in)" }
295 | select extension name
296 | rename extension command
297"#,
298 result: None,
299 },
300 ]
301 }
302
303 fn pipe_redirection(&self) -> (Option<OutDest>, Option<OutDest>) {
304 (Some(OutDest::PipeSeparate), Some(OutDest::PipeSeparate))
305 }
306}
307
308fn saving_to_source_file_error(dest: &Spanned<PathBuf>) -> ShellError {
309 ShellError::Generic(
310 GenericError::new(
311 "pipeline input and output are the same file",
312 format!(
313 "can't save output to '{}' while it's being read",
314 dest.item.display()
315 ),
316 dest.span,
317 )
318 .with_help(
319 "insert a `collect` command in the pipeline before `save` (see `help collect`).",
320 ),
321 )
322}
323
324fn check_saving_to_source_file(
325 metadata: Option<&PipelineMetadata>,
326 dest: &Spanned<PathBuf>,
327 stderr_dest: Option<&Spanned<PathBuf>>,
328) -> Result<(), ShellError> {
329 let Some(DataSource::FilePath(source)) = metadata.map(|meta| &meta.data_source) else {
330 return Ok(());
331 };
332
333 if &dest.item == source {
334 return Err(saving_to_source_file_error(dest));
335 }
336
337 if let Some(dest) = stderr_dest
338 && &dest.item == source
339 {
340 return Err(saving_to_source_file_error(dest));
341 }
342
343 Ok(())
344}
345
346fn extract_extension<'e>(input: &PipelineData, path: &'e Path, raw: bool) -> Option<Cow<'e, str>> {
348 match (raw, input) {
349 (true, _)
350 | (_, PipelineData::ByteStream(..))
351 | (_, PipelineData::Value(Value::String { .. }, ..)) => None,
352 _ => path.extension().map(|name| name.to_string_lossy()),
353 }
354}
355
356fn convert_to_extension(
360 engine_state: &EngineState,
361 extension: &str,
362 stack: &mut Stack,
363 input: PipelineData,
364 span: Span,
365) -> Result<PipelineData, ShellError> {
366 if let Some(decl_id) = engine_state.find_decl(format!("to {extension}").as_bytes(), &[]) {
367 let decl = engine_state.get_decl(decl_id);
368 if let Some(block_id) = decl.block_id() {
369 let block = engine_state.get_block(block_id);
370 let eval_block = get_eval_block(engine_state);
371 eval_block(engine_state, stack, block, input).map(|p| p.body)
372 } else {
373 let call = ast::Call::new(span);
374 decl.run(engine_state, stack, &(&call).into(), input)
375 }
376 } else {
377 Ok(input)
378 }
379}
380
381fn value_to_bytes(value: Value) -> Result<Vec<u8>, ShellError> {
385 match value {
386 Value::String { val, .. } => Ok(val.into_bytes()),
387 Value::Binary { val, .. } => Ok(val),
388 Value::List { vals, .. } => {
389 let val = vals
390 .into_iter()
391 .map(Value::coerce_into_string)
392 .collect::<Result<Vec<String>, ShellError>>()?
393 .join("\n")
394 + "\n";
395
396 Ok(val.into_bytes())
397 }
398 Value::Error { error, .. } => Err(*error),
400 other => Ok(other.coerce_into_string()?.into_bytes()),
401 }
402}
403
404fn prepare_path(
407 path: &Spanned<PathBuf>,
408 append: bool,
409 force: bool,
410) -> Result<(&Path, Span), ShellError> {
411 let span = path.span;
412 let path = &path.item;
413
414 if !(force || append) && path.exists() {
415 Err(ShellError::Generic(
416 GenericError::new(
417 "Destination file already exists",
418 format!(
419 "Destination file '{}' already exists",
420 path.to_string_lossy()
421 ),
422 span,
423 )
424 .with_help("you can use -f, --force to force overwriting the destination"),
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::Generic(
512 GenericError::new(
513 "input and stderr input to same file",
514 "can't save both input and stderr input to the same file",
515 stderr_path_span,
516 )
517 .with_help("you should use `o+e> file` instead"),
518 ))
519 } else {
520 open_file(engine_state, stderr_path, stderr_path_span, append)
521 }
522 })
523 .transpose()?;
524
525 Ok((file, stderr_file))
526}
527
528fn stream_to_file(
529 source: impl Read,
530 known_size: Option<u64>,
531 signals: &Signals,
532 mut file: File,
533 span: Span,
534 progress: bool,
535) -> Result<(), ShellError> {
536 let from_io_error = IoError::factory(span, None);
538
539 if progress {
541 let mut bytes_processed = 0;
542
543 let mut bar = progress_bar::NuProgressBar::new(known_size);
544
545 let mut last_update = Instant::now();
546
547 let mut reader = BufReader::new(source);
548
549 let res = loop {
550 if let Err(err) = signals.check(&span) {
551 bar.abandoned_msg("# Cancelled #".to_owned());
552 return Err(err);
553 }
554
555 match reader.fill_buf() {
556 Ok(&[]) => break Ok(()),
557 Ok(buf) => {
558 file.write_all(buf).map_err(&from_io_error)?;
559 let len = buf.len();
560 reader.consume(len);
561 bytes_processed += len as u64;
562 if last_update.elapsed() >= Duration::from_millis(75) {
563 bar.update_bar(bytes_processed);
564 last_update = Instant::now();
565 }
566 }
567 Err(e) if e.kind() == io::ErrorKind::Interrupted => continue,
568 Err(e) => break Err(e),
569 }
570 };
571
572 if let Err(err) = res {
574 let _ = file.flush();
575 bar.abandoned_msg("# Error while saving #".to_owned());
576 Err(from_io_error(err).into())
577 } else {
578 file.flush().map_err(&from_io_error)?;
579 Ok(())
580 }
581 } else {
582 copy_with_signals(source, file, span, signals)?;
583 Ok(())
584 }
585}