shuck-parser 0.0.41

A fast, safe bash parser library
Documentation
use super::*;

#[test]
fn test_parse_collects_zsh_brace_if_fact_in_bash_mode() {
    let input = "if [[ -n $x ]] {\n  :\n}\n";
    let parsed = Parser::new(input).parse();

    assert_eq!(parsed.syntax_facts.zsh_brace_if_spans.len(), 1);
    assert_eq!(parsed.syntax_facts.zsh_brace_if_spans[0].slice(input), "{");
}

#[test]
fn test_parse_collects_zsh_always_fact_in_posix_mode() {
    let input = "{ :; } always { :; }\n";
    let parsed = Parser::with_dialect(input, ShellDialect::Posix).parse();

    assert_eq!(parsed.syntax_facts.zsh_always_spans.len(), 1);
    assert_eq!(
        parsed.syntax_facts.zsh_always_spans[0].slice(input),
        "always"
    );
}

#[test]
fn test_zsh_and_or_brace_groups_allow_same_line_closing_brace() {
    let input = "true && { echo yes } || { echo no }\n";
    let script = Parser::with_dialect(input, ShellDialect::Zsh)
        .parse()
        .unwrap()
        .file;

    let outer = expect_binary(&script.body[0]);
    assert_eq!(outer.op, BinaryOp::Or);
    let inner = expect_binary(&outer.left);
    assert_eq!(inner.op, BinaryOp::And);

    let (yes_group, yes_redirects) = expect_compound(&inner.right);
    let AstCompoundCommand::BraceGroup(yes_body) = yes_group else {
        panic!("expected brace group on right side of &&");
    };
    assert!(yes_redirects.is_empty());
    assert_eq!(expect_simple(&yes_body[0]).name.render(input), "echo");

    let (no_group, no_redirects) = expect_compound(&outer.right);
    let AstCompoundCommand::BraceGroup(no_body) = no_group else {
        panic!("expected brace group on right side of ||");
    };
    assert!(no_redirects.is_empty());
    assert_eq!(expect_simple(&no_body[0]).name.render(input), "echo");
}

#[test]
fn test_zsh_brace_group_command_can_use_right_brace_as_literal_argument_before_closer() {
    let source = "rbrace() { echo }; }; rbrace\n";
    let output = Parser::with_dialect(source, ShellDialect::Zsh)
        .parse()
        .unwrap();

    let function = expect_function(&output.file.body[0]);
    let (compound, redirects) = expect_compound(function.body.as_ref());
    let AstCompoundCommand::BraceGroup(body) = compound else {
        panic!("expected brace-group function body");
    };

    assert!(redirects.is_empty());
    assert_eq!(body.len(), 1);
    let command = expect_simple(&body[0]);
    assert_eq!(command.name.render(source), "echo");
    assert_eq!(command.args.len(), 1);
    assert_eq!(command.args[0].render(source), "}");
}

#[test]
fn test_parse_zsh_if_with_pattern_capture_rhs() {
    let input = "if [[ \"$buf\" == (#b)(*)(${~pat})* ]]; then\n  print ok\nfi\n";
    parse_zsh_with_options(input, |options| {
        options.extended_glob = OptionValue::On;
    });
}

#[test]
fn test_parse_zsh_if_else_with_inline_anchor_pattern_rhs() {
    let input =
        "if [[ $buffer != (#s)[$'\\t -~']#(#e) ]]; then\n  print ok\nelse\n  print alt\nfi\n";
    parse_zsh_with_options(input, |options| {
        options.extended_glob = OptionValue::On;
    });
}

#[test]
fn test_parse_zsh_remaining_upstream_advanced_command_examples() {
    for input in [
        "cat file.txt | grep pattern | sort | uniq\n",
        "sleep 10 &\n",
        "sleep 10 &!\n",
        "sleep 10 &|\n",
    ] {
        Parser::with_dialect(input, ShellDialect::Zsh)
            .parse()
            .unwrap();
    }
}

#[test]
fn test_parse_zsh_ansi_c_string_regex_match_expression_from_upstream() {
    let input = "[[ \"$x\" =~ \"^foo\"$'\\t'\"bar\" ]]\n";
    let script = Parser::with_dialect(input, ShellDialect::Zsh)
        .parse()
        .unwrap()
        .file;

    let (compound, _) = expect_compound(&script.body[0]);
    let AstCompoundCommand::Conditional(command) = compound else {
        panic!("expected conditional compound command");
    };
    let ConditionalExpr::Binary(binary) = &command.expression else {
        panic!("expected binary conditional");
    };
    assert_eq!(binary.op, ConditionalBinaryOp::RegexMatch);

    let ConditionalExpr::Regex(word) = binary.right.as_ref() else {
        panic!("expected regex rhs");
    };
    assert_eq!(word.span.slice(input), "\"^foo\"$'\\t'\"bar\"");
}

