Skip to main content

nu_cmd_lang/core_commands/
ignore.rs

1use nu_engine::command_prelude::*;
2#[cfg(feature = "os")]
3use nu_protocol::{
4    ByteStream, Signals,
5    process::{ChildPipe, check_ok},
6    shell_error::io::IoError,
7    write_all_and_flush,
8};
9use nu_protocol::{OutDest, engine::StateWorkingSet};
10#[cfg(feature = "os")]
11use std::{
12    io::{self, Cursor},
13    thread,
14};
15
16#[derive(Clone)]
17pub struct Ignore;
18
19impl Command for Ignore {
20    fn name(&self) -> &str {
21        "ignore"
22    }
23
24    fn description(&self) -> &str {
25        "Ignore selected output streams from the previous command in the pipeline."
26    }
27
28    fn signature(&self) -> nu_protocol::Signature {
29        Signature::build("ignore")
30            .input_output_types(vec![(Type::Any, Type::Any)])
31            .switch(
32                "stderr",
33                "Consume all stderr output and allow stdout output through.",
34                Some('e'),
35            )
36            .switch(
37                "stdout",
38                "Consume all stdout output and allow stderr output through.",
39                Some('o'),
40            )
41            .switch(
42                "show-errors",
43                "Allow errors through and set $env.LAST_EXIT_CODE (internal failures use 1).",
44                Some('x'),
45            )
46            .category(Category::Core)
47    }
48
49    fn search_terms(&self) -> Vec<&str> {
50        vec!["silent", "quiet", "out-null"]
51    }
52
53    fn is_const(&self) -> bool {
54        true
55    }
56
57    fn run(
58        &self,
59        engine_state: &EngineState,
60        stack: &mut Stack,
61        call: &Call,
62        input: PipelineData,
63    ) -> Result<PipelineData, ShellError> {
64        let consume_stderr = call.has_flag(engine_state, stack, "stderr")?;
65        let consume_stdout = call.has_flag(engine_state, stack, "stdout")? || !consume_stderr;
66        let show_errors = call.has_flag(engine_state, stack, "show-errors")?;
67        let span = call.head;
68
69        match input {
70            PipelineData::ByteStream(stream, metadata) => {
71                #[cfg(feature = "os")]
72                {
73                    let stream_type = stream.type_();
74                    match stream.into_child() {
75                        Ok(child) => handle_external_child(
76                            engine_state,
77                            stack,
78                            span,
79                            child,
80                            stream_type,
81                            metadata,
82                            consume_stdout,
83                            consume_stderr,
84                            show_errors,
85                        ),
86                        Err(stream) => {
87                            if !consume_stdout {
88                                if show_errors {
89                                    stack.set_last_exit_code(0, span);
90                                }
91                                return Ok(PipelineData::byte_stream(stream, metadata));
92                            }
93
94                            match stream.drain() {
95                                Ok(()) => {
96                                    if show_errors {
97                                        stack.set_last_exit_code(0, span);
98                                    }
99                                    Ok(PipelineData::empty())
100                                }
101                                Err(err) => {
102                                    if show_errors {
103                                        stack.set_last_exit_code(1, span);
104                                    }
105                                    Err(err)
106                                }
107                            }
108                        }
109                    }
110                }
111                #[cfg(not(feature = "os"))]
112                {
113                    if !consume_stdout {
114                        return Ok(PipelineData::byte_stream(stream, metadata));
115                    }
116                    match stream.drain() {
117                        Ok(()) => Ok(PipelineData::empty()),
118                        Err(err) => Err(err),
119                    }
120                }
121            }
122            PipelineData::Value(Value::Error { error, .. }, _) => {
123                if consume_stderr {
124                    if show_errors {
125                        stack.set_last_exit_code(1, span);
126                        Err(*error)
127                    } else {
128                        Ok(PipelineData::empty())
129                    }
130                } else {
131                    if show_errors {
132                        stack.set_last_exit_code(1, span);
133                    }
134                    Err(*error)
135                }
136            }
137            input => {
138                if !consume_stdout {
139                    if show_errors {
140                        stack.set_last_exit_code(0, span);
141                    }
142                    return Ok(input);
143                }
144
145                match input.drain() {
146                    Ok(()) => {
147                        if show_errors {
148                            stack.set_last_exit_code(0, span);
149                        }
150                        Ok(PipelineData::empty())
151                    }
152                    Err(err) => {
153                        if show_errors {
154                            stack.set_last_exit_code(1, span);
155                        }
156                        Err(err)
157                    }
158                }
159            }
160        }
161    }
162
163    fn run_const(
164        &self,
165        working_set: &StateWorkingSet,
166        call: &Call,
167        input: PipelineData,
168    ) -> Result<PipelineData, ShellError> {
169        let consume_stderr = call.has_flag_const(working_set, "stderr")?;
170        let consume_stdout = call.has_flag_const(working_set, "stdout")? || !consume_stderr;
171
172        if consume_stderr && matches!(&input, PipelineData::Value(Value::Error { .. }, _)) {
173            return Ok(PipelineData::empty());
174        }
175
176        if consume_stdout {
177            input.drain()?;
178            Ok(PipelineData::empty())
179        } else {
180            Ok(input)
181        }
182    }
183
184    fn examples(&self) -> Vec<Example<'_>> {
185        vec![
186            Example {
187                description: "Ignore all stdout output (default behavior).",
188                example: "echo done | ignore",
189                result: Some(Value::nothing(Span::test_data())),
190            },
191            Example {
192                description: "Consume stderr and allow stdout through.",
193                example: "echo done | ignore --stderr",
194                result: Some(Value::test_string("done")),
195            },
196            Example {
197                description: "Consume stdout while keeping stderr visible.",
198                example: "$'done' | ignore --stdout",
199                result: Some(Value::nothing(Span::test_data())),
200            },
201            Example {
202                description: "Show internal errors and read the resulting exit code.",
203                example: "try { error make {msg: 'boom'} | ignore --show-errors } catch { $env.LAST_EXIT_CODE }",
204                result: Some(Value::test_int(1)),
205            },
206        ]
207    }
208
209    fn pipe_redirection(&self) -> (Option<OutDest>, Option<OutDest>) {
210        (Some(OutDest::PipeSeparate), Some(OutDest::PipeSeparate))
211    }
212}
213
214#[cfg(feature = "os")]
215#[expect(clippy::too_many_arguments)]
216/// Handle external child-process output/error behavior for `ignore`.
217fn handle_external_child(
218    engine_state: &EngineState,
219    stack: &mut Stack,
220    span: Span,
221    mut child: nu_protocol::process::ChildProcess,
222    stream_type: nu_protocol::ByteStreamType,
223    metadata: Option<nu_protocol::PipelineMetadata>,
224    consume_stdout: bool,
225    consume_stderr: bool,
226    show_errors: bool,
227) -> Result<PipelineData, ShellError> {
228    child.ignore_error(!show_errors);
229
230    // Preserve streaming when stdout should pass through and errors remain suppressed.
231    if !consume_stdout && !show_errors {
232        if consume_stderr && let Some(stderr) = child.stderr.take() {
233            consume_child_pipe_on_thread(stderr, span)?;
234        }
235
236        return Ok(PipelineData::byte_stream(
237            ByteStream::child(child, span),
238            metadata,
239        ));
240    }
241
242    // Once we need exit status enforcement or selective stderr replay, we must collect the
243    // process output before deciding what to forward.
244    let output = match child.wait_with_output() {
245        Ok(output) => output,
246        Err(err) => {
247            if show_errors {
248                stack.set_last_exit_code(1, span);
249            }
250            return Err(err);
251        }
252    };
253
254    if !consume_stderr && let Some(stderr) = output.stderr.as_deref() {
255        write_to_out_dest(stderr, stack.stderr(), span, false, engine_state.signals())?;
256    }
257
258    let stdout = output.stdout.unwrap_or_default();
259    let exit_status = output.exit_status;
260
261    if show_errors {
262        let exit_code = exit_status.code();
263        stack.set_last_exit_code(exit_code, span);
264
265        if let Err(err) = check_ok(exit_status, false, span) {
266            if !consume_stdout && !stdout.is_empty() {
267                write_to_out_dest(&stdout, stack.stdout(), span, true, engine_state.signals())?;
268            }
269            return Err(err);
270        }
271    }
272
273    if !consume_stdout && !stdout.is_empty() {
274        let stream = ByteStream::read(
275            Cursor::new(stdout),
276            span,
277            engine_state.signals().clone(),
278            stream_type,
279        );
280        return Ok(PipelineData::byte_stream(stream, metadata));
281    }
282
283    Ok(PipelineData::empty())
284}
285
286#[cfg(feature = "os")]
287/// Write collected bytes to the requested output destination.
288fn write_to_out_dest(
289    bytes: &[u8],
290    out_dest: &OutDest,
291    span: Span,
292    stdout_fallback: bool,
293    signals: &Signals,
294) -> Result<(), ShellError> {
295    if bytes.is_empty() {
296        return Ok(());
297    }
298
299    match out_dest {
300        OutDest::Null => Ok(()),
301        OutDest::File(file) => {
302            let mut file = file.as_ref();
303            write_all_and_flush(bytes, &mut file, "file", Some(span), signals)
304        }
305        OutDest::Print
306        | OutDest::Inherit
307        | OutDest::Pipe
308        | OutDest::PipeSeparate
309        | OutDest::Value => {
310            if stdout_fallback {
311                let mut stdout = io::stdout();
312                write_all_and_flush(bytes, &mut stdout, "stdout", Some(span), signals)
313            } else {
314                let mut stderr = io::stderr();
315                write_all_and_flush(bytes, &mut stderr, "stderr", Some(span), signals)
316            }
317        }
318    }
319}
320
321#[cfg(feature = "os")]
322/// Consume a child pipe on a detached thread so the child process cannot block on a full stderr
323/// buffer when `ignore` is passing stdout through.
324fn consume_child_pipe_on_thread(pipe: ChildPipe, span: Span) -> Result<(), ShellError> {
325    thread::Builder::new()
326        .name("ignore stderr consumer".into())
327        .spawn(move || {
328            let mut sink = io::sink();
329            match pipe {
330                ChildPipe::Pipe(mut pipe) => io::copy(&mut pipe, &mut sink),
331                ChildPipe::Tee(mut tee) => io::copy(&mut tee, &mut sink),
332            }?;
333            Ok::<(), io::Error>(())
334        })
335        .map_err(|err| ShellError::Io(IoError::new(err, span, None)))?;
336    Ok(())
337}
338
339#[cfg(test)]
340mod test {
341    #[test]
342    fn test_examples() -> nu_test_support::Result {
343        use super::Ignore;
344        nu_test_support::test().examples(Ignore)
345    }
346}