1#![allow(clippy::byte_char_slices)]
2
3use nu_cmd_base::hook::eval_hook;
4use nu_engine::{eval_block, eval_block_with_early_return};
5use nu_parser::{Token, TokenContents, lex, parse, unescape_unquote_string};
6use nu_protocol::{
7 ByteStream, ByteStreamSource, ListStream, PipelineData, PipelineMetadata, ShellError, Signals,
8 Span, Value,
9 ast::Block,
10 block_is_bare_last_result_with,
11 debugger::WithoutDebug,
12 engine::{EngineState, Stack, StateWorkingSet},
13 process::check_exit_status_future,
14 report_error::report_compile_error,
15 report_parse_error, report_parse_warning, report_shell_error,
16 shell_error::{generic::GenericError, io::IoError},
17 truncate_value_to_budget, value_from_bytes,
18};
19#[cfg(windows)]
20use nu_utils::enable_vt_processing;
21use nu_utils::time::Instant;
22use nu_utils::{escape_quote_string, perf};
23use std::path::Path;
24
25pub fn gather_parent_env_vars(engine_state: &mut EngineState, init_cwd: &Path) {
44 gather_env_vars(std::env::vars(), engine_state, init_cwd);
45}
46
47fn gather_env_vars(
48 vars: impl Iterator<Item = (String, String)>,
49 engine_state: &mut EngineState,
50 init_cwd: &Path,
51) {
52 fn report_capture_error(engine_state: &EngineState, env_str: &str, msg: &str) {
53 report_shell_error(
54 None,
55 engine_state,
56 &ShellError::Generic(
57 GenericError::new_internal(
58 format!("Environment variable was not captured: {env_str}"),
59 "",
60 )
61 .with_help(msg.to_string()),
62 ),
63 );
64 }
65
66 fn put_env_to_fake_file(name: &str, val: &str, fake_env_file: &mut String) {
67 fake_env_file.push_str(&escape_quote_string(name));
68 fake_env_file.push('=');
69 fake_env_file.push_str(&escape_quote_string(val));
70 fake_env_file.push('\n');
71 }
72
73 let mut fake_env_file = String::new();
74 for (name, val) in vars {
76 put_env_to_fake_file(&name, &val, &mut fake_env_file);
77 }
78
79 match init_cwd.to_str() {
80 Some(cwd) => {
81 put_env_to_fake_file("PWD", cwd, &mut fake_env_file);
82 }
83 None => {
84 report_shell_error(
86 None,
87 engine_state,
88 &ShellError::Generic(
89 GenericError::new_internal("Current directory is not a valid utf-8 path", "")
90 .with_help(format!(
91 "Retrieving current directory failed: {init_cwd:?} not a valid utf-8 path"
92 )),
93 ),
94 );
95 }
96 }
97
98 let span_offset = engine_state.next_span_start();
101
102 engine_state.add_file(
103 "Host Environment Variables".into(),
104 fake_env_file.as_bytes().into(),
105 );
106
107 let (tokens, _) = lex(fake_env_file.as_bytes(), span_offset, &[], &[], true);
108
109 for token in tokens {
110 if let Token {
111 contents: TokenContents::Item,
112 span: full_span,
113 } = token
114 {
115 let contents = engine_state.get_span_contents(full_span);
116 let (parts, _) = lex(contents, full_span.start, &[], &[b'='], true);
117
118 let name = if let Some(Token {
119 contents: TokenContents::Item,
120 span,
121 }) = parts.first()
122 {
123 let mut working_set = StateWorkingSet::new(engine_state);
124 let bytes = working_set.get_span_contents(*span);
125
126 if bytes.len() < 2 {
127 report_capture_error(
128 engine_state,
129 &String::from_utf8_lossy(contents),
130 "Got empty name.",
131 );
132
133 continue;
134 }
135
136 let (bytes, err) = unescape_unquote_string(bytes, *span);
137 if let Some(err) = err {
138 working_set.error(err);
139 }
140
141 if !working_set.parse_errors.is_empty() {
142 report_capture_error(
143 engine_state,
144 &String::from_utf8_lossy(contents),
145 "Got unparsable name.",
146 );
147
148 continue;
149 }
150
151 bytes
152 } else {
153 report_capture_error(
154 engine_state,
155 &String::from_utf8_lossy(contents),
156 "Got empty name.",
157 );
158
159 continue;
160 };
161
162 let value = if let Some(Token {
163 contents: TokenContents::Item,
164 span,
165 }) = parts.get(2)
166 {
167 let mut working_set = StateWorkingSet::new(engine_state);
168 let bytes = working_set.get_span_contents(*span);
169
170 if bytes.len() < 2 {
171 report_capture_error(
172 engine_state,
173 &String::from_utf8_lossy(contents),
174 "Got empty value.",
175 );
176
177 continue;
178 }
179
180 let (bytes, err) = unescape_unquote_string(bytes, *span);
181 if let Some(err) = err {
182 working_set.error(err);
183 }
184
185 if !working_set.parse_errors.is_empty() {
186 report_capture_error(
187 engine_state,
188 &String::from_utf8_lossy(contents),
189 "Got unparsable value.",
190 );
191
192 continue;
193 }
194
195 Value::string(bytes, *span)
196 } else {
197 report_capture_error(
198 engine_state,
199 &String::from_utf8_lossy(contents),
200 "Got empty value.",
201 );
202
203 continue;
204 };
205
206 engine_state.add_env_var(name, value);
208 }
209 }
210}
211
212pub fn print_pipeline(
220 engine_state: &mut EngineState,
221 stack: &mut Stack,
222 pipeline: PipelineData,
223 no_newline: bool,
224) -> Result<(), ShellError> {
225 let to_stderr = engine_state.is_mcp || engine_state.is_lsp;
226
227 if let Some(hook) = stack.get_config(engine_state).hooks.display_output.clone() {
228 let pipeline = eval_hook(
229 engine_state,
230 stack,
231 Some(pipeline),
232 vec![],
233 &hook,
234 "display_output",
235 )?;
236 pipeline.print_raw(engine_state, no_newline, to_stderr)
237 } else {
238 pipeline.print_table(engine_state, stack, no_newline, to_stderr)
240 }
241}
242
243pub fn eval_source(
244 engine_state: &mut EngineState,
245 stack: &mut Stack,
246 source: &[u8],
247 fname: &str,
248 input: PipelineData,
249 allow_return: bool,
250) -> i32 {
251 let start_time = Instant::now();
252
253 let exit_code = match evaluate_source(engine_state, stack, source, fname, input, allow_return) {
254 Ok(failed) => {
255 let code = failed.into();
256 stack.set_last_exit_code(code, Span::unknown());
258 code
259 }
260 Err(err) => map_eval_error_to_exit_code(engine_state, stack, err),
261 };
262
263 finish_eval_source(engine_state, fname, start_time, exit_code)
264}
265
266pub fn eval_parsed_block_source(
271 engine_state: &mut EngineState,
272 stack: &mut Stack,
273 block: &Block,
274 fname: &str,
275 input: PipelineData,
276 allow_return: bool,
277) -> i32 {
278 let start_time = Instant::now();
279
280 let exit_code = match evaluate_parsed_block(engine_state, stack, block, input, allow_return) {
281 Ok(failed) => {
282 let code = failed.into();
283 stack.set_last_exit_code(code, Span::unknown());
284 code
285 }
286 Err(err) => map_eval_error_to_exit_code(engine_state, stack, err),
287 };
288
289 finish_eval_source(engine_state, fname, start_time, exit_code)
290}
291
292fn map_eval_error_to_exit_code(
293 engine_state: &EngineState,
294 stack: &mut Stack,
295 err: ShellError,
296) -> i32 {
297 if let ShellError::Exit { code, .. } = &err {
298 std::process::exit(*code)
299 }
300 report_shell_error(Some(stack), engine_state, &err);
301 let code = err.exit_code();
302 stack.set_last_error(&err);
303 code.unwrap_or(0)
304}
305
306fn finish_eval_source(
307 engine_state: &EngineState,
308 fname: &str,
309 start_time: Instant,
310 exit_code: i32,
311) -> i32 {
312 #[cfg(windows)]
314 {
315 let _ = enable_vt_processing();
316 }
317
318 perf!(
319 &format!("eval_source {}", fname),
320 start_time,
321 engine_state
322 .get_config()
323 .use_ansi_coloring
324 .get(engine_state)
325 );
326
327 exit_code
328}
329
330pub(crate) fn evaluate_source(
331 engine_state: &mut EngineState,
332 stack: &mut Stack,
333 source: &[u8],
334 fname: &str,
335 input: PipelineData,
336 allow_return: bool,
337) -> Result<bool, ShellError> {
338 let (block, delta) = {
339 let mut working_set = StateWorkingSet::new(engine_state);
340 let output = parse(
341 &mut working_set,
342 Some(fname), source,
344 false,
345 );
346 if let Some(warning) = working_set.parse_warnings.first() {
347 report_parse_warning(Some(stack), &working_set, warning);
348 }
349
350 if let Some(err) = working_set.parse_errors.first() {
351 report_parse_error(Some(stack), &working_set, err);
352 return Ok(true);
353 }
354
355 if let Some(err) = working_set.compile_errors.first() {
356 report_compile_error(Some(stack), &working_set, err);
357 return Ok(true);
358 }
359
360 (output, working_set.render())
361 };
362
363 engine_state.merge_delta(delta)?;
364
365 evaluate_parsed_block(engine_state, stack, &block, input, allow_return)
366}
367
368pub(crate) fn evaluate_parsed_block(
372 engine_state: &mut EngineState,
373 stack: &mut Stack,
374 block: &Block,
375 input: PipelineData,
376 allow_return: bool,
377) -> Result<bool, ShellError> {
378 let pipeline = if allow_return {
379 eval_block_with_early_return::<WithoutDebug>(engine_state, stack, block, input)
380 } else {
381 eval_block::<WithoutDebug>(engine_state, stack, block, input)
382 }?;
383 let mut pipeline_data = pipeline.body;
384
385 for var_id in &stack.deletions {
387 if let Some(active_id) = engine_state.scope.active_overlays.last()
388 && let Some((_, overlay)) = engine_state.scope.overlays.get_mut((*active_id).get())
389 {
390 overlay.vars.retain(|_, v| *v != *var_id);
391 }
392 }
393 stack.deletions.clear();
394
395 if engine_state.is_interactive && engine_state.capture_repl_last_result {
398 pipeline_data = maybe_store_last_result(engine_state, stack, block, pipeline_data);
399 }
400
401 let no_newline = matches!(&pipeline_data, &PipelineData::ByteStream(..));
402 print_pipeline(engine_state, stack, pipeline_data, no_newline)?;
403
404 stack.flush_last_result_truncation_warning(engine_state, Span::unknown());
407
408 let pipefail = nu_experimental::PIPE_FAIL.get();
409 if !pipefail {
410 return Ok(false);
411 }
412 check_exit_status_future(pipeline.exit).map(|_| false)
414}
415
416fn maybe_store_last_result(
421 engine_state: &EngineState,
422 stack: &mut Stack,
423 block: &Block,
424 pipeline_data: PipelineData,
425) -> PipelineData {
426 let budget = stack.get_config(engine_state).max_last_result_size_bytes();
427
428 let mut get_block = |id| engine_state.get_block(id).as_ref();
430 if block_is_bare_last_result_with(block, &mut get_block) {
431 return pipeline_data;
432 }
433
434 if budget == 0 {
435 stack.clear_last_result_payload();
437 return pipeline_data;
438 }
439
440 let signals = engine_state.signals().clone();
441
442 match pipeline_data {
443 PipelineData::Empty => {
444 stack.set_last_result(Value::nothing(Span::unknown()), None, budget);
445 PipelineData::Empty
446 }
447 PipelineData::Value(value, metadata) => {
448 stack.set_last_result(value.clone(), metadata.clone(), budget);
449 PipelineData::Value(value, metadata)
450 }
451 PipelineData::ListStream(stream, metadata) => {
452 store_list_stream_prefix(stack, stream, metadata, budget, signals)
453 }
454 PipelineData::ByteStream(stream, metadata) => {
455 store_byte_stream_prefix(stack, stream, metadata, budget)
456 }
457 }
458}
459
460fn store_list_stream_prefix(
466 stack: &mut Stack,
467 stream: ListStream,
468 metadata: Option<PipelineMetadata>,
469 budget: usize,
470 signals: Signals,
471) -> PipelineData {
472 let span = stream.span();
473 let mut kept: Vec<Value> = Vec::new();
474 let mut used = Value::list(vec![], span).memory_size();
475 let mut truncated = false;
476 let mut overflow_item: Option<Value> = None;
477
478 let mut iter = stream.into_iter();
479 for item in iter.by_ref() {
480 let item_size = item.memory_size();
481 if used.saturating_add(item_size) <= budget {
482 used += item_size;
483 kept.push(item);
484 continue;
485 }
486
487 truncated = true;
489 let table_like = matches!(item, Value::Record { .. })
490 && (kept.is_empty() || kept.iter().all(|v| matches!(v, Value::Record { .. })));
491
492 if table_like {
493 overflow_item = Some(item);
495 break;
496 }
497
498 let remaining = budget.saturating_sub(used);
500 if remaining > 0 {
501 let (partial, _) = truncate_value_to_budget(item.clone(), remaining);
502 if !matches!(partial, Value::Nothing { .. })
503 && used.saturating_add(partial.memory_size()) <= budget
504 {
505 kept.push(partial);
506 }
507 }
508 overflow_item = Some(item);
509 break;
510 }
511
512 let stored = Value::list(kept.clone(), span);
513 let (stored, more_trunc) = if stored.memory_size() > budget {
514 truncate_value_to_budget(stored, budget)
515 } else {
516 (stored, false)
517 };
518 stack.store_last_result_raw(stored, metadata.clone(), truncated || more_trunc);
519
520 let print_iter: Box<dyn Iterator<Item = Value> + Send> = match overflow_item {
522 None => Box::new(kept.into_iter()),
523 Some(item) => Box::new(kept.into_iter().chain(std::iter::once(item)).chain(iter)),
524 };
525
526 PipelineData::ListStream(ListStream::new(print_iter, span, signals), metadata)
527}
528
529fn store_byte_stream_prefix(
530 stack: &mut Stack,
531 stream: ByteStream,
532 metadata: Option<PipelineMetadata>,
533 budget: usize,
534) -> PipelineData {
535 let span = stream.span();
536 let type_ = stream.type_();
537 let signals = stream.signals().clone();
538 let trim_trailing_newline = stream.source().is_external();
541
542 let has_stdout = match stream.source() {
547 ByteStreamSource::Read(_) | ByteStreamSource::File(_) => true,
548 ByteStreamSource::Child(child) => child.stdout.is_some(),
549 };
550 if !has_stdout {
551 return PipelineData::ByteStream(stream, metadata);
552 }
553
554 let Some(mut reader) = stream.reader() else {
556 return PipelineData::Empty;
559 };
560
561 stack.clear_last_result_payload();
563
564 let max_bytes = budget.saturating_sub(std::mem::size_of::<Value>());
565 let mut prefix = Vec::new();
566 let mut buf = [0u8; 8192];
567 let mut truncated = false;
568
569 while prefix.len() < max_bytes {
570 if signals.check(&span).is_err() {
572 truncated = true;
573 break;
574 }
575 let to_read = (max_bytes - prefix.len()).min(buf.len());
576 match std::io::Read::read(&mut reader, &mut buf[..to_read]) {
577 Ok(0) => break,
578 Ok(n) => prefix.extend_from_slice(&buf[..n]),
579 Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
581 Err(_) => break,
582 }
583 }
584
585 let mut trailing: Option<u8> = None;
587 if !truncated && prefix.len() >= max_bytes {
588 if signals.check(&span).is_err() {
589 truncated = true;
590 } else {
591 match std::io::Read::read(&mut reader, &mut buf[..1]) {
592 Ok(0) => {}
593 Ok(1..) => {
594 truncated = true;
595 trailing = Some(buf[0]);
596 }
597 Err(e) if e.kind() == std::io::ErrorKind::Interrupted => {
598 truncated = true;
600 }
601 _ => {}
602 }
603 }
604 }
605
606 let stored = value_from_bytes(
610 prefix.clone(),
611 span,
612 type_,
613 trim_trailing_newline && !truncated,
614 );
615 let (stored, more) = if stored.memory_size() > budget {
616 truncate_value_to_budget(stored, budget)
617 } else {
618 (stored, false)
619 };
620 stack.store_last_result_raw(stored, metadata.clone(), truncated || more);
621
622 let prefix_for_print = prefix;
624 let rebuilt = ByteStream::from_result_iter(
625 std::iter::once(Ok::<Vec<u8>, ShellError>(prefix_for_print))
626 .chain(trailing.map(|b| Ok(vec![b])))
627 .chain(std::iter::from_fn(move || {
628 let mut chunk = vec![0u8; 8192];
629 match std::io::Read::read(&mut reader, &mut chunk) {
630 Ok(0) => None,
631 Ok(n) => {
632 chunk.truncate(n);
633 Some(Ok(chunk))
634 }
635 Err(err) => Some(Err(ShellError::from(IoError::new(err, span, None)))),
636 }
637 })),
638 span,
639 signals,
640 type_,
641 );
642
643 PipelineData::ByteStream(rebuilt, metadata)
644}
645
646#[cfg(test)]
647mod test {
648 use super::*;
649
650 #[test]
651 fn test_gather_env_vars() {
652 let mut engine_state = EngineState::new();
653 let symbols = r##" !"#$%&'()*+,-./:;<=>?@[\]^_`{|}~"##;
654
655 gather_env_vars(
656 [
657 ("FOO".into(), "foo".into()),
658 ("SYMBOLS".into(), symbols.into()),
659 (symbols.into(), "symbols".into()),
660 ]
661 .into_iter(),
662 &mut engine_state,
663 Path::new("t"),
664 );
665
666 let env = engine_state.render_env_vars();
667
668 assert!(matches!(env.get("FOO"), Some(&Value::String { val, .. }) if val == "foo"));
669 assert!(matches!(env.get("SYMBOLS"), Some(&Value::String { val, .. }) if val == symbols));
670 assert!(matches!(env.get(symbols), Some(&Value::String { val, .. }) if val == "symbols"));
671 assert!(env.contains_key("PWD"));
672 assert_eq!(env.len(), 4);
673 }
674}