MontiCore Best Practices - Designing Tools for Command Line Interfaces
Some DSLs require a tool to enable general accessibility via the command line interface (CLI). When designing a tool, we recommend some standard guidelines.
Designing a Tool
The tool provides a general interface for the functionalities developed for a language. This includes all features such as parsing of models, saving and loading of symbol tables, pretty printing, reporting, or export as object diagram.
Default Options
The available options are of course language-specific. However, we suggest some default arguments for standardized access.
-h,--help Prints this help dialog
-i,--input <file> Reads the (mandatory) source file resp. the
contents of the model
-path <dirlist> Sets the artifact path for imported symbols, space separated
-modelpath <dirlist> Sets the artifact path for imported models, space separated
-pp,--prettyprint <file> Prints the AST to stdout or the specified output
file (optional)
-s, --symboltable <file> Serializes and prints the symbol table to stdout
or the specified output file (optional)
-r,--report <dir> Prints reports of the parsed artifact to the
specified directory (optional). Available reports
are language-specific
-o,--output <dir> Path of generated files (optional)
-so,--syntaxobjects <file> Prints an object diagram of the AST to stdout or
the specified file (optional)
-sc,--script <file> Advanced configuration 2: through a groovy script
that allows to adapt and extend the tool workflow (optional)
(only some tools provide groovy scripting)
-ct, --configtemplate Advanced configuration 1: through a Freemarker template
that allows to adapt the generation process (optional)
(only some tools provide a template configuration call)
An example of a complete yet relatively small tool example can be found in the JSON project.
Some explanation to the arguments:
* The tool is meant for handling one individual model (-i
) and store the
results appropriately in files.
* Typical results are
* (1) generated files (-o
) that are used in the next step of
the build process (e.g. for compilation).
* (2) the symboltable (-s
) that is then used by other tools to import symbols
* (3) reports (-r
) and internal information (-so
), like the AST of the
parsed model usable for developers to understand what happened
* (4) and potentially also internal information on used input and generated
output files
that allows the calling build script to understand whether a redo is
needed (as part of a
larger incremental and efficient development process).
* Directories in -path
are separated via spaces, i.e. each path is an argument on its own.
Example: -path a/b x/y
.
* Directories in the above options -path
, -o
describe the root
structure that is further refined by packages (like in Java).
That means with -path a/b x/y
the actual symboltable for a Statechart de.mine.Door
is found in
a/b/de/mine/Door.scsym
or x/y/de/mine/Door.scsym
(in that order)
* Languages typically only load other symbols rather than other models. Therefore, the argument
-path
that identifies only paths containing symbols should be implemented by most languages, whereas
the argument -modelpath
for identifying paths containing models is typically not required.
* Groovy-scripting (-sc
, --script
): A Groovy Script is meant to describe the tool internal
workflow. It controls parsing, symbol construction, reporting, code generation etc.
This kind of scripting should only become necessary when various alternative
configurations are possible. Thus, not every tool provides Groovy scripting.
* Template-scripting (-ct
, --configtemplate
):
It is possible to add a custom template script right before
the full generation process starts. This template is useful to customize the
generation process e.g. by defining hook points and thus injection more templates
or switching verbosity on/off.
Usage of the Tool-JAR
A note to the tool usage: Tools do not organize the correct order of their calls. If embedded in a larger build process, an appropriate gradle (preferred) or make it is useful for incremental efficiency.
This organisation is above the tool, due to the efficiency of the (grade or make) buildscript itself, which must be able to decide, whether a redo is needed. If the tool was called to decide that, too much time was already wasted.
For a build script to decide whether to call the tool or not, a tool call should (and actually MontiCore does) provide among others a list of files it used for input.
Automatically Generating a Tool-JAR
Note to the tool development: To automatically derive an executable JAR from the Gradle build process for the corresponding tool, the following template can be used.
// all in one tool-jar
shadowJar {
manifest {
attributes "Main-Class": "de.monticore.${archiveBaseName.get().capitalize()}Tool"
}
archiveFileName = "MC${archiveBaseName.get()}.${archiveExtension.get()}"
minimize()
archiveClassifier = "mc-tool"
}
jar.dependsOn shadowJar
build.gradle
script to derive a JAR for the tool
class and its provided command line functionalities.
The packed JAR already contains all the necessary dependencies.
The template defines the main class and name of the JAR.
To foster automated reuse, the template has already been configured to generate
a suitable JAR for each language project without manual adjustments.
However, this requires adhering to the following conventions:
* The name of the main class is equal to the language project name (usually defined
in the settings.gradle
) with the suffix Tool.
Furthermore, the first letter of the main class is always capitalized to adhere
to the Java code conventions
* The package of the main class is de.monticore
* The generated JAR can be found in 'target/libs'
Example:
For a language project MyLang
we have to implement the MyLangTool.java
located
in the package de.monticore
.
This automatically generates the executable JAR MCMyLang.jar
In general, the template can be customized by specifying the corresponding main class and JAR name definitions. However, we recommend to use the predefined automatic approach.
Functional Approach
When implementing the tool, we recommend a functional paradigm to provide the desired functionalities, as the too class is not about data structures but only exists to make functions available. In this case it would be counterproductive to store the arguments of the available functions as attributes. Instead, it makes more sense to pass these arguments as parameters when calling the respective methods. This yields several advantages:
- Values that have not yet been set do not have to be displayed with Optionals
- As a result. tedious unwrapping of Optionals with corresponding error messages is no longer necessary
- get/set methods for attributes are not required
- Facilitates reusability of modular functions
Of course, there are always trade-offs, but a more explicit functional way of thinking should be considered more intensively, especially when it is not about data structures but about the functions. For instance, if intermediate results are stored for efficiency reasons, this might a good argument to do it differently.