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, 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/// Captures whether the process was already in raw mode, then restores that
26/// on drop after a Tab completer closure that may have taken the TTY (`fzf`).
27///
28/// Tests and other cooked-mode callers stay cooked: we only re-enable raw
29/// mode when it was on before the closure ran. Menu sources must not use this;
30/// bouncing raw mode on every refresh flickers the prompt.
31#[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            // Crossterm's Unix `enable_raw_mode` is a no-op when it already
47            // recorded raw mode, so it will not `tcsetattr` after fzf.
48            let _ = crossterm::terminal::disable_raw_mode();
49            let _ = crossterm::terminal::enable_raw_mode();
50        }
51    }
52}
53
54// This will collect environment variables from std::env and adds them to a stack.
55//
56// In order to ensure the values have spans, it first creates a dummy file, writes the collected
57// env vars into it (in a "NAME"="value" format, quite similar to the output of the Unix 'env'
58// tool), then uses the file to get the spans. The file stays in memory, no filesystem IO is done.
59//
60// The "PWD" env value will be forced to `init_cwd`.
61// The reason to use `init_cwd`:
62//
63// While gathering parent env vars, the parent `PWD` may not be the same as `current working directory`.
64// Consider to the following command as the case (assume we execute command inside `/tmp`):
65//
66//     tmux split-window -v -c "#{pane_current_path}"
67//
68// Here nu execute external command `tmux`, and tmux starts a new `nushell`, with `init_cwd` value "#{pane_current_path}".
69// But at the same time `PWD` still remains to be `/tmp`.
70//
71// In this scenario, the new `nushell`'s PWD should be "#{pane_current_path}" rather init_cwd.
72pub 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    // Write all the env vars into a fake file
104    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            // Could not capture current working directory
114            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    // Lex the fake file, assign spans to all environment variables and add them
128    // to stack
129    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            // stack.add_env_var(name, value);
236            engine_state.add_env_var(name, value);
237        }
238    }
239}
240
241/// Print a pipeline with formatting applied based on display_output hook.
242///
243/// This function should be preferred when printing values resulting from a completed evaluation.
244/// For values printed as part of a command's execution, such as values printed by the `print` command,
245/// the `PipelineData::print_table` function should be preferred instead as it is not config-dependent.
246///
247/// `no_newline` controls if we need to attach newline character to output.
248pub 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        // if display_output isn't set, we should still prefer to print with some formatting
268        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            // No call span available in eval_source — this wraps generic source evaluation
286            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
295/// Evaluate an already-parsed block with the same print / exit-code behavior as [`eval_source`].
296///
297/// Used by file evaluation so the file is not re-parsed (re-parsing can reuse stale sourced
298/// blocks with old VarIds; see https://github.com/nushell/nushell/issues/18515).
299pub 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    // reset vt processing, aka ansi because illbehaved externals can break it
342    #[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), // format!("repl_entry #{}", entry_num)
372            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
397/// Evaluate a parsed block: run it, apply variable deletions, optionally store interactive
398/// last-result (`$ans.last`), print output, flush last-result truncation warnings, and handle
399/// optional pipefail.
400pub(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    // Update engine_state with deleted variables
415    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    // Capture interactive last-result before display_output transforms/prints the value.
425    // Only for true REPL user lines (`capture_repl_last_result`), not config/env/banner.
426    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    // Truncation warning for `$ans` is deferred until after display so the data
434    // is not scrolled off the screen by the warning.
435    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    // After print pipeline, need to check exit status to implement pipeline feature.
442    check_exit_status_future(pipeline.exit).map(|_| false)
443}
444
445/// Store the successful interactive pipeline result as last-result when appropriate.
446///
447/// Skips bare last-result retrievals. May wrap streams so a prefix is retained for
448/// the last-result variable without fully collecting the stream just for storage.
449fn 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    // Bare `$ans` / `$ans.*` cell-paths (rename via LAST_RESULT_VAR_NAME) must not re-store `.last`.
458    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        // Budget 0: drop `.last`; snapshot still sets exit_code/duration/command.
465        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
491/// Tee a list stream: accumulate `$ans.last` under budget while rebuilding a print stream.
492///
493/// Under budget with a fully drained stream, avoids double-cloning into a separate print
494/// buffer. Overflowing items stop storage (whole rows for tables; optional partial last
495/// scalar for non-tables via [`truncate_value_to_budget`]).
496fn 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        // Item does not fit whole.
519        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            // Prefer whole rows so table-like `$ans.last` still renders with columns.
525            overflow_item = Some(item);
526            break;
527        }
528
529        // Non-table: allow a partial last scalar/string/binary/list like Value path.
530        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    // Print stream: under-budget full drain reuses `kept` without a second buffer of clones.
554    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    // Externals (and file-backed streams) trim a single trailing newline when
572    // decoded to string, matching [`ByteStream::into_value`].
573    let trim_trailing_newline = stream.source().is_external();
574
575    // No capturable bytes (e.g. stdout was null or still inherited/TTY). Do not
576    // replace a prior `$ans.last` with empty binary; leave the stream for print/wait.
577    // Bare interactive externals keep inherited stdout so TUI tools (`nvim`, `btm`)
578    // still attach to the real terminal — capturing them would require a pipe and hang.
579    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    // Only clear payload after we successfully obtain a reader — `reader()` consumes the stream.
588    let Some(mut reader) = stream.reader() else {
589        // Defensive: source said stdout exists but reader failed. Prior `$ans` left intact;
590        // nothing left to print.
591        return PipelineData::Empty;
592    };
593
594    // Drop prior `.last` only; keep exit_code/duration until REPL snapshot refreshes them.
595    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        // Honor Ctrl-C while filling the budget (large streams can take a while).
604        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            // EINTR: retry like ByteStream::into_bytes.
613            Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
614            Err(_) => break,
615        }
616    }
617
618    // Peek whether more data remains (truncation), unless we already stopped for interrupt.
619    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                    // Treat as "may have more" without consuming a peek byte.
632                    truncated = true;
633                }
634                _ => {}
635            }
636        }
637    }
638
639    // Decode for `$ans.last` the same way collecting a byte stream does:
640    // Binary stays binary; String/Unknown become string when UTF-8, else binary.
641    // Display still uses the raw rebuilt byte stream below.
642    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    // Rebuild stream: stored prefix bytes + optional peek byte + remaining reader.
656    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}