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 ast, byte_stream::copy_with_signals, process::ChildPipe, shell_error::io::IoError,
8 ByteStreamSource, DataSource, OutDest, PipelineMetadata, Signals,
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.kind(), 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 ]
267 }
268
269 fn pipe_redirection(&self) -> (Option<OutDest>, Option<OutDest>) {
270 (Some(OutDest::PipeSeparate), Some(OutDest::PipeSeparate))
271 }
272}
273
274fn saving_to_source_file_error(dest: &Spanned<PathBuf>) -> ShellError {
275 ShellError::GenericError {
276 error: "pipeline input and output are the same file".into(),
277 msg: format!(
278 "can't save output to '{}' while it's being read",
279 dest.item.display()
280 ),
281 span: Some(dest.span),
282 help: Some(
283 "insert a `collect` command in the pipeline before `save` (see `help collect`).".into(),
284 ),
285 inner: vec![],
286 }
287}
288
289fn check_saving_to_source_file(
290 metadata: Option<&PipelineMetadata>,
291 dest: &Spanned<PathBuf>,
292 stderr_dest: Option<&Spanned<PathBuf>>,
293) -> Result<(), ShellError> {
294 let Some(DataSource::FilePath(source)) = metadata.map(|meta| &meta.data_source) else {
295 return Ok(());
296 };
297
298 if &dest.item == source {
299 return Err(saving_to_source_file_error(dest));
300 }
301
302 if let Some(dest) = stderr_dest {
303 if &dest.item == source {
304 return Err(saving_to_source_file_error(dest));
305 }
306 }
307
308 Ok(())
309}
310
311fn input_to_bytes(
314 input: PipelineData,
315 path: &Path,
316 raw: bool,
317 engine_state: &EngineState,
318 stack: &mut Stack,
319 span: Span,
320) -> Result<Vec<u8>, ShellError> {
321 let ext = if raw {
322 None
323 } else if let PipelineData::ByteStream(..) = input {
324 None
325 } else if let PipelineData::Value(Value::String { .. }, ..) = input {
326 None
327 } else {
328 path.extension()
329 .map(|name| name.to_string_lossy().to_string())
330 };
331
332 let input = if let Some(ext) = ext {
333 convert_to_extension(engine_state, &ext, stack, input, span)?
334 } else {
335 input
336 };
337
338 value_to_bytes(input.into_value(span)?)
339}
340
341fn convert_to_extension(
345 engine_state: &EngineState,
346 extension: &str,
347 stack: &mut Stack,
348 input: PipelineData,
349 span: Span,
350) -> Result<PipelineData, ShellError> {
351 if let Some(decl_id) = engine_state.find_decl(format!("to {extension}").as_bytes(), &[]) {
352 let decl = engine_state.get_decl(decl_id);
353 if let Some(block_id) = decl.block_id() {
354 let block = engine_state.get_block(block_id);
355 let eval_block = get_eval_block(engine_state);
356 eval_block(engine_state, stack, block, input)
357 } else {
358 let call = ast::Call::new(span);
359 decl.run(engine_state, stack, &(&call).into(), input)
360 }
361 } else {
362 Ok(input)
363 }
364}
365
366fn value_to_bytes(value: Value) -> Result<Vec<u8>, ShellError> {
370 match value {
371 Value::String { val, .. } => Ok(val.into_bytes()),
372 Value::Binary { val, .. } => Ok(val),
373 Value::List { vals, .. } => {
374 let val = vals
375 .into_iter()
376 .map(Value::coerce_into_string)
377 .collect::<Result<Vec<String>, ShellError>>()?
378 .join("\n")
379 + "\n";
380
381 Ok(val.into_bytes())
382 }
383 Value::Error { error, .. } => Err(*error),
385 other => Ok(other.coerce_into_string()?.into_bytes()),
386 }
387}
388
389fn prepare_path(
392 path: &Spanned<PathBuf>,
393 append: bool,
394 force: bool,
395) -> Result<(&Path, Span), ShellError> {
396 let span = path.span;
397 let path = &path.item;
398
399 if !(force || append) && path.exists() {
400 Err(ShellError::GenericError {
401 error: "Destination file already exists".into(),
402 msg: format!(
403 "Destination file '{}' already exists",
404 path.to_string_lossy()
405 ),
406 span: Some(span),
407 help: Some("you can use -f, --force to force overwriting the destination".into()),
408 inner: vec![],
409 })
410 } else {
411 Ok((path, span))
412 }
413}
414
415fn open_file(path: &Path, span: Span, append: bool) -> Result<File, ShellError> {
416 let file: Result<File, nu_protocol::shell_error::io::ErrorKind> = match (append, path.exists())
417 {
418 (true, true) => std::fs::OpenOptions::new()
419 .append(true)
420 .open(path)
421 .map_err(|err| err.kind().into()),
422 _ => {
423 #[cfg(target_os = "windows")]
426 if path.is_dir() {
427 Err(nu_protocol::shell_error::io::ErrorKind::Std(
428 std::io::ErrorKind::IsADirectory,
429 ))
430 } else {
431 std::fs::File::create(path).map_err(|err| err.kind().into())
432 }
433 #[cfg(not(target_os = "windows"))]
434 std::fs::File::create(path).map_err(|err| err.kind().into())
435 }
436 };
437
438 file.map_err(|err_kind| ShellError::Io(IoError::new(err_kind, span, PathBuf::from(path))))
439}
440
441fn get_files(
443 path: &Spanned<PathBuf>,
444 stderr_path: Option<&Spanned<PathBuf>>,
445 append: bool,
446 force: bool,
447) -> Result<(File, Option<File>), ShellError> {
448 let (path, path_span) = prepare_path(path, append, force)?;
450 let stderr_path_and_span = stderr_path
451 .as_ref()
452 .map(|stderr_path| prepare_path(stderr_path, append, force))
453 .transpose()?;
454
455 let file = open_file(path, path_span, append)?;
457
458 let stderr_file = stderr_path_and_span
459 .map(|(stderr_path, stderr_path_span)| {
460 if path == stderr_path {
461 Err(ShellError::GenericError {
462 error: "input and stderr input to same file".into(),
463 msg: "can't save both input and stderr input to the same file".into(),
464 span: Some(stderr_path_span),
465 help: Some("you should use `o+e> file` instead".into()),
466 inner: vec![],
467 })
468 } else {
469 open_file(stderr_path, stderr_path_span, append)
470 }
471 })
472 .transpose()?;
473
474 Ok((file, stderr_file))
475}
476
477fn stream_to_file(
478 source: impl Read,
479 known_size: Option<u64>,
480 signals: &Signals,
481 mut file: File,
482 span: Span,
483 progress: bool,
484) -> Result<(), ShellError> {
485 let from_io_error = IoError::factory(span, None);
487
488 if progress {
490 let mut bytes_processed = 0;
491
492 let mut bar = progress_bar::NuProgressBar::new(known_size);
493
494 let mut last_update = Instant::now();
495
496 let mut reader = BufReader::new(source);
497
498 let res = loop {
499 if let Err(err) = signals.check(span) {
500 bar.abandoned_msg("# Cancelled #".to_owned());
501 return Err(err);
502 }
503
504 match reader.fill_buf() {
505 Ok(&[]) => break Ok(()),
506 Ok(buf) => {
507 file.write_all(buf).map_err(&from_io_error)?;
508 let len = buf.len();
509 reader.consume(len);
510 bytes_processed += len as u64;
511 if last_update.elapsed() >= Duration::from_millis(75) {
512 bar.update_bar(bytes_processed);
513 last_update = Instant::now();
514 }
515 }
516 Err(e) if e.kind() == io::ErrorKind::Interrupted => continue,
517 Err(e) => break Err(e),
518 }
519 };
520
521 if let Err(err) = res {
523 let _ = file.flush();
524 bar.abandoned_msg("# Error while saving #".to_owned());
525 Err(from_io_error(err).into())
526 } else {
527 file.flush().map_err(&from_io_error)?;
528 Ok(())
529 }
530 } else {
531 copy_with_signals(source, file, span, signals)?;
532 Ok(())
533 }
534}