use super::*;
#[test]
fn test_posix_function_allows_conditional_body() {
let input = "f() [[ -n \"$x\" ]]\n";
let script = Parser::new(input).parse().unwrap().file;
let function = expect_function(&script.body[0]);
let (compound, redirects) = expect_compound(function.body.as_ref());
let AstCompoundCommand::Conditional(command) = compound else {
panic!("expected conditional function body");
};
assert!(!function.uses_function_keyword());
assert!(function.has_name_parens());
assert!(redirects.is_empty());
assert_eq!(command.span.slice(input), "[[ -n \"$x\" ]]");
}
#[test]
fn test_posix_function_parses_negated_bracket_if_body() {
let input = "\
recent_file(){
if ! [ -f \"$file\" ]; then
return 1
elif find \"$file\" -mtime -\"$days\" -print | grep -q .; then
return 0
else
local days_ago_in_seconds
days_ago_in_seconds=\"$(date -d \"$days days ago\" '+%s')\"
if is_mac; then
if [ \"$(stat -f '%m' \"$file\")\" -ge \"$days_ago_in_seconds\" ]; then
return 0
else
return 1
fi
elif [ \"$(stat -c '%Y' \"$file\")\" -ge \"$days_ago_in_seconds\" ]; then
return 0
else
return 1
fi
fi
}
";
let parsed = Parser::new(input).parse().unwrap();
assert!(matches!(
expect_function(&parsed.file.body[0]).body.command,
AstCommand::Compound(..)
));
}
#[test]
fn test_double_left_paren_test_clause_parses_as_command() {
let input =
"if ! ((test x\\\"$i\\\" = x-g) || (test x\\\"$i\\\" = x-O2)); then\n echo bye\nfi\n";
Parser::new(input).parse().unwrap();
}
#[test]
fn test_double_left_paren_pipeline_parses_as_command() {
let input = "((cat </dev/zero; echo $? >&7) | true) 7>&1\n";
Parser::new(input).parse().unwrap();
}
#[test]
fn test_parse_conditional_builds_structured_logical_ast() {
let script = Parser::new("[[ ! (foo && bar) ]]\n").parse().unwrap().file;
let (compound, _) = expect_compound(&script.body[0]);
let AstCompoundCommand::Conditional(command) = compound else {
panic!("expected conditional compound command");
};
let ConditionalExpr::Unary(unary) = &command.expression else {
panic!("expected unary conditional");
};
assert_eq!(unary.op, ConditionalUnaryOp::Not);
let ConditionalExpr::Parenthesized(paren) = unary.expr.as_ref() else {
panic!("expected parenthesized conditional");
};
let ConditionalExpr::Binary(binary) = paren.expr.as_ref() else {
panic!("expected binary conditional");
};
assert_eq!(binary.op, ConditionalBinaryOp::And);
assert!(matches!(binary.left.as_ref(), ConditionalExpr::Word(_)));
assert!(matches!(binary.right.as_ref(), ConditionalExpr::Word(_)));
assert_eq!(command.left_bracket_span.start.column, 1);
assert_eq!(command.right_bracket_span.start.column, 19);
}
#[test]
fn test_parse_conditional_accepts_nested_grouping_with_double_parens() {
let input = "[[ ! -e \"$cache\" && (( -e \"$prefix/n\" && ! -w \"$prefix/n\" ) || ( ! -e \"$prefix/n\" && ! -w \"$prefix\" )) ]]\n";
let script = Parser::new(input).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::And);
let ConditionalExpr::Parenthesized(paren) = binary.right.as_ref() else {
panic!("expected parenthesized conditional term");
};
assert_eq!(
paren.span().slice(input),
"(( -e \"$prefix/n\" && ! -w \"$prefix/n\" ) || ( ! -e \"$prefix/n\" && ! -w \"$prefix\" ))"
);
let ConditionalExpr::Binary(inner) = paren.expr.as_ref() else {
panic!("expected grouped binary conditional");
};
assert_eq!(inner.op, ConditionalBinaryOp::Or);
assert!(matches!(
inner.left.as_ref(),
ConditionalExpr::Parenthesized(_)
));
assert!(matches!(
inner.right.as_ref(),
ConditionalExpr::Parenthesized(_)
));
}
#[test]
fn test_parse_conditional_accepts_adjacent_group_closes_after_rhs_words() {
let input = "[[ -n $brew_prefix && (($brew_prefix != \"/usr\" && $brew_prefix != \"/usr/local\") || (is_mac && osx_using_default_compiler && $CFLAGS =~ (^|\\ )-isysroot\\ )) ]]\n";
let script = Parser::new(input).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::And);
assert!(matches!(
binary.right.as_ref(),
ConditionalExpr::Parenthesized(_)
));
}
#[test]
fn test_parse_conditional_stops_quoted_rhs_before_adjacent_group_closes() {
for input in ["[[ (( x == \"foo\" )) ]]\n", "[[ (( x =~ \"foo\" )) ]]\n"] {
let script = Parser::new(input).parse().unwrap().file;
let (compound, _) = expect_compound(&script.body[0]);
let AstCompoundCommand::Conditional(command) = compound else {
panic!("expected conditional compound command for {input}");
};
assert!(matches!(
command.expression,
ConditionalExpr::Parenthesized(_)
));
}
}
#[test]
fn test_parse_conditional_pattern_rhs_preserves_structure() {
let input = "[[ foo == @(bar|baz)* ]]\n";
let script = Parser::new(input).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::PatternEq);
let ConditionalExpr::Pattern(pattern) = binary.right.as_ref() else {
panic!("expected pattern rhs");
};
assert_eq!(pattern.render(input), "@(bar|baz)*");
assert!(matches!(
&pattern.parts[0].kind,
PatternPart::Group {
kind: PatternGroupKind::ExactlyOne,
..
}
));
assert!(matches!(&pattern.parts[1].kind, PatternPart::AnyString));
}
#[test]
fn test_parse_zsh_conditional_unary_operand_with_subscripted_word() {
let input = "[[ -z $opts[(r)-P] ]]\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::Unary(unary) = &command.expression else {
panic!("expected unary conditional");
};
assert_eq!(unary.op, ConditionalUnaryOp::EmptyString);
let ConditionalExpr::Word(word) = unary.expr.as_ref() else {
panic!("expected word operand");
};
assert_eq!(word.render(input), "$opts[(r)-P]");
}
#[test]
fn test_parse_zsh_conditional_arithmetic_comparison_operand_with_subscripted_word() {
let input = "[[ $GLOBALIAS_FILTER_VALUES[(Ie)$word] -eq 0 ]]\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::ArithmeticEq);
let ConditionalExpr::Word(left) = binary.left.as_ref() else {
panic!("expected word operand on left");
};
assert_eq!(left.render(input), "$GLOBALIAS_FILTER_VALUES[(Ie)$word]");
}
#[test]
fn test_parse_zsh_conditional_pattern_rhs_with_backrefs_and_parameter_expansion() {
let input = "[[ \"$buf\" == (#b)(*)(${~pat})* ]]\n";
let script = parse_zsh_with_options(input, |options| {
options.extended_glob = OptionValue::On;
})
.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::PatternEq);
let ConditionalExpr::Pattern(pattern) = binary.right.as_ref() else {
panic!("expected pattern rhs");
};
assert_eq!(pattern.render(input), "(#b)(*)(${~pat})*");
}
#[test]
fn test_parse_zsh_conditional_pattern_rhs_with_inline_anchors() {
let input = "[[ $buffer != (#s)[$'\\t -~']#(#e) ]]\n";
let script = parse_zsh_with_options(input, |options| {
options.extended_glob = OptionValue::On;
})
.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::PatternNe);
let ConditionalExpr::Pattern(pattern) = binary.right.as_ref() else {
panic!("expected pattern rhs");
};
assert_eq!(pattern.render(input), "(#s)[\t -~]#(#e)");
}
#[test]
fn test_parse_zsh_conditional_pattern_rhs_accepts_bare_alternation_groups() {
let input = "[[ $OPTARG != (|+|-)<->(|.<->)(|[eE](|-|+)<->) ]]\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::PatternNe);
let ConditionalExpr::Pattern(pattern) = binary.right.as_ref() else {
panic!("expected pattern rhs");
};
assert_eq!(pattern.render(input), "(|+|-)<->(|.<->)(|[eE](|-|+)<->)");
assert!(!pattern.parts.is_empty());
}
#[test]
fn test_zsh_conditional_ksh_glob_requires_option() {
let input = "[[ $mode == @(disable|enable) ]]\n";
let default_script = Parser::with_dialect(input, ShellDialect::Zsh)
.parse()
.unwrap()
.file;
let (default_compound, _) = expect_compound(&default_script.body[0]);
let AstCompoundCommand::Conditional(default_command) = default_compound else {
panic!("expected conditional compound command");
};
let ConditionalExpr::Binary(default_binary) = &default_command.expression else {
panic!("expected binary conditional");
};
let ConditionalExpr::Pattern(default_pattern) = default_binary.right.as_ref() else {
panic!("expected pattern rhs");
};
assert_eq!(default_pattern.render_syntax(input), "@(disable|enable)");
let [prefix, group] = default_pattern.parts.as_slice() else {
panic!("expected literal prefix followed by bare zsh group");
};
assert!(matches!(&prefix.kind, PatternPart::Literal(_)));
let PatternPart::Group { kind, patterns } = &group.kind else {
panic!("expected bare zsh group after literal prefix");
};
assert_eq!(*kind, PatternGroupKind::ExactlyOne);
assert_eq!(
patterns
.iter()
.map(|pattern| pattern.render_syntax(input))
.collect::<Vec<_>>(),
vec!["disable", "enable"]
);
let script = parse_zsh_with_options(input, |options| {
options.ksh_glob = OptionValue::On;
})
.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");
};
let ConditionalExpr::Pattern(pattern) = binary.right.as_ref() else {
panic!("expected pattern rhs");
};
assert_eq!(pattern.render_syntax(input), "@(disable|enable)");
let glob = expect_pattern_zsh_qualified_glob(pattern);
let [segment] = glob.segments.as_slice() else {
panic!("expected a single pattern segment");
};
let [part] = expect_zsh_glob_pattern_segment(segment).parts.as_slice() else {
panic!("expected a single group part");
};
let PatternPart::Group { kind, patterns } = &part.kind else {
panic!("expected ksh glob group");
};
assert_eq!(*kind, PatternGroupKind::ExactlyOne);
assert_eq!(
patterns
.iter()
.map(|pattern| pattern.render_syntax(input))
.collect::<Vec<_>>(),
vec!["disable", "enable"]
);
}
#[test]
fn test_zsh_conditional_prefixed_bare_group_reparse_handles_single_glob_word() {
let input = "[[ $mode == *@(disable|enable) ]]\n";
let default_script = Parser::with_dialect(input, ShellDialect::Zsh)
.parse()
.unwrap()
.file;
let (default_compound, _) = expect_compound(&default_script.body[0]);
let AstCompoundCommand::Conditional(default_command) = default_compound else {
panic!("expected conditional compound command");
};
let ConditionalExpr::Binary(default_binary) = &default_command.expression else {
panic!("expected binary conditional");
};
let ConditionalExpr::Pattern(default_pattern) = default_binary.right.as_ref() else {
panic!("expected pattern rhs");
};
assert_eq!(default_pattern.render_syntax(input), "*@(disable|enable)");
let [wildcard, prefix, group] = default_pattern.parts.as_slice() else {
panic!("expected wildcard, literal prefix, and bare zsh group");
};
assert!(matches!(&wildcard.kind, PatternPart::AnyString));
assert!(matches!(&prefix.kind, PatternPart::Literal(_)));
let PatternPart::Group { kind, patterns } = &group.kind else {
panic!("expected bare zsh group after literal prefix");
};
assert_eq!(*kind, PatternGroupKind::ExactlyOne);
assert_eq!(
patterns
.iter()
.map(|pattern| pattern.render_syntax(input))
.collect::<Vec<_>>(),
vec!["disable", "enable"]
);
let script = parse_zsh_with_options(input, |options| {
options.ksh_glob = OptionValue::On;
})
.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");
};
let ConditionalExpr::Pattern(pattern) = binary.right.as_ref() else {
panic!("expected pattern rhs");
};
assert_eq!(pattern.render_syntax(input), "*@(disable|enable)");
let glob = expect_pattern_zsh_qualified_glob(pattern);
let [segment] = glob.segments.as_slice() else {
panic!("expected a single pattern segment");
};
assert!(matches!(
expect_zsh_glob_pattern_segment(segment).parts.as_slice(),
[
PatternPartNode {
kind: PatternPart::AnyString,
..
},
PatternPartNode {
kind: PatternPart::Group {
kind: PatternGroupKind::ExactlyOne,
..
},
..
}
]
));
}
#[test]
fn test_zsh_conditional_extended_glob_backreference_requires_option() {
let input = "[[ $buf == (#b)(*) ]]\n";
let default_script = Parser::with_dialect(input, ShellDialect::Zsh)
.parse()
.unwrap()
.file;
let (default_compound, _) = expect_compound(&default_script.body[0]);
let AstCompoundCommand::Conditional(default_command) = default_compound else {
panic!("expected conditional compound command");
};
let ConditionalExpr::Binary(default_binary) = &default_command.expression else {
panic!("expected binary conditional");
};
let ConditionalExpr::Pattern(default_pattern) = default_binary.right.as_ref() else {
panic!("expected pattern rhs");
};
let default_glob = expect_pattern_zsh_qualified_glob(default_pattern);
let [default_segment] = default_glob.segments.as_slice() else {
panic!("expected a single source-preserved pattern segment");
};
assert_eq!(
expect_zsh_glob_pattern_segment(default_segment).render_syntax(input),
"(#b)(*)"
);
let script = parse_zsh_with_options(input, |options| {
options.extended_glob = OptionValue::On;
})
.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");
};
let ConditionalExpr::Pattern(pattern) = binary.right.as_ref() else {
panic!("expected pattern rhs");
};
let glob = expect_pattern_zsh_qualified_glob(pattern);
let [control, segment] = glob.segments.as_slice() else {
panic!("expected inline control followed by pattern segment");
};
assert!(matches!(
control,
ZshGlobSegment::InlineControl(ZshInlineGlobControl::Backreferences { span })
if span.slice(input) == "(#b)"
));
assert_eq!(
expect_zsh_glob_pattern_segment(segment).render_syntax(input),
"(*)"
);
}
#[test]
fn test_zsh_conditional_bare_group_requires_sh_glob_off() {
let input = "[[ $mode == (foo|bar) ]]\n";
let default_script = Parser::with_dialect(input, ShellDialect::Zsh)
.parse()
.unwrap()
.file;
let (default_compound, _) = expect_compound(&default_script.body[0]);
let AstCompoundCommand::Conditional(default_command) = default_compound else {
panic!("expected conditional compound command");
};
let ConditionalExpr::Binary(default_binary) = &default_command.expression else {
panic!("expected binary conditional");
};
let ConditionalExpr::Pattern(default_pattern) = default_binary.right.as_ref() else {
panic!("expected pattern rhs");
};
assert!(matches!(
default_pattern.parts.as_slice(),
[PatternPartNode {
kind: PatternPart::Word(word),
..
}] if matches!(
word.parts.as_slice(),
[WordPartNode {
kind: WordPart::ZshQualifiedGlob(_),
..
}]
)
));
let sh_glob_script = parse_zsh_with_options(input, |options| {
options.sh_glob = OptionValue::On;
})
.file;
let (sh_glob_compound, _) = expect_compound(&sh_glob_script.body[0]);
let AstCompoundCommand::Conditional(sh_glob_command) = sh_glob_compound else {
panic!("expected conditional compound command");
};
let ConditionalExpr::Binary(sh_glob_binary) = &sh_glob_command.expression else {
panic!("expected binary conditional");
};
let ConditionalExpr::Pattern(sh_glob_pattern) = sh_glob_binary.right.as_ref() else {
panic!("expected pattern rhs");
};
assert_eq!(sh_glob_pattern.render(input), "(foo|bar)");
assert!(!matches!(
sh_glob_pattern.parts.as_slice(),
[PatternPartNode {
kind: PatternPart::Word(word),
..
}] if matches!(
word.parts.as_slice(),
[WordPartNode {
kind: WordPart::ZshQualifiedGlob(_),
..
}]
)
));
}
#[test]
fn test_parse_zsh_conditional_pattern_with_hash_repetition_after_char_class() {
let input = "[[ $_p9k__ret == (#b)Python\\ ([[:digit:].]##)* ]]\n";
parse_zsh_with_options(input, |options| {
options.extended_glob = OptionValue::On;
});
}
#[test]
fn test_parse_zsh_conditional_pattern_with_numeric_range_prefix_and_and_rhs() {
let input = "[[ $load == <->(|.<->) && $load != $_p9k__load_value ]]\n";
Parser::with_dialect(input, ShellDialect::Zsh)
.parse()
.unwrap();
}
#[test]
fn test_zsh_conditional_numeric_range_requires_hyphen() {
let literal_input = "[[ $jobspec == jobspec:<123> ]]\n";
let literal_script = Parser::with_dialect(literal_input, ShellDialect::Zsh)
.parse()
.unwrap()
.file;
let (literal_compound, _) = expect_compound(&literal_script.body[0]);
let AstCompoundCommand::Conditional(literal_command) = literal_compound else {
panic!("expected conditional compound command");
};
let ConditionalExpr::Binary(literal_binary) = &literal_command.expression else {
panic!("expected binary conditional");
};
let ConditionalExpr::Pattern(literal_pattern) = literal_binary.right.as_ref() else {
panic!("expected pattern rhs");
};
assert_eq!(literal_pattern.render(literal_input), "jobspec:<123>");
assert!(!pattern_has_zsh_qualified_glob(literal_pattern));
let range_input = "[[ $jobspec == jobspec:<1-9> ]]\n";
let range_script = Parser::with_dialect(range_input, ShellDialect::Zsh)
.parse()
.unwrap()
.file;
let (range_compound, _) = expect_compound(&range_script.body[0]);
let AstCompoundCommand::Conditional(range_command) = range_compound else {
panic!("expected conditional compound command");
};
let ConditionalExpr::Binary(range_binary) = &range_command.expression else {
panic!("expected binary conditional");
};
let ConditionalExpr::Pattern(range_pattern) = range_binary.right.as_ref() else {
panic!("expected pattern rhs");
};
let glob = expect_pattern_zsh_qualified_glob(range_pattern);
let [segment] = glob.segments.as_slice() else {
panic!("expected a single pattern segment");
};
assert_eq!(
expect_zsh_glob_pattern_segment(segment).render_syntax(range_input),
"jobspec:<1-9>"
);
}
#[test]
fn test_zsh_setopt_ksh_glob_changes_following_conditional_pattern_parse() {
let input = concat!(
"[[ $mode == @(disable|enable) ]]\n",
"setopt ksh_glob\n",
"[[ $mode == @(disable|enable) ]]\n",
);
let script = Parser::with_dialect(input, ShellDialect::Zsh)
.parse()
.unwrap()
.file;
let (first_compound, _) = expect_compound(&script.body[0]);
let AstCompoundCommand::Conditional(first_command) = first_compound else {
panic!("expected conditional compound command");
};
let ConditionalExpr::Binary(first_binary) = &first_command.expression else {
panic!("expected binary conditional");
};
let ConditionalExpr::Pattern(first_pattern) = first_binary.right.as_ref() else {
panic!("expected pattern rhs");
};
let [first_prefix, first_group] = first_pattern.parts.as_slice() else {
panic!("expected literal prefix followed by bare zsh group");
};
assert!(matches!(&first_prefix.kind, PatternPart::Literal(_)));
assert!(matches!(
&first_group.kind,
PatternPart::Group {
kind: PatternGroupKind::ExactlyOne,
..
}
));
let (second_compound, _) = expect_compound(&script.body[2]);
let AstCompoundCommand::Conditional(second_command) = second_compound else {
panic!("expected conditional compound command");
};
let ConditionalExpr::Binary(second_binary) = &second_command.expression else {
panic!("expected binary conditional");
};
let ConditionalExpr::Pattern(second_pattern) = second_binary.right.as_ref() else {
panic!("expected pattern rhs");
};
let second_glob = expect_pattern_zsh_qualified_glob(second_pattern);
let [second_segment] = second_glob.segments.as_slice() else {
panic!("expected a single pattern segment");
};
assert!(matches!(
expect_zsh_glob_pattern_segment(second_segment)
.parts
.as_slice(),
[PatternPartNode {
kind: PatternPart::Group {
kind: PatternGroupKind::ExactlyOne,
..
},
..
}]
));
}
#[test]
fn test_parse_zsh_conditional_pattern_rhs_accepts_hash_q_glob_qualifier() {
let input = "[[ foo.txt == *.txt(#qN) ]]\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::PatternEq);
let ConditionalExpr::Pattern(pattern) = binary.right.as_ref() else {
panic!("expected pattern rhs");
};
assert_eq!(pattern.render(input), "*.txt(#qN)");
}
#[test]
fn test_parse_zsh_conditional_pattern_rhs_accepts_negated_hash_q_glob_qualifier() {
let input = "[[ $file != *.tmp(#qN) ]]\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::PatternNe);
let ConditionalExpr::Pattern(pattern) = binary.right.as_ref() else {
panic!("expected pattern rhs");
};
assert_eq!(pattern.render(input), "*.tmp(#qN)");
}
#[test]
fn test_parse_zsh_remaining_upstream_conditionals_and_control_flow_examples() {
for input in [
"[[ ( a == b ) || ( c == d ) ]]\n",
"[[ ! -f file ]]\n",
"[[ -f file1 && ( -r file1 || -w file1 ) ]]\n",
"case $var in\n *.txt) echo \"text file\" ;;\n *.sh) echo \"shell script\" ;;\n *) echo \"other\" ;;\nesac\n",
"if [[ $a -eq $b ]]; then\n echo \"equal\"\nfi\n",
"if [[ $a -gt 5 ]]; then\n echo \"greater\"\nelif [[ $a -lt 0 ]]; then\n echo \"less\"\nelse\n echo \"between\"\nfi\n",
"until [[ $count -ge 10 ]]; do\n echo $count\n ((count++))\ndone\n",
"while true; do\n if [[ $done ]]; then\n break\n fi\n echo \"working\"\ndone\n",
] {
Parser::with_dialect(input, ShellDialect::Zsh)
.parse()
.unwrap();
}
}
#[test]
fn test_parse_zsh_newer_than_conditional_from_upstream() {
let input = "[[ file1 -nt file2 ]]\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::NewerThan);
}
#[test]
fn test_parse_zsh_remaining_upstream_conditional_glob_examples() {
for input in [
"[[ $file == *.txt ]]\n",
"[[ $name != (#i)*.TMP ]]\n",
"[[ -f *.log(.) ]]\n",
] {
parse_zsh_with_options(input, |options| {
options.extended_glob = OptionValue::On;
});
}
}
#[test]
fn test_parse_zsh_conditional_group_with_arithmetic_subexpression() {
let input = "until [[ $i -gt 99 || ( $i -ge $((length - ellen)) || $dir == $part ) && ( (( ${#expn} == 1 )) || $dir = $expn ) ]]; do\n :\ndone\n";
Parser::with_dialect(input, ShellDialect::Zsh)
.parse()
.unwrap();
}
#[test]
fn test_parse_conditional_var_ref_operand() {
let input = "[[ -v assoc[$key] ]]\n";
let script = Parser::new(input).parse().unwrap().file;
let (compound, _) = expect_compound(&script.body[0]);
let AstCompoundCommand::Conditional(command) = compound else {
panic!("expected conditional compound command");
};
let ConditionalExpr::Unary(unary) = &command.expression else {
panic!("expected unary conditional");
};
assert_eq!(unary.op, ConditionalUnaryOp::VariableSet);
let ConditionalExpr::VarRef(var_ref) = unary.expr.as_ref() else {
panic!("expected typed var-ref operand");
};
assert_eq!(var_ref.name.as_str(), "assoc");
assert_eq!(var_ref.name_span.slice(input), "assoc");
expect_subscript(var_ref, input, "$key");
}
#[test]
fn test_parse_conditional_quoted_command_substitution_preserves_nested_quotes() {
let input = "[[ \"$(get_permission \"$1\")\" != \"$(id -u)\" ]]\n";
let script = Parser::new(input).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::PatternNe);
let ConditionalExpr::Word(left) = binary.left.as_ref() else {
panic!("expected left operand word");
};
let ConditionalExpr::Pattern(right) = binary.right.as_ref() else {
panic!("expected right operand pattern");
};
assert_eq!(left.span.slice(input), "\"$(get_permission \"$1\")\"");
assert_eq!(right.span.slice(input), "\"$(id -u)\"");
let WordPart::DoubleQuoted { parts, .. } = &left.parts[0].kind else {
panic!("expected double-quoted left operand");
};
let WordPart::CommandSubstitution { body, syntax } = &parts[0].kind else {
panic!("expected left command substitution");
};
assert_eq!(*syntax, CommandSubstitutionSyntax::DollarParen);
let inner = expect_simple(&body[0]);
assert_eq!(inner.name.render(input), "get_permission");
assert_eq!(inner.args[0].render_syntax(input), "\"$1\"");
}
#[test]
fn test_parse_conditional_regex_rhs_preserves_structure() {
let input = "[[ foo =~ [ab](c|d) ]]\n";
let script = Parser::new(input).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.render(input), "[ab](c|d)");
}
#[test]
fn test_parse_conditional_regex_rhs_with_double_left_paren_groups() {
let input = "[[ x =~ ^\\\"\\-1[[:blank:]]((\\?[luds])+).* ]]\n";
let script = Parser::new(input).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.render(input), "^\\\"\\-1[[:blank:]]((\\?[luds])+).*");
}
#[test]
fn test_parse_conditional_regex_rhs_keeps_parameter_pattern_with_literal_paren() {
let input = "[[ \"${2}\" =~ ^${__theme%% (*} ]]\n";
let script = Parser::new(input).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.render(input), "^${__theme%% (*}");
}
#[test]
fn test_parse_conditional_regex_allows_left_brace_operand() {
let input = "[[ { =~ \"{\" ]]\n";
let script = Parser::new(input).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::Word(left) = binary.left.as_ref() else {
panic!("expected literal left operand");
};
assert_eq!(left.span.slice(input), "{");
let ConditionalExpr::Regex(right) = binary.right.as_ref() else {
panic!("expected regex rhs");
};
assert_eq!(right.render(input), "{");
}
#[test]
fn test_parse_if_condition_accepts_chained_double_brackets_with_regex_brace_literal() {
let input = "if [[ $MOTD ]] && ! [[ $MOTD =~ ^{ ]]; then\n :\nfi\n";
let script = Parser::new(input).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);
assert_eq!(expect_binary(&command.condition[0]).op, BinaryOp::And);
}
#[test]
fn test_posix_dialect_rejects_double_bracket_conditionals() {
let error = Parser::with_dialect("[[ foo == bar ]]\n", ShellDialect::Posix)
.parse()
.unwrap_err();
assert!(matches!(
error,
Error::Parse { message, .. } if message.contains("[[ ]] conditionals")
));
}
#[test]
fn test_bash_and_mksh_dialects_accept_double_bracket_conditionals() {
Parser::with_dialect("[[ foo == bar ]]\n", ShellDialect::Bash)
.parse()
.unwrap();
Parser::with_dialect("[[ foo == bar ]]\n", ShellDialect::Mksh)
.parse()
.unwrap();
Parser::with_dialect("[[ foo == bar ]]\n", ShellDialect::Zsh)
.parse()
.unwrap();
}