-
Notifications
You must be signed in to change notification settings - Fork 128
Add ScriptReader for script template parsing #343
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
23 commits
Select commit
Hold shift + click to select a range
d17a43b
Use a separate class to read script code from a file
urfeex 4e96383
WIP: Add parsing include statements
urfeex 3b387b5
Use replacement dictionary for replacing variables
urfeex bef8761
Add parsing of simple conditionals
urfeex a552f62
Implemented parsing conditional expressions
urfeex a00ede6
Add a test for conditionally including a file
urfeex 3b14693
WIP: Support VersionInformation in ScriptDreader DataVariant
urfeex f520cdf
Doxygen documentation of code
urfeex 0a6bfa6
Add sphinx documentation and an example for the ScriptReader
urfeex 72a9eab
Make sure sent script has an empty line
urfeex dc1c17e
Add missing algorithm include
urfeex e6f99c9
Explicitly convert match to string
urfeex 8ba6e2b
More windows fixes
urfeex de3e099
Add missing test
urfeex 33adccb
Add test for VersionInformation::toString()
urfeex af08325
Add changes from code review
urfeex 76f8d01
Add documentation about variable substitutions
urfeex 5fdc8d0
Apply suggestions from code review
urfeex c1c5e3a
Fix parsing elifs
urfeex 97f4d26
Re-sort variable declaration in tests
urfeex 1d04975
Revert and deprecate changing the UrDriver's readScriptFile method
urfeex b366542
Add a time frame to deprecation notice
urfeex fc04b1f
Also test yes/no in parseBoolean test
urfeex File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,156 @@ | ||
:github_url: https://github.com/UniversalRobots/Universal_Robots_Client_Library/blob/master/doc/architecture/script_reader.rst | ||
|
||
.. _script_reader: | ||
|
||
ScriptReader | ||
============ | ||
|
||
Script code used by the :ref:`script_sender` is read from a file. That script code might have to be | ||
dynamically modified based on some configuration input. For example, if the script code contains | ||
connections to a remote PC, that PC's IP address might be configured by the user. Another example | ||
would be to include certain parts of the script code only if the robot's software version supports | ||
that. | ||
|
||
For that purpose the ``ScriptReader`` class is provided. It reads the script code from a file and | ||
performs the following substitutions: | ||
|
||
- Replaces variables in the form of ``{{variable_name}}`` with the value of the variable | ||
from a provided dictionary. | ||
- Includes other script files using the directive ``{% include file_name %}``. The included file is | ||
read from the same directory as the main script file. Nested includes are also possible. | ||
- Use conditionals in order to add certain parts of the script code only if a condition matches. | ||
|
||
The supported substitutions use a basic implementation of the `Jinja2 templating engine` syntax. | ||
|
||
.. note:: | ||
One special literal is defined for **version information**. Use a software version prefixed with a | ||
``v`` character, e.g. ``v10.8.0`` to encode a software version. Version information entries can be | ||
compared with each other. | ||
|
||
**Do not** wrap version information into quotes, as this will be interpreted as a string. | ||
|
||
Example | ||
------- | ||
|
||
Given two script files: | ||
|
||
.. literalinclude:: ../../tests/resources/example_urscript_main.urscript | ||
:caption: tests/resources/example_urscript_main.urscript | ||
:linenos: | ||
:lineno-match: | ||
|
||
.. literalinclude:: ../../tests/resources/example_urscript_feature.urscript | ||
:language: python | ||
:caption: tests/resources/example_urscript_feature.urscript | ||
:linenos: | ||
:lineno-match: | ||
|
||
The dictionary entry for ``feature_name`` is "torque control". | ||
|
||
Depending on the ``SOFTWARE_VERSION`` entry in the dictionary passed to the | ||
``ScriptReader``, the script code will be read as follows: | ||
|
||
Given ``SOFTWARE_VERSION = v5.21.0``, the script code will be: | ||
|
||
.. code-block:: python | ||
|
||
popup("The cool new feature is not supported on Software version 5.23.0") | ||
|
||
|
||
Given ``SOFTWARE_VERSION = v5.23.0``, the script code will be: | ||
|
||
.. code-block:: python | ||
|
||
textmsg("torque control is a very cool feature!") | ||
|
||
Supported Data | ||
-------------- | ||
|
||
Data dictionary (C++ side) | ||
~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||
|
||
The data dictionary supports the following types | ||
|
||
- ``str``: A string value, e.g. "Hello World" | ||
- ``int``: An integer value, e.g. 42 | ||
- ``double``: A floating point value, e.g. 3.14 | ||
- ``bool``: A boolean value, e.g. ``true`` or ``false`` | ||
- ``VersionInformation``: A version information value, e.g. ``VersionInformation::fromString("10.8.0")`` | ||
|
||
Script code side | ||
~~~~~~~~~~~~~~~~ | ||
|
||
Variable replacements | ||
^^^^^^^^^^^^^^^^^^^^^ | ||
|
||
Variable replacements in the script code are done using the syntax ``{{ variable_name }}``. For | ||
this to work, the variable ``variable_name`` has to be defined in the data dictionary passed to the | ||
``ScriptReader``. | ||
|
||
The expression ``{{ variable_name }}`` will be replaced with the string representation of the | ||
variable's content. | ||
|
||
- If the variable is a string, it has to be wrapped into quotes in the script | ||
code. e.g. | ||
|
||
.. code-block:: python | ||
|
||
textmsg("{{ log_message }}") | ||
|
||
|
||
- Boolean variables will be replaced with the string ``True`` or ``False``. | ||
- Numeric variables (integer and floating point) will be replaced with the string representation generated by | ||
`std::to_string() <https://en.cppreference.com/w/cpp/string/basic_string/to_string>`_. | ||
- Version information variables will be replaced with the string representation similar to | ||
``10.7.0.0`` | ||
|
||
Boolean expressions | ||
^^^^^^^^^^^^^^^^^^^ | ||
|
||
Boolean expressions have to follow one of two possible syntax variations | ||
|
||
- Direct evaluation of a boolean variable from the data dictionary: | ||
|
||
.. code-block:: | ||
|
||
boolean_variable_name | ||
|
||
- Comparison of a variable with a value using an operator. | ||
|
||
.. code-block:: | ||
|
||
variable_name operator value | ||
|
||
The operator has to be one of ``==``, ``!=``, ``<``, ``<=``, ``>``, ``>=``. On the lefthand side | ||
of the operator there has to be a variable from the data dictionary. The right hand side can be | ||
either a variable name or a value. If the right hand side is a variable name, it has to be | ||
defined in the data dictionary as well. Values will be parsed as follows: | ||
|
||
- Strings: Wrapped in quotes, e.g. ``"Hello World"`` or ``'Universal Robots'``. | ||
- Numerical values such as ``42``, ``3.14``, ``-1``, ``1e-12``. | ||
- Boolean values: See below. | ||
- Version information: Prefixed with a ``v`` character, e.g. ``v10.8.0``, ``v5.23.0``. | ||
|
||
Boolean values parsing | ||
^^^^^^^^^^^^^^^^^^^^^^ | ||
|
||
Boolean values can be parsed from the following strings: | ||
|
||
- ``true``, ``True``, ``TRUE`` | ||
- ``on``, ``On``, ``ON`` | ||
- ``yes``, ``Yes``, ``YES`` | ||
- ``1`` | ||
- ``false``, ``False``, ``FALSE`` | ||
- ``off``, ``Off``, ``OFF`` | ||
- ``no``, ``No``, ``NO`` | ||
- ``0`` | ||
|
||
Conditional blocks | ||
^^^^^^^^^^^^^^^^^^ | ||
|
||
Conditional blocks have to be started with a ``{% if condition %}`` directive and closed with a | ||
``{% endif %}`` directive. The condition can be any boolean expression as described above. | ||
|
||
The ``{% elif condition %}`` and ``{% else %}`` directives can be used to add alternative paths. | ||
|
||
Conditional blocks can be nested. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,128 @@ | ||
// -- BEGIN LICENSE BLOCK ---------------------------------------------- | ||
// Copyright 2025 Universal Robots A/S | ||
// | ||
// Redistribution and use in source and binary forms, with or without | ||
// modification, are permitted provided that the following conditions are met: | ||
// | ||
// * Redistributions of source code must retain the above copyright | ||
// notice, this list of conditions and the following disclaimer. | ||
// | ||
// * Redistributions in binary form must reproduce the above copyright | ||
// notice, this list of conditions and the following disclaimer in the | ||
// documentation and/or other materials provided with the distribution. | ||
// | ||
// * Neither the name of the {copyright_holder} nor the names of its | ||
// contributors may be used to endorse or promote products derived from | ||
// this software without specific prior written permission. | ||
// | ||
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" | ||
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | ||
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE | ||
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE | ||
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR | ||
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF | ||
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS | ||
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN | ||
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) | ||
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE | ||
// POSSIBILITY OF SUCH DAMAGE. | ||
// -- END LICENSE BLOCK ------------------------------------------------ | ||
|
||
#pragma once | ||
#include <filesystem> | ||
#include <string> | ||
#include <unordered_map> | ||
#include <variant> | ||
|
||
#include <ur_client_library/ur/datatypes.h> | ||
#include <ur_client_library/ur/version_information.h> | ||
|
||
namespace urcl | ||
{ | ||
namespace control | ||
{ | ||
/*! | ||
* \brief This class handles reading script files parsing special instructions that will get replaced. | ||
* | ||
* When parsing the script code, it is supported to have | ||
* - Variable replacements using `{{ VARIABLE_NAME }}` | ||
* - Including other files using `{% include <filename> %}`. | ||
* The filename has to be relative to the root script file's folder | ||
* - Conditionals using | ||
* | ||
* {% if <condition %} | ||
* ... | ||
* {% elif <condition> %} | ||
* ... | ||
* {% else %} | ||
* ... | ||
* {% endif %} | ||
* | ||
* | ||
* Those directives use Jinja2 notation. | ||
*/ | ||
class ScriptReader | ||
{ | ||
public: | ||
using DataVariant = std::variant<std::string, double, int, bool, VersionInformation>; | ||
using DataDict = std::unordered_map<std::string, DataVariant>; | ||
|
||
ScriptReader() = default; | ||
|
||
/*! | ||
* \brief Reads a script file and applies variable replacements, includes, and conditionals. | ||
* \param file_path Path of the script file to be loaded. | ||
* \param data Data dictionary used for variable replacements and expression evaluation. | ||
* \return The Script code with all replacements, includes and conditionals applied. | ||
*/ | ||
std::string readScriptFile(const std::string& file_path, const DataDict& data = DataDict()); | ||
|
||
/*! | ||
* \brief Evaluate a boolean expression | ||
* \param expression The boolean expression to be evaluated. | ||
* \param data A data dictionary that will be used when evaluating the expressions | ||
* \return The result of evaluating the boolean expression | ||
*/ | ||
static bool evaluateExpression(const std::string& expression, const DataDict& data); | ||
|
||
private: | ||
enum BlockType | ||
{ | ||
IF, | ||
ELIF, | ||
ELSE | ||
}; | ||
struct BlockState | ||
{ | ||
BlockType type; | ||
bool condition_matched; // Has any previous condition in this block matched? | ||
bool should_render; // Should this block render? | ||
bool parent_render; // Is the parent block rendering? | ||
}; | ||
|
||
std::filesystem::path script_path_; | ||
|
||
static std::string readFileContent(const std::string& file_path); | ||
void replaceIncludes(std::string& script_code, const DataDict& data); | ||
static void replaceVariables(std::string& script_code, const DataDict& data); | ||
static void replaceConditionals(std::string& script_code, const DataDict& data); | ||
}; | ||
|
||
bool operator<(const ScriptReader::DataVariant& lhs, const ScriptReader::DataVariant& rhs); | ||
bool operator>(const ScriptReader::DataVariant& lhs, const ScriptReader::DataVariant& rhs); | ||
bool operator==(const ScriptReader::DataVariant& lhs, const ScriptReader::DataVariant& rhs); | ||
|
||
inline bool operator!=(const ScriptReader::DataVariant& lhs, const ScriptReader::DataVariant& rhs) | ||
{ | ||
return !(lhs == rhs); | ||
} | ||
inline bool operator<=(const ScriptReader::DataVariant& lhs, const ScriptReader::DataVariant& rhs) | ||
{ | ||
return (lhs < rhs || lhs == rhs); | ||
} | ||
inline bool operator>=(const ScriptReader::DataVariant& lhs, const ScriptReader::DataVariant& rhs) | ||
{ | ||
return (lhs > rhs || lhs == rhs); | ||
} | ||
} // namespace control | ||
} // namespace urcl |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.