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 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 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 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
321fn 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
351fn 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
376fn 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 Value::Error { error, .. } => Err(*error),
395 other => Ok(other.coerce_into_string()?.into_bytes()),
396 }
397}
398
399fn 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 #[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
455fn 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 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 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 let from_io_error = IoError::factory(span, None);
501
502 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 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}