shuck-parser 0.0.41

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

#[test]
fn test_parse_arithmetic_command_preserves_exact_spans() {
    let input = "(( 1 +\n 2 <= 3 ))\n";
    let script = Parser::new(input).parse().unwrap().file;

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

    assert!(redirects.is_empty());
    assert_eq!(command.left_paren_span.slice(input), "((");
    assert_eq!(command.right_paren_span.slice(input), "))");
    assert_eq!(command.expr_span.unwrap().slice(input), " 1 +\n 2 <= 3 ");
    let expr = command
        .expr_ast
        .as_ref()
        .expect("expected typed arithmetic AST");
    let ArithmeticExpr::Binary { left, op, right } = &expr.kind else {
        panic!("expected binary arithmetic expression");
    };
    assert_eq!(*op, ArithmeticBinaryOp::LessThanOrEqual);
    let ArithmeticExpr::Binary {
        left: add_left,
        op: add_op,
        right: add_right,
    } = &left.kind
    else {
        panic!("expected additive left operand");
    };
    assert_eq!(*add_op, ArithmeticBinaryOp::Add);
    expect_number(add_left, input, "1");
    expect_number(add_right, input, "2");
    expect_number(right, input, "3");
}

#[test]
fn test_parse_empty_arithmetic_command_keeps_span_without_typed_ast() {
    let input = "((   ))\n";
    let script = Parser::new(input).parse().unwrap().file;

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

    assert!(redirects.is_empty());
    assert_eq!(command.expr_span.unwrap().slice(input), "   ");
    assert!(command.expr_ast.is_none());
}

#[test]
fn test_parse_dynamic_arithmetic_command_keeps_compound_shape_with_typed_ast() {
    let input = "((proc[selected]==(1${filter:++1})-proc[start]))\n";
    let script = Parser::new(input).parse().unwrap().file;

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

    assert!(redirects.is_empty());
    assert_eq!(command.left_paren_span.slice(input), "((");
    assert_eq!(command.right_paren_span.slice(input), "))");
    assert_eq!(
        command.expr_span.unwrap().slice(input),
        "proc[selected]==(1${filter:++1})-proc[start]"
    );

    let expr = command
        .expr_ast
        .as_ref()
        .expect("expected typed arithmetic AST");
    let ArithmeticExpr::Binary { left, op, right } = &expr.kind else {
        panic!("expected comparison expression");
    };
    assert_eq!(*op, ArithmeticBinaryOp::Equal);

    let ArithmeticExpr::Indexed { name, index } = &left.kind else {
        panic!("expected indexed left operand");
    };
    assert_eq!(name, "proc");
    expect_variable(index, "selected");

    let ArithmeticExpr::Binary {
        left: subtract_left,
        op: subtract_op,
        right: subtract_right,
    } = &right.kind
    else {
        panic!("expected subtraction on comparison right-hand side");
    };
    assert_eq!(*subtract_op, ArithmeticBinaryOp::Subtract);

    let ArithmeticExpr::Parenthesized { expression } = &subtract_left.kind else {
        panic!("expected grouped dynamic numeric literal");
    };
    expect_shell_word(expression, input, "1${filter:++1}");

    let ArithmeticExpr::Indexed { name, index } = &subtract_right.kind else {
        panic!("expected indexed right operand");
    };
    assert_eq!(name, "proc");
    expect_variable(index, "start");
}

#[test]
fn test_parse_arithmetic_command_with_nested_parens_and_double_right_paren() {
    let input = "(( (previous_pipe_index > 0) && (previous_pipe_index == ($# - 1)) ))\n";
    let script = Parser::new(input).parse().unwrap().file;

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

    assert!(redirects.is_empty());
    assert_eq!(command.left_paren_span.slice(input), "((");
    assert_eq!(command.right_paren_span.slice(input), "))");
    assert_eq!(
        command.expr_span.unwrap().slice(input),
        " (previous_pipe_index > 0) && (previous_pipe_index == ($# - 1)) "
    );
}

#[test]
fn test_parse_arithmetic_command_with_nested_parens_before_outer_close() {
    let input = "(( a <= (1 || 2)))\n";
    let script = Parser::new(input).parse().unwrap().file;

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

    assert!(redirects.is_empty());
    assert_eq!(command.left_paren_span.slice(input), "((");
    assert_eq!(command.right_paren_span.slice(input), "))");
    assert_eq!(command.expr_span.unwrap().slice(input), " a <= (1 || 2)");
}

#[test]
fn test_parse_arithmetic_command_with_grouped_term_before_logical_and() {
    let input = "((threads>(cpu_height-3)*3 && tty_width>=200))\n";
    let script = Parser::new(input).parse().unwrap().file;

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

    assert!(redirects.is_empty());
    assert_eq!(command.left_paren_span.slice(input), "((");
    assert_eq!(command.right_paren_span.slice(input), "))");
    assert_eq!(
        command.expr_span.unwrap().slice(input),
        "threads>(cpu_height-3)*3 && tty_width>=200"
    );
}