#[test]
fn test_parse_zsh_if_with_defaulting_subscript_and_or_condition() {
    let input = "if [[ $zsyh_user_options[ignorebraces] == on || ${zsyh_user_options[ignoreclosebraces]:-off} == on ]]; then\n  print ok\nfi\n";
    Parser::with_dialect(input, ShellDialect::Zsh)
        .parse()
        .unwrap();
}

#[test]
fn test_parse_zsh_job_spec_commands_preserve_percent_arguments() {
    for input in ["fg %1\n", "bg %2\n"] {
        let script = Parser::with_dialect(input, ShellDialect::Zsh)
            .parse()
            .unwrap()
            .file;
        let command = expect_simple(&script.body[0]);

        assert_eq!(command.args.len(), 1);
        assert!(command.args[0].span.slice(input).starts_with('%'));
    }
}

#[test]
fn test_parse_zsh_command_substitution_with_comments_containing_apostrophes() {
    let input = "eval $(\n  exec 3>&1 >/dev/null\n  {\n    # won't break the command substitution scanner\n    print ok\n  } always {\n    :\n  }\n)\n";
    Parser::with_dialect(input, ShellDialect::Zsh)
        .parse()
        .unwrap();
}

#[test]
fn test_parse_zsh_if_with_comment_only_else_clause() {
    let input = "if [[ $this_word != *':start_of_pipeline:'* ]]; then\n  style=unknown-token\nelse\n  # '!' reserved word at start of pipeline; style already set above\nfi\n";
    Parser::with_dialect(input, ShellDialect::Zsh)
        .parse()
        .unwrap();
}

#[test]
fn test_parse_zsh_if_with_or_brace_condition_then_compact_prompt_helpers() {
    let input = concat!(
        "if zstyle -t ':omz:alpha:lib:git' async-prompt \\\n",
        "  || { is-at-least 5.0.6 && zstyle -T ':omz:alpha:lib:git' async-prompt }; then\n",
        "  function git_prompt_info() {\n",
        "    print -- info\n",
        "  }\n",
        "  function git_prompt_status() {\n",
        "    print -- status\n",
        "  }\n",
        "fi\n",
    );
    let script = Parser::with_dialect(input, ShellDialect::Zsh)
        .parse()
        .unwrap()
        .file;

    let (compound, redirects) = expect_compound(&script.body[0]);
    let AstCompoundCommand::If(command) = compound else {
        panic!("expected if command");
    };

    assert!(redirects.is_empty());
    assert_eq!(command.condition.len(), 1);
    let condition = expect_binary(&command.condition[0]);
    assert_eq!(condition.op, BinaryOp::Or);

    let (group, group_redirects) = expect_compound(&condition.right);
    let AstCompoundCommand::BraceGroup(body) = group else {
        panic!("expected brace-group right-hand side");
    };
    assert!(group_redirects.is_empty());
    assert_eq!(body.len(), 1);
    assert_eq!(expect_binary(&body[0]).op, BinaryOp::And);

    assert_eq!(command.then_branch.len(), 2);
    assert!(matches!(
        command.then_branch[0].command,
        AstCommand::Function(_)
    ));
    assert!(matches!(
        command.then_branch[1].command,
        AstCommand::Function(_)
    ));
}

#[test]
fn test_parse_zsh_helper_predicate_with_trailing_or_brace_group() {
    let input = concat!(
        "_rake_does_task_list_need_generating() {\n",
        "  _rake_tasks_missing || _rake_tasks_version_changed || _rakefile_has_changes || { _is_rails_app && _tasks_changed }\n",
        "}\n",
    );
    let script = Parser::with_dialect(input, ShellDialect::Zsh)
        .parse()
        .unwrap()
        .file;

    let function = expect_function(&script.body[0]);
    let (compound, redirects) = expect_compound(function.body.as_ref());
    let AstCompoundCommand::BraceGroup(body) = compound else {
        panic!("expected brace-group function body");
    };

    assert!(redirects.is_empty());
    assert_eq!(body.len(), 1);

    let outer = expect_binary(&body[0]);
    assert_eq!(outer.op, BinaryOp::Or);
    let (group, group_redirects) = expect_compound(&outer.right);
    let AstCompoundCommand::BraceGroup(fallback_body) = group else {
        panic!("expected trailing brace-group fallback");
    };
    assert!(group_redirects.is_empty());
    assert_eq!(fallback_body.len(), 1);
    assert_eq!(expect_binary(&fallback_body[0]).op, BinaryOp::And);
}

