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, value_is_error_only,
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
25#[must_use = "captures raw-mode state that is restored on drop"]
32pub(crate) struct ReplTerminalGuard {
33 was_raw: bool,
34}
35
36impl ReplTerminalGuard {
37 pub(crate) fn capture() -> Self {
38 let was_raw = crossterm::terminal::is_raw_mode_enabled().unwrap_or(false);
39 Self { was_raw }
40 }
41}
42
43impl Drop for ReplTerminalGuard {
44 fn drop(&mut self) {
45 if self.was_raw {
46 let _ = crossterm::terminal::disable_raw_mode();
49 let _ = crossterm::terminal::enable_raw_mode();
50 }
51 }
52}
53
54pub fn gather_parent_env_vars(engine_state: &mut EngineState, init_cwd: &Path) {
73 gather_env_vars(std::env::vars(), engine_state, init_cwd);
74}
75
76fn gather_env_vars(
77 vars: impl Iterator<Item = (String, String)>,
78 engine_state: &mut EngineState,
79 init_cwd: &Path,
80) {
81 fn report_capture_error(engine_state: &EngineState, env_str: &str, msg: &str) {
82 report_shell_error(
83 None,
84 engine_state,
85 &ShellError::Generic(
86 GenericError::new_internal(
87 format!("Environment variable was not captured: {env_str}"),
88 "",
89 )
90 .with_help(msg.to_string()),
91 ),
92 );
93 }
94
95 fn put_env_to_fake_file(name: &str, val: &str, fake_env_file: &mut String) {
96 fake_env_file.push_str(&escape_quote_string(name));
97 fake_env_file.push('=');
98 fake_env_file.push_str(&escape_quote_string(val));
99 fake_env_file.push('\n');
100 }
101
102 let mut fake_env_file = String::new();
103 for (name, val) in vars {
105 put_env_to_fake_file(&name, &val, &mut fake_env_file);
106 }
107
108 match init_cwd.to_str() {
109 Some(cwd) => {
110 put_env_to_fake_file("PWD", cwd, &mut fake_env_file);
111 }
112 None => {
113 report_shell_error(
115 None,
116 engine_state,
117 &ShellError::Generic(
118 GenericError::new_internal("Current directory is not a valid utf-8 path", "")
119 .with_help(format!(
120 "Retrieving current directory failed: {init_cwd:?} not a valid utf-8 path"
121 )),
122 ),
123 );
124 }
125 }
126
127 let span_offset = engine_state.next_span_start();
130
131 engine_state.add_file(
132 "Host Environment Variables".into(),
133 fake_env_file.as_bytes().into(),
134 );
135
136 let (tokens, _) = lex(fake_env_file.as_bytes(), span_offset, &[], &[], true);
137
138 for token in tokens {
139 if let Token {
140 contents: TokenContents::Item,
141 span: full_span,
142 } = token
143 {
144 let contents = engine_state.get_span_contents(full_span);
145 let (parts, _) = lex(contents, full_span.start, &[], &[b'='], true);
146
147 let name = if let Some(Token {
148 contents: TokenContents::Item,
149 span,
150 }) = parts.first()
151 {
152 let mut working_set = StateWorkingSet::new(engine_state);
153 let bytes = working_set.get_span_contents(*span);
154
155 if bytes.len() < 2 {
156 report_capture_error(
157 engine_state,
158 &String::from_utf8_lossy(contents),
159 "Got empty name.",
160 );
161
162 continue;
163 }
164
165 let (bytes, err) = unescape_unquote_string(bytes, *span);
166 if let Some(err) = err {
167 working_set.error(err);
168 }
169
170 if !working_set.parse_errors.is_empty() {
171 report_capture_error(
172 engine_state,
173 &String::from_utf8_lossy(contents),
174 "Got unparsable name.",
175 );
176
177 continue;
178 }
179
180 bytes
181 } else {
182 report_capture_error(
183 engine_state,
184 &String::from_utf8_lossy(contents),
185 "Got empty name.",
186 );
187
188 continue;
189 };
190
191 let value = if let Some(Token {
192 contents: TokenContents::Item,
193 span,
194 }) = parts.get(2)
195 {
196 let mut working_set = StateWorkingSet::new(engine_state);
197 let bytes = working_set.get_span_contents(*span);
198
199 if bytes.len() < 2 {
200 report_capture_error(
201 engine_state,
202 &String::from_utf8_lossy(contents),
203 "Got empty value.",
204 );
205
206 continue;
207 }
208
209 let (bytes, err) = unescape_unquote_string(bytes, *span);
210 if let Some(err) = err {
211 working_set.error(err);
212 }
213
214 if !working_set.parse_errors.is_empty() {
215 report_capture_error(
216 engine_state,
217 &String::from_utf8_lossy(contents),
218 "Got unparsable value.",
219 );
220
221 continue;
222 }
223
224 Value::string(bytes, *span)
225 } else {
226 report_capture_error(
227 engine_state,
228 &String::from_utf8_lossy(contents),
229 "Got empty value.",
230 );
231
232 continue;
233 };
234
235 engine_state.add_env_var(name, value);
237 }
238 }
239}
240
241pub fn print_pipeline(
249 engine_state: &mut EngineState,
250 stack: &mut Stack,
251 pipeline: PipelineData,
252 no_newline: bool,
253) -> Result<(), ShellError> {
254 let to_stderr = engine_state.is_mcp || engine_state.is_lsp;
255
256 if let Some(hook) = stack.get_config(engine_state).hooks.display_output.clone() {
257 let pipeline = eval_hook(
258 engine_state,
259 stack,
260 Some(pipeline),
261 vec![],
262 &hook,
263 "display_output",
264 )?;
265 pipeline.print_raw(engine_state, no_newline, to_stderr)
266 } else {
267 pipeline.print_table(engine_state, stack, no_newline, to_stderr)
269 }
270}
271
272pub fn eval_source(
273 engine_state: &mut EngineState,
274 stack: &mut Stack,
275 source: &[u8],
276 fname: &str,
277 input: PipelineData,
278 allow_return: bool,
279) -> i32 {
280 let start_time = Instant::now();
281
282 let exit_code = match evaluate_source(engine_state, stack, source, fname, input, allow_return) {
283 Ok(failed) => {
284 let code = failed.into();
285 stack.set_last_exit_code(code, Span::unknown());
287 code
288 }
289 Err(err) => map_eval_error_to_exit_code(engine_state, stack, err),
290 };
291
292 finish_eval_source(engine_state, fname, start_time, exit_code)
293}
294
295pub fn eval_parsed_block_source(
300 engine_state: &mut EngineState,
301 stack: &mut Stack,
302 block: &Block,
303 fname: &str,
304 input: PipelineData,
305 allow_return: bool,
306) -> i32 {
307 let start_time = Instant::now();
308
309 let exit_code = match evaluate_parsed_block(engine_state, stack, block, input, allow_return) {
310 Ok(failed) => {
311 let code = failed.into();
312 stack.set_last_exit_code(code, Span::unknown());
313 code
314 }
315 Err(err) => map_eval_error_to_exit_code(engine_state, stack, err),
316 };
317
318 finish_eval_source(engine_state, fname, start_time, exit_code)
319}
320
321fn map_eval_error_to_exit_code(
322 engine_state: &EngineState,
323 stack: &mut Stack,
324 err: ShellError,
325) -> i32 {
326 if let ShellError::Exit { code, .. } = &err {
327 std::process::exit(*code)
328 }
329 report_shell_error(Some(stack), engine_state, &err);
330 let code = err.exit_code();
331 stack.set_last_error(&err);
332 code.unwrap_or(0)
333}
334
335fn finish_eval_source(
336 engine_state: &EngineState,
337 fname: &str,
338 start_time: Instant,
339 exit_code: i32,
340) -> i32 {
341 #[cfg(windows)]
343 {
344 let _ = enable_vt_processing();
345 }
346
347 perf!(
348 &format!("eval_source {}", fname),
349 start_time,
350 engine_state
351 .get_config()
352 .use_ansi_coloring
353 .get(engine_state)
354 );
355
356 exit_code
357}
358
359pub(crate) fn evaluate_source(
360 engine_state: &mut EngineState,
361 stack: &mut Stack,
362 source: &[u8],
363 fname: &str,
364 input: PipelineData,
365 allow_return: bool,
366) -> Result<bool, ShellError> {
367 let (block, delta) = {
368 let mut working_set = StateWorkingSet::new(engine_state);
369 let output = parse(
370 &mut working_set,
371 Some(fname), source,
373 false,
374 );
375 if let Some(warning) = working_set.parse_warnings.first() {
376 report_parse_warning(Some(stack), &working_set, warning);
377 }
378
379 if let Some(err) = working_set.parse_errors.first() {
380 report_parse_error(Some(stack), &working_set, err);
381 return Ok(true);
382 }
383
384 if let Some(err) = working_set.compile_errors.first() {
385 report_compile_error(Some(stack), &working_set, err);
386 return Ok(true);
387 }
388
389 (output, working_set.render())
390 };
391
392 engine_state.merge_delta(delta)?;
393
394 evaluate_parsed_block(engine_state, stack, &block, input, allow_return)
395}
396
397pub(crate) fn evaluate_parsed_block(
401 engine_state: &mut EngineState,
402 stack: &mut Stack,
403 block: &Block,
404 input: PipelineData,
405 allow_return: bool,
406) -> Result<bool, ShellError> {
407 let pipeline = if allow_return {
408 eval_block_with_early_return::<WithoutDebug>(engine_state, stack, block, input)
409 } else {
410 eval_block::<WithoutDebug>(engine_state, stack, block, input)
411 }?;
412 let mut pipeline_data = pipeline.body;
413
414 for var_id in &stack.deletions {
416 if let Some(active_id) = engine_state.scope.active_overlays.last()
417 && let Some((_, overlay)) = engine_state.scope.overlays.get_mut((*active_id).get())
418 {
419 overlay.vars.retain(|_, v| *v != *var_id);
420 }
421 }
422 stack.deletions.clear();
423
424 if engine_state.is_interactive && engine_state.capture_repl_last_result {
427 pipeline_data = maybe_store_last_result(engine_state, stack, block, pipeline_data);
428 }
429
430 let no_newline = matches!(&pipeline_data, &PipelineData::ByteStream(..));
431 print_pipeline(engine_state, stack, pipeline_data, no_newline)?;
432
433 stack.flush_last_result_truncation_warning(engine_state, Span::unknown());
436
437 let pipefail = nu_experimental::PIPE_FAIL.get();
438 if !pipefail {
439 return Ok(false);
440 }
441 check_exit_status_future(pipeline.exit).map(|_| false)
443}
444
445fn maybe_store_last_result(
450 engine_state: &EngineState,
451 stack: &mut Stack,
452 block: &Block,
453 pipeline_data: PipelineData,
454) -> PipelineData {
455 let budget = stack.get_config(engine_state).max_last_result_size_bytes();
456
457 let mut get_block = |id| engine_state.get_block(id).as_ref();
459 if block_is_bare_last_result_with(block, &mut get_block) {
460 return pipeline_data;
461 }
462
463 if budget == 0 {
464 stack.clear_last_result_payload();
466 return pipeline_data;
467 }
468
469 let signals = engine_state.signals().clone();
470
471 match pipeline_data {
472 PipelineData::Empty => {
473 stack.set_last_result(Value::nothing(Span::unknown()), None, budget);
474 PipelineData::Empty
475 }
476 PipelineData::Value(value, metadata) => {
477 if !value_is_error_only(&value) {
478 stack.set_last_result(value.clone(), metadata.clone(), budget);
479 }
480 PipelineData::Value(value, metadata)
481 }
482 PipelineData::ListStream(stream, metadata) => {
483 store_list_stream_prefix(stack, stream, metadata, budget, signals)
484 }
485 PipelineData::ByteStream(stream, metadata) => {
486 store_byte_stream_prefix(stack, stream, metadata, budget)
487 }
488 }
489}
490
491fn store_list_stream_prefix(
497 stack: &mut Stack,
498 stream: ListStream,
499 metadata: Option<PipelineMetadata>,
500 budget: usize,
501 signals: Signals,
502) -> PipelineData {
503 let span = stream.span();
504 let mut kept: Vec<Value> = Vec::new();
505 let mut used = Value::list(vec![], span).memory_size();
506 let mut truncated = false;
507 let mut overflow_item: Option<Value> = None;
508
509 let mut iter = stream.into_iter();
510 for item in iter.by_ref() {
511 let item_size = item.memory_size();
512 if used.saturating_add(item_size) <= budget {
513 used += item_size;
514 kept.push(item);
515 continue;
516 }
517
518 truncated = true;
520 let table_like = matches!(item, Value::Record { .. })
521 && (kept.is_empty() || kept.iter().all(|v| matches!(v, Value::Record { .. })));
522
523 if table_like {
524 overflow_item = Some(item);
526 break;
527 }
528
529 let remaining = budget.saturating_sub(used);
531 if remaining > 0 {
532 let (partial, _) = truncate_value_to_budget(item.clone(), remaining);
533 if !matches!(partial, Value::Nothing { .. })
534 && used.saturating_add(partial.memory_size()) <= budget
535 {
536 kept.push(partial);
537 }
538 }
539 overflow_item = Some(item);
540 break;
541 }
542
543 let stored = Value::list(kept.clone(), span);
544 let (stored, more_trunc) = if stored.memory_size() > budget {
545 truncate_value_to_budget(stored, budget)
546 } else {
547 (stored, false)
548 };
549 if !value_is_error_only(&stored) {
550 stack.store_last_result_raw(stored, metadata.clone(), truncated || more_trunc);
551 }
552
553 let print_iter: Box<dyn Iterator<Item = Value> + Send> = match overflow_item {
555 None => Box::new(kept.into_iter()),
556 Some(item) => Box::new(kept.into_iter().chain(std::iter::once(item)).chain(iter)),
557 };
558
559 PipelineData::ListStream(ListStream::new(print_iter, span, signals), metadata)
560}
561
562fn store_byte_stream_prefix(
563 stack: &mut Stack,
564 stream: ByteStream,
565 metadata: Option<PipelineMetadata>,
566 budget: usize,
567) -> PipelineData {
568 let span = stream.span();
569 let type_ = stream.type_();
570 let signals = stream.signals().clone();
571 let trim_trailing_newline = stream.source().is_external();
574
575 let has_stdout = match stream.source() {
580 ByteStreamSource::Read(_) | ByteStreamSource::File(_) => true,
581 ByteStreamSource::Child(child) => child.stdout.is_some(),
582 };
583 if !has_stdout {
584 return PipelineData::ByteStream(stream, metadata);
585 }
586
587 let Some(mut reader) = stream.reader() else {
589 return PipelineData::Empty;
592 };
593
594 stack.clear_last_result_payload();
596
597 let max_bytes = budget.saturating_sub(std::mem::size_of::<Value>());
598 let mut prefix = Vec::new();
599 let mut buf = [0u8; 8192];
600 let mut truncated = false;
601
602 while prefix.len() < max_bytes {
603 if signals.check(&span).is_err() {
605 truncated = true;
606 break;
607 }
608 let to_read = (max_bytes - prefix.len()).min(buf.len());
609 match std::io::Read::read(&mut reader, &mut buf[..to_read]) {
610 Ok(0) => break,
611 Ok(n) => prefix.extend_from_slice(&buf[..n]),
612 Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
614 Err(_) => break,
615 }
616 }
617
618 let mut trailing: Option<u8> = None;
620 if !truncated && prefix.len() >= max_bytes {
621 if signals.check(&span).is_err() {
622 truncated = true;
623 } else {
624 match std::io::Read::read(&mut reader, &mut buf[..1]) {
625 Ok(0) => {}
626 Ok(1..) => {
627 truncated = true;
628 trailing = Some(buf[0]);
629 }
630 Err(e) if e.kind() == std::io::ErrorKind::Interrupted => {
631 truncated = true;
633 }
634 _ => {}
635 }
636 }
637 }
638
639 let stored = value_from_bytes(
643 prefix.clone(),
644 span,
645 type_,
646 trim_trailing_newline && !truncated,
647 );
648 let (stored, more) = if stored.memory_size() > budget {
649 truncate_value_to_budget(stored, budget)
650 } else {
651 (stored, false)
652 };
653 stack.store_last_result_raw(stored, metadata.clone(), truncated || more);
654
655 let prefix_for_print = prefix;
657 let rebuilt = ByteStream::from_result_iter(
658 std::iter::once(Ok::<Vec<u8>, ShellError>(prefix_for_print))
659 .chain(trailing.map(|b| Ok(vec![b])))
660 .chain(std::iter::from_fn(move || {
661 let mut chunk = vec![0u8; 8192];
662 match std::io::Read::read(&mut reader, &mut chunk) {
663 Ok(0) => None,
664 Ok(n) => {
665 chunk.truncate(n);
666 Some(Ok(chunk))
667 }
668 Err(err) => Some(Err(ShellError::from(IoError::new(err, span, None)))),
669 }
670 })),
671 span,
672 signals,
673 type_,
674 );
675
676 PipelineData::ByteStream(rebuilt, metadata)
677}
678
679#[cfg(test)]
680mod test {
681 use super::*;
682
683 #[test]
684 fn test_gather_env_vars() {
685 let mut engine_state = EngineState::new();
686 let symbols = r##" !"#$%&'()*+,-./:;<=>?@[\]^_`{|}~"##;
687
688 gather_env_vars(
689 [
690 ("FOO".into(), "foo".into()),
691 ("SYMBOLS".into(), symbols.into()),
692 (symbols.into(), "symbols".into()),
693 ]
694 .into_iter(),
695 &mut engine_state,
696 Path::new("t"),
697 );
698
699 let env = engine_state.render_env_vars();
700
701 assert!(matches!(env.get("FOO"), Some(&Value::String { val, .. }) if val == "foo"));
702 assert!(matches!(env.get("SYMBOLS"), Some(&Value::String { val, .. }) if val == symbols));
703 assert!(matches!(env.get(symbols), Some(&Value::String { val, .. }) if val == "symbols"));
704 assert!(env.contains_key("PWD"));
705 assert_eq!(env.len(), 4);
706 }
707}