#[test]
fn test_parse_arithmetic_command_with_nested_double_parens_and_grouping() {
    let input = "(( x = ((1 + 2) * (3 - 4)) ))\n";
    let script = Parser::new(input).parse().unwrap().file;

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

    assert!(redirects.is_empty());
    assert_eq!(command.left_paren_span.slice(input), "((");
    assert_eq!(command.right_paren_span.slice(input), "))");
    assert_eq!(
        command.expr_span.unwrap().slice(input),
        " x = ((1 + 2) * (3 - 4)) "
    );

    let ArithmeticExpr::Assignment { target, op, value } = &command
        .expr_ast
        .as_ref()
        .expect("expected typed arithmetic AST")
        .kind
    else {
        panic!("expected arithmetic assignment");
    };
    assert_eq!(*op, ArithmeticAssignOp::Assign);
    let ArithmeticLvalue::Variable(name) = target else {
        panic!("expected variable assignment target");
    };
    assert_eq!(name, "x");
    assert!(matches!(value.kind, ArithmeticExpr::Parenthesized { .. }));
}

#[test]
fn test_parse_arithmetic_command_respects_precedence_and_associativity() {
    let input = "(( a + b * c ** d ))\n";
    let script = Parser::new(input).parse().unwrap().file;

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

    let expr = command
        .expr_ast
        .as_ref()
        .expect("expected typed arithmetic AST");
    let ArithmeticExpr::Binary {
        left,
        op: add_op,
        right,
    } = &expr.kind
    else {
        panic!("expected additive expression");
    };
    assert_eq!(*add_op, ArithmeticBinaryOp::Add);
    expect_variable(left, "a");

    let ArithmeticExpr::Binary {
        left: mul_left,
        op: mul_op,
        right: mul_right,
    } = &right.kind
    else {
        panic!("expected multiplicative expression");
    };
    assert_eq!(*mul_op, ArithmeticBinaryOp::Multiply);
    expect_variable(mul_left, "b");

    let ArithmeticExpr::Binary {
        left: pow_left,
        op: pow_op,
        right: pow_right,
    } = &mul_right.kind
    else {
        panic!("expected power expression");
    };
    assert_eq!(*pow_op, ArithmeticBinaryOp::Power);
    expect_variable(pow_left, "c");
    expect_variable(pow_right, "d");
}

#[test]
fn test_parse_arithmetic_command_parses_updates_ternary_and_comma() {
    let input = "(( ++i ? j-- : (k = 1), m ))\n";
    let script = Parser::new(input).parse().unwrap().file;

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

    let expr = command
        .expr_ast
        .as_ref()
        .expect("expected typed arithmetic AST");
    let ArithmeticExpr::Binary {
        left,
        op: comma_op,
        right,
    } = &expr.kind
    else {
        panic!("expected comma expression");
    };
    assert_eq!(*comma_op, ArithmeticBinaryOp::Comma);
    expect_variable(right, "m");

    let ArithmeticExpr::Conditional {
        condition,
        then_expr,
        else_expr,
    } = &left.kind
    else {
        panic!("expected conditional expression");
    };

    let ArithmeticExpr::Unary { op: unary_op, expr } = &condition.kind else {
        panic!("expected prefix update condition");
    };
    assert_eq!(*unary_op, ArithmeticUnaryOp::PreIncrement);
    expect_variable(expr, "i");

    let ArithmeticExpr::Postfix {
        expr,
        op: postfix_op,
    } = &then_expr.kind
    else {
        panic!("expected postfix update in then branch");
    };
    assert_eq!(*postfix_op, ArithmeticPostfixOp::Decrement);
    expect_variable(expr, "j");

    let ArithmeticExpr::Parenthesized { expression } = &else_expr.kind else {
        panic!("expected parenthesized else branch");
    };
    let ArithmeticExpr::Assignment { target, op, value } = &expression.kind else {
        panic!("expected assignment inside else branch");
    };
    assert_eq!(*op, ArithmeticAssignOp::Assign);
    let ArithmeticLvalue::Variable(name) = target else {
        panic!("expected variable else target");
    };
    assert_eq!(name, "k");
    expect_number(value, input, "1");
}

#[test]
fn test_double_left_paren_command_closed_with_spaced_right_parens_parses_as_subshells() {
    let input = "(( echo 1\necho 2\n(( x ))\n: $(( x ))\necho 3\n) )\n";
    let script = Parser::new(input).parse().unwrap().file;

    let (compound, redirects) = expect_compound(&script.body[0]);
    let AstCompoundCommand::Subshell(commands) = compound else {
        panic!("expected outer subshell");
    };
    assert!(redirects.is_empty());
    assert_eq!(commands.len(), 1);
    assert!(matches!(
        commands[0].command,
        AstCommand::Compound(AstCompoundCommand::Subshell(_))
    ));
}