#[test]
fn test_parse_zsh_compact_bind_widgets_helper_before_hook_registration() {
    let input = concat!(
        "if is-at-least 5.9 && _zsh_highlight__function_callable_p add-zle-hook-widget\n",
        "then\n",
        "  _zsh_highlight__zle-line-pre-redraw() {\n",
        "    true && _zsh_highlight \"$@\"\n",
        "  }\n",
        "  _zsh_highlight_bind_widgets(){}\n",
        "  if [[ -o zle ]]; then\n",
        "    add-zle-hook-widget zle-line-pre-redraw _zsh_highlight__zle-line-pre-redraw\n",
        "    add-zle-hook-widget zle-line-finish _zsh_highlight__zle-line-finish\n",
        "  fi\n",
        "else\n",
        "  _zsh_highlight_bind_widgets() {\n",
        "    zmodload zsh/zleparameter 2>/dev/null || {\n",
        "      print -r -- >&2 failed\n",
        "      return 1\n",
        "    }\n",
        "  }\n",
        "fi\n",
    );
    let script = Parser::with_dialect(input, ShellDialect::Zsh)
        .parse()
        .unwrap()
        .file;

    let (compound, redirects) = expect_compound(&script.body[0]);
    let AstCompoundCommand::If(command) = compound else {
        panic!("expected if command");
    };

    assert!(redirects.is_empty());
    assert_eq!(command.then_branch.len(), 3);

    let helper = expect_function(&command.then_branch[1]);
    let (helper_compound, helper_redirects) = expect_compound(helper.body.as_ref());
    let AstCompoundCommand::BraceGroup(helper_body) = helper_compound else {
        panic!("expected compact helper body");
    };
    assert!(helper_redirects.is_empty());
    assert!(helper_body.is_empty());
    assert!(matches!(
        command.then_branch[2].command,
        AstCommand::Compound(AstCompoundCommand::If(_))
    ));
    assert!(command.else_branch.is_some());
}

#[test]
fn test_parse_zsh_top_level_failure_blocks_after_or_lists() {
    let input = concat!(
        "_zsh_highlight_bind_widgets || {\n",
        "  print -r -- >&2 failed-binding\n",
        "  return 1\n",
        "}\n",
        "_zsh_highlight_load_highlighters \"$dir\" || {\n",
        "  print -r -- >&2 failed-loading\n",
        "  return 1\n",
        "}\n",
    );
    let script = Parser::with_dialect(input, ShellDialect::Zsh)
        .parse()
        .unwrap()
        .file;

    assert_eq!(script.body.len(), 2);
    for stmt in script.body.iter() {
        let command = expect_binary(stmt);
        assert_eq!(command.op, BinaryOp::Or);

        let (group, redirects) = expect_compound(&command.right);
        let AstCompoundCommand::BraceGroup(body) = group else {
            panic!("expected top-level failure brace group");
        };
        assert!(redirects.is_empty());
        assert_eq!(body.len(), 2);
        assert!(matches!(
            body[1].command,
            AstCommand::Builtin(AstBuiltinCommand::Return(_))
        ));
    }
}

#[test]
fn test_parse_zsh_if_compact_brace_bodies_without_space_after_left_brace() {
    let input = "f() { if (($4==0)){c=1;} elif (($4==1)){c=2;} else {c=3;}; echo ok; }\n";
    Parser::with_dialect(input, ShellDialect::Zsh)
        .parse()
        .unwrap();
}

#[test]
fn test_parse_zsh_if_with_last_argument_slice_and_opt_subscripts() {
    let input = "f() {\n  local -A opts\n  if [[ ${@: -1} == /* ]] && (( ! $+opts[-e] && ! $+opts[-l] )); then\n    [[ -d ${@: -1} ]] && print ${@: -1} && return\n  fi\n}\n";
    Parser::with_dialect(input, ShellDialect::Zsh)
        .parse()
        .unwrap();
}

#[test]
fn test_parse_zsh_parameter_replacements_with_quoted_backslashes() {
    let input = "f() {\n  local path_field escaped_path_field\n  escaped_path_field=${path_field//'\\'/'\\\\'}\n  escaped_path_field=${escaped_path_field//'`'/'\\`'}\n  escaped_path_field=${escaped_path_field//'('/'\\('}\n  escaped_path_field=${escaped_path_field//')'/'\\)'}\n  escaped_path_field=${escaped_path_field//'['/'\\['}\n  escaped_path_field=${escaped_path_field//']'/'\\]'}\n}\n";
    Parser::with_dialect(input, ShellDialect::Zsh)
        .parse()
        .unwrap();
}