nu-command 0.115.0

Nushell's built-in commands
Documentation
use indoc::indoc;
use nu_protocol::{ByteStream, ByteStreamType, PipelineData, ShellError, Signals, Span};
use nu_test_support::{fs::Stub, prelude::*};
use pretty_assertions::assert_matches;

mod simple {
    use super::*;

    #[test]
    fn extracts_fields_from_the_given_the_pattern() -> Result {
        let input = indoc! {"
            VAR1=Cheese
            VAR2=JTParsed
            VAR3=NushellSecretIngredient
        "};

        let code = r#"
            $in
            | lines
            | each {|it| $it | parse "{Name}={Value}" }
            | flatten
            | get 1.Value
        "#;

        test()
            .run_with_data(code, input)
            .expect_value_eq("JTParsed")
    }

    #[test]
    fn double_open_curly_evaluates_to_a_single_curly() -> Result {
        let code = r#"
            "{abc}123"
            | parse "{{abc}{name}"
            | get name.0
        "#;

        test().run(code).expect_value_eq("123")
    }

    #[test]
    fn char_lbrace_before_capture() -> Result {
        test()
            .run(r#""1234{56" | parse $'{a}(char lbrace){b}' | get a.0"#)
            .expect_value_eq("1234")
    }

    #[test]
    fn double_brace_at_end_matches_literal_brace_with_capture() -> Result {
        test()
            .run(r#""{hello" | parse "{{foo}" | get foo.0"#)
            .expect_value_eq("hello")
    }

    #[test]
    fn double_brace_at_end_does_not_match_without_brace_in_input() -> Result {
        test()
            .run(r#""hello" | parse "{{foo}" | length"#)
            .expect_value_eq(0)
    }

    #[test]
    fn double_brace_with_suffix_before_capture_stays_literal() -> Result {
        test()
            .run(r#""{foo}x123" | parse "{{foo}x{bar}" | get bar.0"#)
            .expect_value_eq("123")
    }

    #[test]
    fn properly_escapes_text() -> Result {
        let code = r#"
            "(abc)123"
            | parse "(abc){name}"
            | get name.0
        "#;

        test().run(code).expect_value_eq("123")
    }

    #[test]
    fn properly_captures_empty_column() -> Result {
        let code = r#"
            ["1:INFO:component:all is well" "2:ERROR::something bad happened"]
            | parse "{timestamp}:{level}:{tag}:{entry}"
            | get entry.1
        "#;

        test().run(code).expect_value_eq("something bad happened")
    }

    #[test]
    fn errors_when_missing_closing_brace() -> Result {
        let code = r#"
            "(abc)123"
            | parse "(abc){name"
            | get name
        "#;

        let err = test().run(code).expect_shell_error()?;
        assert_matches!(
            err,
            ShellError::DelimiterError { msg, .. }
                if msg == "Found opening `{` without an associated closing `}`"
        );

        Ok(())
    }

    #[test]
    fn ignore_multiple_placeholder() -> Result {
        let code = r#"
            ["1:INFO:component:all is well" "2:ERROR::something bad happened"]
            | parse "{_}:{level}:{_}:{entry}"
        "#;

        test().run(code).expect_value_eq(test_table![
            ["level", "entry"];
            ["INFO", "all is well"],
            ["ERROR", "something bad happened"],
        ])
    }
}

mod regex {
    use super::*;

    fn nushell_git_log_oneline() -> &'static str {
        "ae87582c Fix missing invocation errors (#1846)\nb89976da let format access variables also (#1842)"
    }

    #[test]
    fn extracts_fields_with_all_named_groups() -> Result {
        let code = r#"
            $in
            | parse --regex "(?P<Hash>\\w+) (?P<Message>.+) \\(#(?P<PR>\\d+)\\)"
            | get 1.PR
        "#;

        test()
            .run_with_data(code, nushell_git_log_oneline())
            .expect_value_eq("1842")
    }

    #[test]
    fn extracts_fields_with_all_unnamed_groups() -> Result {
        let code = r#"
            $in
            | parse --regex "(\\w+) (.+) \\(#(\\d+)\\)"
            | get 1.capture0
        "#;

        test()
            .run_with_data(code, nushell_git_log_oneline())
            .expect_value_eq("b89976da")
    }

    #[test]
    fn extracts_fields_with_named_and_unnamed_groups() -> Result {
        let code = r#"
            $in
            | parse --regex "(?P<Hash>\\w+) (.+) \\(#(?P<PR>\\d+)\\)"
            | get 1.capture1
        "#;

        test()
            .run_with_data(code, nushell_git_log_oneline())
            .expect_value_eq("let format access variables also")
    }

    #[test]
    fn errors_with_invalid_regex() -> Result {
        let code = r#"
            $in
            | parse --regex "(?P<Hash>\\w+ unfinished capture group"
        "#;

        let err = test()
            .run_with_data(code, nushell_git_log_oneline())
            .expect_shell_error()?;
        let ShellError::InvalidValue { actual, .. } = err else {
            panic!("expected InvalidValue, got {err:?}");
        };
        assert_contains("Opening parenthesis without closing parenthesis", actual);

        Ok(())
    }

    #[test]
    fn parse_works_with_streaming() -> Result {
        test()
            .run(r#"seq char a z | each {|c| $c + " a"} | parse '{letter} {a}' | describe"#)
            .expect_value_eq("table<letter: string, a: string> (stream)")
    }

    #[test]
    fn parse_does_not_truncate_list_streams() -> Result {
        let code = r#"
            [a b c]
            | each {|x| $x}
            | parse --regex "[ac]"
            | length
        "#;

        test().run(code).expect_value_eq(2)
    }

    #[test]
    fn parse_handles_external_stream_chunking() -> Result {
        Playground::setup("parse_test_streaming_1", |dirs, sandbox| {
            let data: String = "abcdefghijklmnopqrstuvwxyz".repeat(1000);
            sandbox.with_files(&[Stub::FileWithContent("data.txt", &data)]);

            test()
                .cwd(dirs.test())
                .run(r#"open data.txt | parse --regex "(abcdefghijklmnopqrstuvwxyz)" | length"#)
                .expect_value_eq(1000)
        })
    }

    #[test]
    fn multiline_regex() -> Result {
        let mut tester = test();

        let pattern = r#"(?ms)^(?<n>\d+)\. (?<text>.*?)(?=$\s^\d|\Z)"#;
        let () = tester.run_with_data("let pattern = $in", pattern)?;

        let input = [
            "1. one\n",
            "2. two and\n",
            "   a half\n",
            "3. three\n",
            "4. four and\n",
            "   some more\n",
        ];
        let byte_stream = PipelineData::ByteStream(
            ByteStream::from_iter(
                input,
                Span::test_data(),
                Signals::empty(),
                ByteStreamType::Unknown,
            ),
            None,
        );

        let code = "parse -r $pattern";
        tester
            .run_raw_with_data(code, byte_stream)
            .and_then(|x| x.body.into_value(Span::test_data()).map_err(Error::from))
            .expect_value_eq(test_table![
              ["n", "text"];
              ["1", "one"],
              ["2", "two and\n   a half"],
              ["3", "three"],
              ["4", "four and\n   some more"]
            ])
    }
}