CLI for Incremental Batch Builds
We have tasks for compiling grammars and parsing files, but we need to pass file paths and a rule name into these tasks.
We will pass this data to the program via command-line arguments.
To parse command-line arguments, we will use clap, which is an awesome library for easily parsing command-line arguments.
Add clap as a dependency to pie/Cargo.toml
:
We’re using the derive
feature of clap to automatically derive a full-featured argument parser from a struct.
Modify pie/examples/parser_dev/main.rs
:
The Args
struct contains exactly the data we need: the path to the grammar file, the name of the rule to start parsing with, and paths to program files to parse.
We derive an argument parser for Args
with #[derive(Parser)]
.
Then we parse command-line arguments in main
with Args::parse()
.
Test this program with cargo run --example parser_dev -- --help
, which should result in usage help for the program.
Note that the names, ordering, and doc-comments of the fields are used to generate this help.
You can test out several more commands:
cargo run --example parser_dev --
cargo run --example parser_dev -- foo
cargo run --example parser_dev -- foo bar
cargo run --example parser_dev -- foo bar baz qux
Now let’s use these arguments to actually compile the grammar and parse example program files.
Modify pie/examples/parser_dev/main.rs
:
In compile_grammar_and_parse
, we create a new Pie
instance that writes the build log to stderr, and create a new build session.
Then, we require a compile grammar task using the grammar_file_path
from Args
, and write any errors to the errors
String
.
We then require a parse task for every path in args.program_file_paths
, using the previously created compile_grammar_task
and args.rule_name
.
Successes are printed to stdout and errors are written to errors
.
Finally, we print errors
to stdout if there are any.
To test this out, we need a grammar and some test files. Create grammar.pest
:
num = @{ ASCII_DIGIT+ }
main = { SOI ~ num ~ EOI }
WHITESPACE = _{ " " | "\t" | "\n" | "\r" }
You don’t need to fully understand pest grammars to finish this example. However, I will explain the basics of this grammar here. Feel free to learn and experiment more if you are interested.
Grammars are lists of rules, such as num
and main
.
This grammar parses numbers with the num
rule, matching 1 or more ASCII_DIGIT
with repetition.
The main
rule ensures that there is no additional text before and after a num
rule, using
SOI
(start of input) EOI
(end of input), and using the
~
operator to sequence these rules.
We set the
WHITESPACE
builtin rule to { " " | "\t" | "\n" | "\r" }
so that spaces, tabs, newlines, and carriage return characters are implicitly allowed between sequenced rules.
The @
operator before {
indicates that it is an atomic rule, disallowing implicit whitespace.
We want this on the num
rule so that we can’t add spaces in between digits of a number (try removing it and see!)
The _
operator before {
indicates that it is a silent rule that does not contribute to the parse result.
This is important when processing the parse result into an Abstract Syntax Tree (AST).
In this example we just print the parse result, so silent rules are not really needed, but I included it for completeness.
Create test_1.txt
with:
42
And create test_2.txt
with:
foo
Run the program with cargo run --example parser_dev -- grammar.pest main test_1.txt test_2.txt
.
This should result in a build log showing that the grammar is successfully compiled, that one file is successfully parsed, and that one file has a parse error.
Unfortunately, there is no incrementality between different runs of the example, because the Pie
Store
is not persisted.
The Store
only exists in-memory while the program is running, and is then thrown away.
Thus, there cannot be any incrementality.
To get incrementality, we need to serialize the Store
before the program exits, and deserialize it when the program starts.
This is possible and not actually that hard, I just never got around to explaining it in this tutorial.
See the Side Note: Serialization section at the end for info on how this can be implemented.
If you are using a bash-like shell on a UNIX-like OS, you can hide the build log by redirecting stderr to /dev/null
with: cargo run --example parser_dev -- grammar.pest main test_1.txt test_2.txt 2>/dev/null
.
Otherwise, you can hide the build log by replacing WritingTracker::with_stderr()
with NoopTracker
.
Feel free to experiment a bit with the grammar, example files, etc. before continuing. We will develop an interactive editor next however, which will make experimentation easier!