nu-command 0.62.0

Nushell's built-in commands
Documentation
use nu_protocol::ast::Call;
use nu_protocol::engine::{Command, EngineState, Stack};
use nu_protocol::{
    Category, Example, IntoInterruptiblePipelineData, PipelineData, RawStream, ShellError,
    Signature, Span, Value,
};

#[derive(Clone)]
pub struct Lines;

impl Command for Lines {
    fn name(&self) -> &str {
        "lines"
    }

    fn usage(&self) -> &str {
        "Converts input to lines"
    }

    fn signature(&self) -> nu_protocol::Signature {
        Signature::build("lines")
            .switch("skip-empty", "skip empty lines", Some('s'))
            .category(Category::Filters)
    }

    fn run(
        &self,
        engine_state: &EngineState,
        _stack: &mut Stack,
        call: &Call,
        input: PipelineData,
    ) -> Result<nu_protocol::PipelineData, nu_protocol::ShellError> {
        let head = call.head;
        let ctrlc = engine_state.ctrlc.clone();
        let skip_empty = call.has_flag("skip-empty");
        match input {
            #[allow(clippy::needless_collect)]
            // Collect is needed because the string may not live long enough for
            // the Rc structure to continue using it. If split could take ownership
            // of the split values, then this wouldn't be needed
            PipelineData::Value(Value::String { val, span }, ..) => {
                let split_char = if val.contains("\r\n") { "\r\n" } else { "\n" };

                let mut lines = val
                    .split(split_char)
                    .map(|s| s.to_string())
                    .collect::<Vec<String>>();

                // if the last one is empty, remove it, as it was just
                // a newline at the end of the input we got
                if let Some(last) = lines.last() {
                    if last.is_empty() {
                        lines.pop();
                    }
                }

                let iter = lines.into_iter().filter_map(move |s| {
                    if skip_empty && s.trim().is_empty() {
                        None
                    } else {
                        Some(Value::string(s, span))
                    }
                });

                Ok(iter.into_pipeline_data(engine_state.ctrlc.clone()))
            }
            PipelineData::ListStream(stream, ..) => {
                let mut split_char = "\n";

                let iter = stream
                    .into_iter()
                    .filter_map(move |value| {
                        if let Value::String { val, span } = value {
                            if split_char != "\r\n" && val.contains("\r\n") {
                                split_char = "\r\n";
                            }

                            let mut lines = val
                                .split(split_char)
                                .filter_map(|s| {
                                    if skip_empty && s.trim().is_empty() {
                                        None
                                    } else {
                                        Some(s.to_string())
                                    }
                                })
                                .collect::<Vec<_>>();

                            // if the last one is empty, remove it, as it was just
                            // a newline at the end of the input we got
                            if let Some(last) = lines.last() {
                                if last.is_empty() {
                                    lines.pop();
                                }
                            }

                            Some(
                                lines
                                    .into_iter()
                                    .map(move |x| Value::String { val: x, span }),
                            )
                        } else {
                            None
                        }
                    })
                    .flatten();

                Ok(iter.into_pipeline_data(engine_state.ctrlc.clone()))
            }
            PipelineData::Value(val, ..) => Err(ShellError::UnsupportedInput(
                format!("Not supported input: {}", val.as_string()?),
                head,
            )),
            PipelineData::ExternalStream { stdout: None, .. } => Ok(PipelineData::new(head)),
            PipelineData::ExternalStream {
                stdout: Some(stream),
                ..
            } => Ok(RawStreamLinesAdapter::new(stream, head, skip_empty)
                .into_iter()
                .enumerate()
                .map(move |(_idx, x)| match x {
                    Ok(x) => x,
                    Err(err) => Value::Error { error: err },
                })
                .into_pipeline_data(ctrlc)),
        }
    }

    fn examples(&self) -> Vec<Example> {
        vec![Example {
            description: "Split multi-line string into lines",
            example: "echo $'two(char nl)lines' | lines",
            result: Some(Value::List {
                vals: vec![Value::test_string("two"), Value::test_string("lines")],
                span: Span::test_data(),
            }),
        }]
    }
}

#[derive(Debug)]
struct RawStreamLinesAdapter {
    inner: RawStream,
    inner_complete: bool,
    skip_empty: bool,
    span: Span,
    incomplete_line: String,
    queue: Vec<String>,
}

impl Iterator for RawStreamLinesAdapter {
    type Item = Result<Value, ShellError>;

    fn next(&mut self) -> Option<Self::Item> {
        loop {
            if !self.queue.is_empty() {
                let s = self.queue.remove(0usize);

                if self.skip_empty && s.trim().is_empty() {
                    continue;
                }

                return Some(Ok(Value::String {
                    val: s,
                    span: self.span,
                }));
            } else {
                // inner is complete, feed out remaining state
                if self.inner_complete {
                    if !self.incomplete_line.is_empty() {
                        let r = Some(Ok(Value::String {
                            val: self.incomplete_line.to_string(),
                            span: self.span,
                        }));
                        self.incomplete_line = String::new();
                        return r;
                    }

                    return None;
                }

                // pull more data from inner
                if let Some(result) = self.inner.next() {
                    match result {
                        Ok(v) => {
                            match v {
                                Value::String { val, span } => {
                                    self.span = span;

                                    let split_char =
                                        if val.contains("\r\n") { "\r\n" } else { "\n" };

                                    let mut lines = val
                                        .split(split_char)
                                        .map(|s| s.to_string())
                                        .collect::<Vec<_>>();

                                    // handle incomplete line from previous
                                    if !self.incomplete_line.is_empty() {
                                        if let Some(first) = lines.first() {
                                            let new_incomplete_line =
                                                self.incomplete_line.to_string() + first;
                                            lines.splice(0..1, vec![new_incomplete_line]);
                                            self.incomplete_line = String::new();
                                        }
                                    }

                                    // store incomplete line from current
                                    if let Some(last) = lines.last() {
                                        if last.is_empty() {
                                            // we ended on a line ending
                                            lines.pop();
                                        } else {
                                            // incomplete line, save for next time
                                            if let Some(s) = lines.pop() {
                                                self.incomplete_line = s;
                                            }
                                        }
                                    }

                                    // save completed lines
                                    self.queue.append(&mut lines);
                                }
                                // TODO: Value::Binary support required?
                                _ => {
                                    return Some(Err(ShellError::UnsupportedInput(
                                        "Unsupport type from raw stream".to_string(),
                                        self.span,
                                    )))
                                }
                            }
                        }
                        Err(_) => todo!(),
                    }
                } else {
                    self.inner_complete = true;
                }
            }
        }
    }
}

impl RawStreamLinesAdapter {
    pub fn new(inner: RawStream, span: Span, skip_empty: bool) -> Self {
        Self {
            inner,
            span,
            skip_empty,
            incomplete_line: String::new(),
            queue: Vec::<String>::new(),
            inner_complete: false,
        }
    }
}