Skip to main content

nu_cli/
util.rs

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
25// This will collect environment variables from std::env and adds them to a stack.
26//
27// In order to ensure the values have spans, it first creates a dummy file, writes the collected
28// env vars into it (in a "NAME"="value" format, quite similar to the output of the Unix 'env'
29// tool), then uses the file to get the spans. The file stays in memory, no filesystem IO is done.
30//
31// The "PWD" env value will be forced to `init_cwd`.
32// The reason to use `init_cwd`:
33//
34// While gathering parent env vars, the parent `PWD` may not be the same as `current working directory`.
35// Consider to the following command as the case (assume we execute command inside `/tmp`):
36//
37//     tmux split-window -v -c "#{pane_current_path}"
38//
39// Here nu execute external command `tmux`, and tmux starts a new `nushell`, with `init_cwd` value "#{pane_current_path}".
40// But at the same time `PWD` still remains to be `/tmp`.
41//
42// In this scenario, the new `nushell`'s PWD should be "#{pane_current_path}" rather init_cwd.
43pub 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    // Write all the env vars into a fake file
75    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            // Could not capture current working directory
85            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    // Lex the fake file, assign spans to all environment variables and add them
99    // to stack
100    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            // stack.add_env_var(name, value);
207            engine_state.add_env_var(name, value);
208        }
209    }
210}
211
212/// Print a pipeline with formatting applied based on display_output hook.
213///
214/// This function should be preferred when printing values resulting from a completed evaluation.
215/// For values printed as part of a command's execution, such as values printed by the `print` command,
216/// the `PipelineData::print_table` function should be preferred instead as it is not config-dependent.
217///
218/// `no_newline` controls if we need to attach newline character to output.
219pub 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        // if display_output isn't set, we should still prefer to print with some formatting
239        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            // No call span available in eval_source — this wraps generic source evaluation
257            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
266/// Evaluate an already-parsed block with the same print / exit-code behavior as [`eval_source`].
267///
268/// Used by file evaluation so the file is not re-parsed (re-parsing can reuse stale sourced
269/// blocks with old VarIds; see https://github.com/nushell/nushell/issues/18515).
270pub 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    // reset vt processing, aka ansi because illbehaved externals can break it
313    #[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), // format!("repl_entry #{}", entry_num)
343            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
368/// Evaluate a parsed block: run it, apply variable deletions, optionally store interactive
369/// last-result (`$ans.last`), print output, flush last-result truncation warnings, and handle
370/// optional pipefail.
371pub(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    // Update engine_state with deleted variables
386    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    // Capture interactive last-result before display_output transforms/prints the value.
396    // Only for true REPL user lines (`capture_repl_last_result`), not config/env/banner.
397    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    // Truncation warning for `$ans` is deferred until after display so the data
405    // is not scrolled off the screen by the warning.
406    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    // After print pipeline, need to check exit status to implement pipeline feature.
413    check_exit_status_future(pipeline.exit).map(|_| false)
414}
415
416/// Store the successful interactive pipeline result as last-result when appropriate.
417///
418/// Skips bare last-result retrievals. May wrap streams so a prefix is retained for
419/// the last-result variable without fully collecting the stream just for storage.
420fn 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    // Bare `$ans` / `$ans.*` cell-paths (rename via LAST_RESULT_VAR_NAME) must not re-store `.last`.
429    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        // Budget 0: drop `.last`; snapshot still sets exit_code/duration/command.
436        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
460/// Tee a list stream: accumulate `$ans.last` under budget while rebuilding a print stream.
461///
462/// Under budget with a fully drained stream, avoids double-cloning into a separate print
463/// buffer. Overflowing items stop storage (whole rows for tables; optional partial last
464/// scalar for non-tables via [`truncate_value_to_budget`]).
465fn 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        // Item does not fit whole.
488        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            // Prefer whole rows so table-like `$ans.last` still renders with columns.
494            overflow_item = Some(item);
495            break;
496        }
497
498        // Non-table: allow a partial last scalar/string/binary/list like Value path.
499        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    // Print stream: under-budget full drain reuses `kept` without a second buffer of clones.
521    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    // Externals (and file-backed streams) trim a single trailing newline when
539    // decoded to string, matching [`ByteStream::into_value`].
540    let trim_trailing_newline = stream.source().is_external();
541
542    // No capturable bytes (e.g. stdout was null or still inherited/TTY). Do not
543    // replace a prior `$ans.last` with empty binary; leave the stream for print/wait.
544    // Bare interactive externals keep inherited stdout so TUI tools (`nvim`, `btm`)
545    // still attach to the real terminal — capturing them would require a pipe and hang.
546    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    // Only clear payload after we successfully obtain a reader — `reader()` consumes the stream.
555    let Some(mut reader) = stream.reader() else {
556        // Defensive: source said stdout exists but reader failed. Prior `$ans` left intact;
557        // nothing left to print.
558        return PipelineData::Empty;
559    };
560
561    // Drop prior `.last` only; keep exit_code/duration until REPL snapshot refreshes them.
562    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        // Honor Ctrl-C while filling the budget (large streams can take a while).
571        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            // EINTR: retry like ByteStream::into_bytes.
580            Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
581            Err(_) => break,
582        }
583    }
584
585    // Peek whether more data remains (truncation), unless we already stopped for interrupt.
586    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                    // Treat as "may have more" without consuming a peek byte.
599                    truncated = true;
600                }
601                _ => {}
602            }
603        }
604    }
605
606    // Decode for `$ans.last` the same way collecting a byte stream does:
607    // Binary stays binary; String/Unknown become string when UTF-8, else binary.
608    // Display still uses the raw rebuilt byte stream below.
609    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    // Rebuild stream: stored prefix bytes + optional peek byte + remaining reader.
623    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}