Justfile grammar is a little weird. Because of the freeform nature of recipe bodies, we don't tokenize them with the same rules as the rest of the justfile. Instead the tokenizer will emit a INDENT at the beginning of a recipe body, one or more LINEs, which match everything after the INDENT whitespace, and a DEDENT at the end. Thus the lexer is context sensitive, which is a little gross. tokens: NAME = /[a-z]((_|-)?[a-z0-9])*/ EOL = /\n|\r\n/ COMMENT = /#[^!].*/ COLON = /:/ INDENT = emitted when indentation increases DEDENT = emitted when indentation decreases LINE = /.*/ only emitted between INDENT/DEDENT pairs, doesn't include INDENT whitespace EOF = emitted at the end of input grammar: justfile = item* EOF item = COMMENT | recipe | EOL assignment = NAME EQUALS expression COMMENT? EOL expression = STRING recipe = NAME+ COLON NAME* EOL (INDENT LINE+ DEDENT)?