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    PipelineData, ShellError, Span, Value,
8    debugger::WithoutDebug,
9    engine::{EngineState, Stack, StateWorkingSet},
10    process::check_exit_status_future,
11    report_error::report_compile_error,
12    report_parse_error, report_parse_warning, report_shell_error,
13    shell_error::generic::GenericError,
14};
15#[cfg(windows)]
16use nu_utils::enable_vt_processing;
17use nu_utils::time::Instant;
18use nu_utils::{escape_quote_string, perf};
19use std::path::Path;
20
21// This will collect environment variables from std::env and adds them to a stack.
22//
23// In order to ensure the values have spans, it first creates a dummy file, writes the collected
24// env vars into it (in a "NAME"="value" format, quite similar to the output of the Unix 'env'
25// tool), then uses the file to get the spans. The file stays in memory, no filesystem IO is done.
26//
27// The "PWD" env value will be forced to `init_cwd`.
28// The reason to use `init_cwd`:
29//
30// While gathering parent env vars, the parent `PWD` may not be the same as `current working directory`.
31// Consider to the following command as the case (assume we execute command inside `/tmp`):
32//
33//     tmux split-window -v -c "#{pane_current_path}"
34//
35// Here nu execute external command `tmux`, and tmux starts a new `nushell`, with `init_cwd` value "#{pane_current_path}".
36// But at the same time `PWD` still remains to be `/tmp`.
37//
38// In this scenario, the new `nushell`'s PWD should be "#{pane_current_path}" rather init_cwd.
39pub fn gather_parent_env_vars(engine_state: &mut EngineState, init_cwd: &Path) {
40    gather_env_vars(std::env::vars(), engine_state, init_cwd);
41}
42
43fn gather_env_vars(
44    vars: impl Iterator<Item = (String, String)>,
45    engine_state: &mut EngineState,
46    init_cwd: &Path,
47) {
48    fn report_capture_error(engine_state: &EngineState, env_str: &str, msg: &str) {
49        report_shell_error(
50            None,
51            engine_state,
52            &ShellError::Generic(
53                GenericError::new_internal(
54                    format!("Environment variable was not captured: {env_str}"),
55                    "",
56                )
57                .with_help(msg.to_string()),
58            ),
59        );
60    }
61
62    fn put_env_to_fake_file(name: &str, val: &str, fake_env_file: &mut String) {
63        fake_env_file.push_str(&escape_quote_string(name));
64        fake_env_file.push('=');
65        fake_env_file.push_str(&escape_quote_string(val));
66        fake_env_file.push('\n');
67    }
68
69    let mut fake_env_file = String::new();
70    // Write all the env vars into a fake file
71    for (name, val) in vars {
72        put_env_to_fake_file(&name, &val, &mut fake_env_file);
73    }
74
75    match init_cwd.to_str() {
76        Some(cwd) => {
77            put_env_to_fake_file("PWD", cwd, &mut fake_env_file);
78        }
79        None => {
80            // Could not capture current working directory
81            report_shell_error(
82                None,
83                engine_state,
84                &ShellError::Generic(
85                    GenericError::new_internal("Current directory is not a valid utf-8 path", "")
86                        .with_help(format!(
87                            "Retrieving current directory failed: {init_cwd:?} not a valid utf-8 path"
88                        )),
89                ),
90            );
91        }
92    }
93
94    // Lex the fake file, assign spans to all environment variables and add them
95    // to stack
96    let span_offset = engine_state.next_span_start();
97
98    engine_state.add_file(
99        "Host Environment Variables".into(),
100        fake_env_file.as_bytes().into(),
101    );
102
103    let (tokens, _) = lex(fake_env_file.as_bytes(), span_offset, &[], &[], true);
104
105    for token in tokens {
106        if let Token {
107            contents: TokenContents::Item,
108            span: full_span,
109        } = token
110        {
111            let contents = engine_state.get_span_contents(full_span);
112            let (parts, _) = lex(contents, full_span.start, &[], &[b'='], true);
113
114            let name = if let Some(Token {
115                contents: TokenContents::Item,
116                span,
117            }) = parts.first()
118            {
119                let mut working_set = StateWorkingSet::new(engine_state);
120                let bytes = working_set.get_span_contents(*span);
121
122                if bytes.len() < 2 {
123                    report_capture_error(
124                        engine_state,
125                        &String::from_utf8_lossy(contents),
126                        "Got empty name.",
127                    );
128
129                    continue;
130                }
131
132                let (bytes, err) = unescape_unquote_string(bytes, *span);
133                if let Some(err) = err {
134                    working_set.error(err);
135                }
136
137                if !working_set.parse_errors.is_empty() {
138                    report_capture_error(
139                        engine_state,
140                        &String::from_utf8_lossy(contents),
141                        "Got unparsable name.",
142                    );
143
144                    continue;
145                }
146
147                bytes
148            } else {
149                report_capture_error(
150                    engine_state,
151                    &String::from_utf8_lossy(contents),
152                    "Got empty name.",
153                );
154
155                continue;
156            };
157
158            let value = if let Some(Token {
159                contents: TokenContents::Item,
160                span,
161            }) = parts.get(2)
162            {
163                let mut working_set = StateWorkingSet::new(engine_state);
164                let bytes = working_set.get_span_contents(*span);
165
166                if bytes.len() < 2 {
167                    report_capture_error(
168                        engine_state,
169                        &String::from_utf8_lossy(contents),
170                        "Got empty value.",
171                    );
172
173                    continue;
174                }
175
176                let (bytes, err) = unescape_unquote_string(bytes, *span);
177                if let Some(err) = err {
178                    working_set.error(err);
179                }
180
181                if !working_set.parse_errors.is_empty() {
182                    report_capture_error(
183                        engine_state,
184                        &String::from_utf8_lossy(contents),
185                        "Got unparsable value.",
186                    );
187
188                    continue;
189                }
190
191                Value::string(bytes, *span)
192            } else {
193                report_capture_error(
194                    engine_state,
195                    &String::from_utf8_lossy(contents),
196                    "Got empty value.",
197                );
198
199                continue;
200            };
201
202            // stack.add_env_var(name, value);
203            engine_state.add_env_var(name, value);
204        }
205    }
206}
207
208/// Print a pipeline with formatting applied based on display_output hook.
209///
210/// This function should be preferred when printing values resulting from a completed evaluation.
211/// For values printed as part of a command's execution, such as values printed by the `print` command,
212/// the `PipelineData::print_table` function should be preferred instead as it is not config-dependent.
213///
214/// `no_newline` controls if we need to attach newline character to output.
215pub fn print_pipeline(
216    engine_state: &mut EngineState,
217    stack: &mut Stack,
218    pipeline: PipelineData,
219    no_newline: bool,
220) -> Result<(), ShellError> {
221    let to_stderr = engine_state.is_mcp || engine_state.is_lsp;
222
223    if let Some(hook) = stack.get_config(engine_state).hooks.display_output.clone() {
224        let pipeline = eval_hook(
225            engine_state,
226            stack,
227            Some(pipeline),
228            vec![],
229            &hook,
230            "display_output",
231        )?;
232        pipeline.print_raw(engine_state, no_newline, to_stderr)
233    } else {
234        // if display_output isn't set, we should still prefer to print with some formatting
235        pipeline.print_table(engine_state, stack, no_newline, to_stderr)
236    }
237}
238
239pub fn eval_source(
240    engine_state: &mut EngineState,
241    stack: &mut Stack,
242    source: &[u8],
243    fname: &str,
244    input: PipelineData,
245    allow_return: bool,
246) -> i32 {
247    let start_time = Instant::now();
248
249    let exit_code = match evaluate_source(engine_state, stack, source, fname, input, allow_return) {
250        Ok(failed) => {
251            let code = failed.into();
252            // No call span available in eval_source — this wraps generic source evaluation
253            stack.set_last_exit_code(code, Span::unknown());
254            code
255        }
256        Err(err) => {
257            report_shell_error(Some(stack), engine_state, &err);
258            let code = err.exit_code();
259            stack.set_last_error(&err);
260            code.unwrap_or(0)
261        }
262    };
263
264    // reset vt processing, aka ansi because illbehaved externals can break it
265    #[cfg(windows)]
266    {
267        let _ = enable_vt_processing();
268    }
269
270    perf!(
271        &format!("eval_source {}", &fname),
272        start_time,
273        engine_state
274            .get_config()
275            .use_ansi_coloring
276            .get(engine_state)
277    );
278
279    exit_code
280}
281
282fn evaluate_source(
283    engine_state: &mut EngineState,
284    stack: &mut Stack,
285    source: &[u8],
286    fname: &str,
287    input: PipelineData,
288    allow_return: bool,
289) -> Result<bool, ShellError> {
290    let (block, delta) = {
291        let mut working_set = StateWorkingSet::new(engine_state);
292        let output = parse(
293            &mut working_set,
294            Some(fname), // format!("repl_entry #{}", entry_num)
295            source,
296            false,
297        );
298        if let Some(warning) = working_set.parse_warnings.first() {
299            report_parse_warning(Some(stack), &working_set, warning);
300        }
301
302        if let Some(err) = working_set.parse_errors.first() {
303            report_parse_error(Some(stack), &working_set, err);
304            return Ok(true);
305        }
306
307        if let Some(err) = working_set.compile_errors.first() {
308            report_compile_error(Some(stack), &working_set, err);
309            return Ok(true);
310        }
311
312        (output, working_set.render())
313    };
314
315    engine_state.merge_delta(delta)?;
316
317    let pipeline = if allow_return {
318        eval_block_with_early_return::<WithoutDebug>(engine_state, stack, &block, input)
319    } else {
320        eval_block::<WithoutDebug>(engine_state, stack, &block, input)
321    }?;
322    let pipeline_data = pipeline.body;
323
324    // Update engine_state with deleted variables
325    for var_id in &stack.deletions {
326        if let Some(active_id) = engine_state.scope.active_overlays.last()
327            && let Some((_, overlay)) = engine_state.scope.overlays.get_mut((*active_id).get())
328        {
329            overlay.vars.retain(|_, v| *v != *var_id);
330        }
331    }
332    stack.deletions.clear();
333
334    let no_newline = matches!(&pipeline_data, &PipelineData::ByteStream(..));
335    print_pipeline(engine_state, stack, pipeline_data, no_newline)?;
336
337    let pipefail = nu_experimental::PIPE_FAIL.get();
338    if !pipefail {
339        return Ok(false);
340    }
341    // After print pipeline, need to check exit status to implement pipeline feature.
342    check_exit_status_future(pipeline.exit).map(|_| false)
343}
344
345#[cfg(test)]
346mod test {
347    use super::*;
348
349    #[test]
350    fn test_gather_env_vars() {
351        let mut engine_state = EngineState::new();
352        let symbols = r##" !"#$%&'()*+,-./:;<=>?@[\]^_`{|}~"##;
353
354        gather_env_vars(
355            [
356                ("FOO".into(), "foo".into()),
357                ("SYMBOLS".into(), symbols.into()),
358                (symbols.into(), "symbols".into()),
359            ]
360            .into_iter(),
361            &mut engine_state,
362            Path::new("t"),
363        );
364
365        let env = engine_state.render_env_vars();
366
367        assert!(matches!(env.get("FOO"), Some(&Value::String { val, .. }) if val == "foo"));
368        assert!(matches!(env.get("SYMBOLS"), Some(&Value::String { val, .. }) if val == symbols));
369        assert!(matches!(env.get(symbols), Some(&Value::String { val, .. }) if val == "symbols"));
370        assert!(env.contains_key("PWD"));
371        assert_eq!(env.len(), 4);
372    }
373}