Match rules

Match rules are used to check if the files match the expected files. Multiple match rules can be defined and will be processed in order.

Rule syntax

A match rule contains a file selector, file class and action to take if a file name matches the selector.

rule       ::=  [ type ] filter [ "[" nameclass "]" ] "=>" action [ message ]
type       ::=  "+" | "-"
filter     ::=  ["!"] ["^"] glob [ "{" countrange "}" ]
nameclass  ::=  class [ "#" name ] | "#" name
action     ::=  "pass" | "pass-secret" | "pass-hidden" | "fail"
countrange ::=  from_count [ "," [ to_count] ] | "," to_count

Type

If the rule starts with a -, the matched files will be consumed (default), if the rule starts with a + they will not be consumed and remain available to match in later rules.

Filter

The filter is a Unix shell style glob pattern applied to the name of the file.

  • * matches in exactly one path segment

  • ** matches in multiple path segments (including none)

  • ? matches any single character

  • [seq] matches any character in seq

  • [!seq] matches any char not in seq

Path segments (separated with a forward slash / ) can be used. A * will match a single path segment, whereas a ** will match any number of path segments, i.e. * matches any character except the forward slash, whereas ** matches all characters. See the examples below for more details.

The filter has the following modifications to a normal glob pattern:

  • if the filter starts with a !, the rule is inverted, i.e. it will match if no files match the filter.

  • if the filter starts with a ^, the file glob is case-insensitive. In case both ! and ^ are needed, ! should be specified first.

  • the filter can be followed by a {x,y} count to indicate that the filter should only match if at least x files match, and match at most y files. Both x and y can be empty indicating 0 and infinity respectively. {x} is identical to {x,x}, i.e. there must be exactly x files matching the selector.

Files matched by a rule will be removed from the list of files for the next rule, unless the rule type is +.

Name and class

The file selector can be followed with a file class and (optionally) new file name between square brackets ([]). The class and name parts are separated by a hash sign (#), e.g. [class#name.txt].

Action

The action to take if the selector matches can be specified. The action can be one of: pass, pass-secret, pass-hidden, fail. These will generate a corresponding outcome. The pass-secret and pass-hidden outcomes will be marked as secret (not visible to learners, but visible to reviewers) and hidden (not visible to anyone) respectively.

Message

If a message is specified, it will be included in the report if the rule matches.

For positive match rules, the message can contain the following placeholders:

  • {#} is replaced with the number of matched files.

  • {=x|y} is replaced with the string x if 1 file matched, y if more than 1 file matched. Either string can be empty.

  • {*} is replaced with a comma separated list of matched file names,

  • {𝒾}, with 𝒾 an integer index ≥ 0, is replaced with the (𝒾+1)th matched file, i.e. {0} is replaced with the first matched file, {1} with the second, etc.

To include a literal { in the message, use \{.

Placeholders can only be used with positive matche rules, as negative matches (using the ! prefix) do not have files matched.

Examples

  • -*.py{2,} => fail Multiple Python files detected, expected no more than one.

    This rule will generate a fail outcome with an error message Multiple Python files detected, expected no more than one. if more than one *.py file is detected.

    If a none or a single *.py file is detected, the optional file will be consumed and the next rule will be processed.

  • !*.py{1} => fail Expected exactly one Python file.

    This rule is similar to the previous one, with the difference that it will also generate a fail outcome if no *.py file is detected.

    No files will be consumed because due to the ! the rule matches if no files match it, so the next rule will be processed with the same set of files.

  • +*.py [python] => pass Python detected

    This rule will generate a pass outcome if any *.py file is detected. The file will not be consumed and will be available for the next rule. The file will be tagged with a python file class.

  • !Main.java => fail Main.java not submitted

    This rule will generate a fail outcome if no Main.java file is detected.

  • !Library.java => pass WARNING: Expected Library.java file not submitted

    This will generate a warning message in the report if no Library.java file is detected.

  • */Main.java [main#source/Main.java] => pass-secret

  • **/Main.java [main#source/Main.java] => pass-secret

  • */**/Main.java [main#source/Main.java] => pass-secret

    These rules look for a Main.java file in one or more sub-directories. If it is found, it will be tagged with a main file class and renamed to source/Main.java. Also, the step will be marked as secret.

    The first version will only match if Main.java is in a single sub-directory, the second version will match if it is anywhere in the project, either the root or any sub-directory level deep, and the third version will match if it is in at least one sub-directory level deep.

  • * => fail Rejected {#} unexpected file{=|s}: {*}

    This “catch-all” rule is useful to use as the last rule in a set of rules. It will generate a fail outcome if any file is detected that was not matched and consumed by any of the previous rules.

    The error message will contain the number of unexpected files and a comma separated list of their names.