Skip to main content

shrub_parser/
test_command.rs

1//! Parser for shell test commands.
2
3use crate::{ast, error};
4
5/// Parses a test command expression.
6///
7/// # Arguments
8///
9/// * `input` - The test command expression to parse, in string form.
10///
11/// # Errors
12///
13/// Returns [`crate::TestCommandParseError`] if the arguments do not form a
14/// valid `test` expression, such as a missing operand or unbalanced
15/// parentheses.
16pub fn parse<S: AsRef<str>>(input: &[S]) -> Result<ast::TestExpr, error::TestCommandParseError> {
17    let expr =
18        test_command::full_expression(&input.iter().map(AsRef::as_ref).collect::<Vec<&str>>())?;
19
20    Ok(expr)
21}
22
23peg::parser! {
24    grammar test_command<'a>() for [&'a str] {
25        pub(crate) rule full_expression() -> ast::TestExpr =
26            end() { ast::TestExpr::False } /
27            e:one_arg_expr() end() { e } /
28            e:two_arg_expr() end()  { e } /
29            e:three_arg_expr() end()  { e } /
30            e:four_arg_expr() end()  { e } /
31            expression()
32
33        rule one_arg_expr() -> ast::TestExpr =
34            [s] { ast::TestExpr::Literal(s.to_owned()) }
35
36        rule two_arg_expr() -> ast::TestExpr =
37            ["!"] e:one_arg_expr() { ast::TestExpr::Not(Box::from(e)) } /
38            op:unary_op() [s] { ast::TestExpr::UnaryTest(op, s.to_owned()) }
39
40        rule three_arg_expr() -> ast::TestExpr =
41            [left] ["-a"] [right] { ast::TestExpr::And(Box::from(ast::TestExpr::Literal(left.to_owned())), Box::from(ast::TestExpr::Literal(right.to_owned()))) } /
42            [left] ["-o"] [right] { ast::TestExpr::Or(Box::from(ast::TestExpr::Literal(left.to_owned())), Box::from(ast::TestExpr::Literal(right.to_owned()))) } /
43            [left] op:binary_op() [right] { ast::TestExpr::BinaryTest(op, left.to_owned(), right.to_owned()) } /
44            ["!"] e:two_arg_expr() { ast::TestExpr::Not(Box::from(e)) } /
45            ["("] e:one_arg_expr() [")"] { e }
46
47        rule four_arg_expr() -> ast::TestExpr =
48            ["!"] e:three_arg_expr() { ast::TestExpr::Not(Box::from(e)) }
49
50        rule expression() -> ast::TestExpr = precedence! {
51            left:(@) ["-a"] right:@ { ast::TestExpr::And(Box::from(left), Box::from(right)) }
52            left:(@) ["-o"] right:@ { ast::TestExpr::Or(Box::from(left), Box::from(right)) }
53            --
54            ["("] e:expression() [")"] { ast::TestExpr::Parenthesized(Box::from(e)) }
55            --
56            ["!"] e:@ { ast::TestExpr::Not(Box::from(e)) }
57            --
58            [left] op:binary_op() [right] { ast::TestExpr::BinaryTest(op, left.to_owned(), right.to_owned()) }
59            --
60            op:unary_op() [operand] { ast::TestExpr::UnaryTest(op, operand.to_owned()) }
61            --
62            [s] { ast::TestExpr::Literal(s.to_owned()) }
63        }
64
65        rule unary_op() -> ast::UnaryPredicate =
66            ["-a"] { ast::UnaryPredicate::FileExists } /
67            ["-b"] { ast::UnaryPredicate::FileExistsAndIsBlockSpecialFile } /
68            ["-c"] { ast::UnaryPredicate::FileExistsAndIsCharSpecialFile } /
69            ["-d"] { ast::UnaryPredicate::FileExistsAndIsDir } /
70            ["-e"] { ast::UnaryPredicate::FileExists } /
71            ["-f"] { ast::UnaryPredicate::FileExistsAndIsRegularFile } /
72            ["-g"] { ast::UnaryPredicate::FileExistsAndIsSetgid } /
73            ["-h"] { ast::UnaryPredicate::FileExistsAndIsSymlink } /
74            ["-k"] { ast::UnaryPredicate::FileExistsAndHasStickyBit } /
75            ["-n"] { ast::UnaryPredicate::StringHasNonZeroLength } /
76            ["-o"] { ast::UnaryPredicate::ShellOptionEnabled } /
77            ["-p"] { ast::UnaryPredicate::FileExistsAndIsFifo } /
78            ["-r"] { ast::UnaryPredicate::FileExistsAndIsReadable } /
79            ["-s"] { ast::UnaryPredicate::FileExistsAndIsNotZeroLength } /
80            ["-t"] { ast::UnaryPredicate::FdIsOpenTerminal } /
81            ["-u"] { ast::UnaryPredicate::FileExistsAndIsSetuid } /
82            ["-v"] { ast::UnaryPredicate::ShellVariableIsSetAndAssigned } /
83            ["-w"] { ast::UnaryPredicate::FileExistsAndIsWritable } /
84            ["-x"] { ast::UnaryPredicate::FileExistsAndIsExecutable } /
85            ["-z"] { ast::UnaryPredicate::StringHasZeroLength } /
86            ["-G"] { ast::UnaryPredicate::FileExistsAndOwnedByEffectiveGroupId } /
87            ["-L"] { ast::UnaryPredicate::FileExistsAndIsSymlink } /
88            ["-N"] { ast::UnaryPredicate::FileExistsAndModifiedSinceLastRead } /
89            ["-O"] { ast::UnaryPredicate::FileExistsAndOwnedByEffectiveUserId } /
90            ["-R"] { ast::UnaryPredicate::ShellVariableIsSetAndNameRef } /
91            ["-S"] { ast::UnaryPredicate::FileExistsAndIsSocket }
92
93        rule binary_op() -> ast::BinaryPredicate =
94            ["=="]  { ast::BinaryPredicate::StringExactlyMatchesString } /
95            ["-ef"] { ast::BinaryPredicate::FilesReferToSameDeviceAndInodeNumbers } /
96            ["-eq"] { ast::BinaryPredicate::ArithmeticEqualTo } /
97            ["-ge"] { ast::BinaryPredicate::ArithmeticGreaterThanOrEqualTo } /
98            ["-gt"] { ast::BinaryPredicate::ArithmeticGreaterThan } /
99            ["-le"] { ast::BinaryPredicate::ArithmeticLessThanOrEqualTo } /
100            ["-lt"] { ast::BinaryPredicate::ArithmeticLessThan } /
101            ["-ne"] { ast::BinaryPredicate::ArithmeticNotEqualTo } /
102            ["-nt"] { ast::BinaryPredicate::LeftFileIsNewerOrExistsWhenRightDoesNot } /
103            ["-ot"] { ast::BinaryPredicate::LeftFileIsOlderOrDoesNotExistWhenRightDoes } /
104            ["="]   { ast::BinaryPredicate::StringExactlyMatchesString } /
105            ["!="]  { ast::BinaryPredicate::StringDoesNotExactlyMatchString } /
106            ["<"]   { ast::BinaryPredicate::LeftSortsBeforeRight } /
107            [">"]   { ast::BinaryPredicate::LeftSortsAfterRight }
108
109        rule end() = ![_]
110    }
111}