diff --git a/src/language/PugsParser.cpp b/src/language/PugsParser.cpp
index efddefd1a2081ee30fe26cb77cb16e95eb33c8d1..427e084e87ae89de498558fff98b7c246b1d4a68 100644
--- a/src/language/PugsParser.cpp
+++ b/src/language/PugsParser.cpp
@@ -32,18 +32,25 @@
 #include <unordered_map>
 #include <variant>
 
+const std::unique_ptr<ASTNode>* p_root_node = nullptr;
+
 void
 parser(const std::string& filename)
 {
   const size_t grammar_issues = analyze<language::grammar>();
 
-  std::cout << rang::fgB::yellow << "grammar_issues=" << rang::fg::reset << grammar_issues << '\n';
+  if (grammar_issues != 0) {
+    std::ostringstream os;
+    os << "invalid grammar: " << rang::fgB::yellow << grammar_issues << rang::fg::reset << " were detected!";
+    throw UnexpectedError(os.str());
+  }
 
   std::cout << rang::style::bold << "Parsing file " << rang::style::reset << rang::style::underline << filename
             << rang::style::reset << " ...\n";
 
   auto parse_and_execute = [](auto& input) {
     std::unique_ptr<ASTNode> root_node = ASTBuilder::build(input);
+    p_root_node                        = &root_node;
 
     ASTModulesImporter{*root_node};
     ASTNodeTypeCleaner<language::import_instruction>{*root_node};
@@ -64,35 +71,17 @@ parser(const std::string& filename)
     ASTNodeTypeCleaner<language::var_declaration>{*root_node};
     ASTNodeTypeCleaner<language::fct_declaration>{*root_node};
 
-    {
-      std::string dot_filename{"parse_tree.dot"};
-      std::ofstream fout(dot_filename);
-      ASTDotPrinter dot_printer{*root_node};
-      fout << dot_printer;
-      std::cout << "   AST dot file: " << dot_filename << '\n';
-    }
-
     ASTNodeEmptyBlockCleaner{*root_node};
 
     ASTNodeExpressionBuilder{*root_node};
-
-    std::cout << ASTPrinter{*root_node} << '\n';
-
-    auto& function_table = root_node->m_symbol_table->functionTable();
-
-    for (size_t i_function = 0; i_function < function_table.size(); ++i_function) {
-      const auto& function_descriptor = function_table[i_function];
-      std::cout << "function " << rang::fgB::magenta << function_descriptor.name() << rang::style::reset << '\n';
-      std::cout << ASTPrinter(function_descriptor.domainMappingNode());
-      std::cout << ASTPrinter(function_descriptor.definitionNode());
-      std::cout << "--------\n";
-    }
+    std::cout << "-------------------------------------------------------\n";
+    std::cout << rang::style::bold << "Executing AST..." << rang::style::reset << '\n';
 
     ExecutionPolicy exec_all;
     root_node->execute(exec_all);
-    std::cout << *(root_node->m_symbol_table) << '\n';
 
     root_node->m_symbol_table->clearValues();
+    p_root_node = nullptr;
   };
 
   if (not SignalManager::pauseOnError()) {
@@ -107,7 +96,7 @@ parser(const std::string& filename)
                 << rang::fgB::red << "error: " << rang::fg::reset << rang::style::bold << e.what() << rang::style::reset
                 << '\n'
                 << input.line_at(p) << '\n'
-                << std::string(p.column, ' ') << rang::fgB::yellow << '^' << rang::fg::reset << '\n';
+                << std::string(p.column - 1, ' ') << rang::fgB::yellow << '^' << rang::fg::reset << '\n';
       finalize();
       std::exit(1);
     }
@@ -115,5 +104,4 @@ parser(const std::string& filename)
     read_input input(filename);
     parse_and_execute(input);
   }
-  std::cout << "Executed successfuly: " << filename << '\n';
 }
diff --git a/src/language/ast/ASTNodeDataTypeBuilder.cpp b/src/language/ast/ASTNodeDataTypeBuilder.cpp
index 4a56bdae9cc854582e7e8df435ebbaea8e01a582..7cbeb6adfdc96f8c2a56796c32c83592f0839d40 100644
--- a/src/language/ast/ASTNodeDataTypeBuilder.cpp
+++ b/src/language/ast/ASTNodeDataTypeBuilder.cpp
@@ -16,8 +16,7 @@ ASTNodeDataTypeBuilder::_buildDeclarationNodeDataTypes(ASTNode& type_node, ASTNo
       std::ostringstream message;
       message << "number of product spaces (" << type_node.children.size() << ") " << rang::fgB::yellow
               << type_node.string() << rang::style::reset << rang::style::bold << " differs from number of variables ("
-              << name_node.children.size() << ") " << rang::fgB::yellow << name_node.string() << rang::style::reset
-              << std::ends;
+              << name_node.children.size() << ") " << rang::fgB::yellow << name_node.string() << rang::style::reset;
       throw ParseError(message.str(), name_node.begin());
     }
 
@@ -55,7 +54,7 @@ ASTNodeDataTypeBuilder::_buildDeclarationNodeDataTypes(ASTNode& type_node, ASTNo
         } else if (i_type_symbol->attributes().dataType() != ASTNodeDataType::type_name_id_t) {
           std::ostringstream os;
           os << "invalid type identifier, '" << type_name_id << "' was previously defined as a '"
-             << dataTypeName(i_type_symbol->attributes().dataType()) << "'" << std::ends;
+             << dataTypeName(i_type_symbol->attributes().dataType()) << '\'';
           throw ParseError(os.str(), std::vector{content_node->begin()});
         }
 
@@ -92,7 +91,7 @@ ASTNodeDataTypeBuilder::_buildDeclarationNodeDataTypes(ASTNode& type_node, ASTNo
       } else if (i_type_symbol->attributes().dataType() != ASTNodeDataType::type_name_id_t) {
         std::ostringstream os;
         os << "invalid type identifier, '" << type_name_id << "' was previously defined as a '"
-           << dataTypeName(i_type_symbol->attributes().dataType()) << "'" << std::ends;
+           << dataTypeName(i_type_symbol->attributes().dataType()) << '\'';
         throw ParseError(os.str(), std::vector{type_node.begin()});
       }
 
@@ -199,7 +198,7 @@ ASTNodeDataTypeBuilder::_buildNodeDataTypes(ASTNode& n) const
           message << "note: number of product spaces (" << nb_parameter_domains << ") " << rang::fgB::yellow
                   << parameters_domain_node.string() << rang::style::reset << rang::style::bold
                   << " differs from number of variables (" << nb_parameter_names << ") " << rang::fgB::yellow
-                  << parameters_name_node.string() << rang::style::reset << std::ends;
+                  << parameters_name_node.string() << rang::style::reset;
           throw ParseError(message.str(), parameters_domain_node.begin());
         }
 
@@ -247,8 +246,7 @@ ASTNodeDataTypeBuilder::_buildNodeDataTypes(ASTNode& n) const
             if (image_type.dimension() != nb_image_expressions) {
               std::ostringstream message;
               message << "expecting " << image_type.dimension() << " scalar expressions or an "
-                      << dataTypeName(image_type) << ", found " << nb_image_expressions << " scalar expressions"
-                      << std::ends;
+                      << dataTypeName(image_type) << ", found " << nb_image_expressions << " scalar expressions";
               throw ParseError(message.str(), image_domain_node.begin());
             }
           } else {
@@ -256,7 +254,7 @@ ASTNodeDataTypeBuilder::_buildNodeDataTypes(ASTNode& n) const
             message << "number of image spaces (" << nb_image_domains << ") " << rang::fgB::yellow
                     << image_domain_node.string() << rang::style::reset << rang::style::bold
                     << " differs from number of expressions (" << nb_image_expressions << ") " << rang::fgB::yellow
-                    << image_expression_node.string() << rang::style::reset << std::ends;
+                    << image_expression_node.string() << rang::style::reset;
             throw ParseError(message.str(), image_domain_node.begin());
           }
         }
@@ -396,7 +394,7 @@ ASTNodeDataTypeBuilder::_buildNodeDataTypes(ASTNode& n) const
         std::ostringstream message;
         message << "undefined binary operator\n"
                 << "note: incompatible operand types " << n.children[0]->string() << " (" << dataTypeName(type_0)
-                << ") and " << n.children[1]->string() << " (" << dataTypeName(type_1) << ')' << std::ends;
+                << ") and " << n.children[1]->string() << " (" << dataTypeName(type_1) << ')';
         throw ParseError(message.str(), n.begin());
       }
     } else if (n.is_type<language::function_evaluation>()) {
@@ -429,7 +427,7 @@ ASTNodeDataTypeBuilder::_buildNodeDataTypes(ASTNode& n) const
         std::ostringstream message;
         message << "invalid function call\n"
                 << "note: '" << n.children[0]->string() << "' (type: " << dataTypeName(n.children[0]->m_data_type)
-                << ") is not a function!" << std::ends;
+                << ") is not a function!";
         throw ParseError(message.str(), n.begin());
       }
     } else if (n.is_type<language::subscript_expression>()) {
@@ -442,7 +440,7 @@ ASTNodeDataTypeBuilder::_buildNodeDataTypes(ASTNode& n) const
         std::ostringstream message;
         message << "invalid types '" << rang::fgB::yellow << dataTypeName(array_expression.m_data_type)
                 << rang::style::reset << '[' << dataTypeName(index_expression.m_data_type) << ']'
-                << "' for array subscript" << std::ends;
+                << "' for array subscript";
 
         throw ParseError(message.str(), n.begin());
       } else {
diff --git a/src/language/ast/ASTSymbolInitializationChecker.cpp b/src/language/ast/ASTSymbolInitializationChecker.cpp
index 0e4d441fe590fb16534987bc1683ee2aadf0e98e..c37d4ba6bbe45563a71c876c5d1b70e992762729 100644
--- a/src/language/ast/ASTSymbolInitializationChecker.cpp
+++ b/src/language/ast/ASTSymbolInitializationChecker.cpp
@@ -22,7 +22,7 @@ ASTSymbolInitializationChecker::_checkSymbolInitialization(ASTNode& node)
       Assert(def_name_node.is_type<language::name>());
       if (decl_name_node.string() != def_name_node.string()) {
         std::ostringstream os;
-        os << "invalid identifier, expecting '" << decl_name_node.string() << "'" << std::ends;
+        os << "invalid identifier, expecting '" << decl_name_node.string() << '\'';
         throw ParseError(os.str(), std::vector{def_name_node.begin()});
       }
     };
@@ -45,7 +45,7 @@ ASTSymbolInitializationChecker::_checkSymbolInitialization(ASTNode& node)
         if (decl_name_list_node.children.size() != def_name_list_node.children.size()) {
           std::ostringstream os;
           os << "invalid number of definition identifiers, expecting " << decl_name_list_node.children.size()
-             << " found " << def_name_list_node.children.size() << std::ends;
+             << " found " << def_name_list_node.children.size();
           throw ParseError(os.str(), std::vector{def_name_list_node.begin()});
         }
 
diff --git a/src/language/modules/CMakeLists.txt b/src/language/modules/CMakeLists.txt
index 0a996fd6764b4d2eed57c02e8682283f1e640901..cd4c99bd8fb9d8e354706020a4077bf0c8234057 100644
--- a/src/language/modules/CMakeLists.txt
+++ b/src/language/modules/CMakeLists.txt
@@ -7,6 +7,7 @@ add_library(PugsLanguageModules
   MeshModule.cpp
   ModuleRepository.cpp
   SchemeModule.cpp
+  UtilsModule.cpp
   VTKModule.cpp
 )
 
diff --git a/src/language/modules/LinearSolverModule.cpp b/src/language/modules/LinearSolverModule.cpp
index 9e626ba2b0e4707df0495de2057a00d81ea4b118..a5bb2cf90058b9232bd84e2f2bbdd71eb02ef3c7 100644
--- a/src/language/modules/LinearSolverModule.cpp
+++ b/src/language/modules/LinearSolverModule.cpp
@@ -57,29 +57,36 @@ LinearSolverModule::LinearSolverModule()
 
                                               ));
 
-  this->_addBuiltinFunction("printLSOptions", std::make_shared<BuiltinFunctionEmbedder<void()>>(
+  this->_addBuiltinFunction("getLSOptions", std::make_shared<BuiltinFunctionEmbedder<std::string()>>(
+
+                                              []() -> std::string {
+                                                std::ostringstream os;
+                                                os << rang::fgB::yellow << "Linear solver options" << rang::style::reset
+                                                   << '\n';
+                                                os << LinearSolverOptions::default_options;
+                                                return os.str();
+                                              }
+
+                                              ));
+
+  this->_addBuiltinFunction("getLSAvailable", std::make_shared<BuiltinFunctionEmbedder<std::string()>>(
 
-                                                []() -> void {
-                                                  std::cout << rang::fgB::yellow << "Linear solver options"
-                                                            << rang::style::reset << '\n';
-                                                  std::cout << LinearSolverOptions::default_options;
+                                                []() -> std::string {
+                                                  std::ostringstream os;
+
+                                                  os << rang::fgB::yellow << "Available linear solver options"
+                                                     << rang::style::reset << '\n';
+                                                  os << rang::fgB::blue << " libraries" << rang::style::reset << '\n';
+
+                                                  printLSEnumListNames<LSLibrary>(os);
+                                                  os << rang::fgB::blue << " methods" << rang::style::reset << '\n';
+                                                  printLSEnumListNames<LSMethod>(os);
+                                                  os << rang::fgB::blue << " preconditioners" << rang::style::reset
+                                                     << '\n';
+                                                  printLSEnumListNames<LSPrecond>(os);
+
+                                                  return os.str();
                                                 }
 
                                                 ));
-
-  this->_addBuiltinFunction("printLSAvailable",
-                            std::make_shared<BuiltinFunctionEmbedder<void()>>(
-
-                              []() -> void {
-                                std::cout << rang::fgB::yellow << "Available linear solver options"
-                                          << rang::style::reset << '\n';
-                                std::cout << rang::fgB::blue << " libraries" << rang::style::reset << '\n';
-                                printLSEnumListNames<LSLibrary>(std::cout);
-                                std::cout << rang::fgB::blue << " methods" << rang::style::reset << '\n';
-                                printLSEnumListNames<LSMethod>(std::cout);
-                                std::cout << rang::fgB::blue << " preconditioners" << rang::style::reset << '\n';
-                                printLSEnumListNames<LSPrecond>(std::cout);
-                              }
-
-                              ));
 }
diff --git a/src/language/modules/ModuleRepository.cpp b/src/language/modules/ModuleRepository.cpp
index df403fa87f0c0d39778414a92ec7aa991b3d8d5b..83d44b23ee90423f495144216f5244a9b40db482 100644
--- a/src/language/modules/ModuleRepository.cpp
+++ b/src/language/modules/ModuleRepository.cpp
@@ -5,6 +5,7 @@
 #include <language/modules/MathModule.hpp>
 #include <language/modules/MeshModule.hpp>
 #include <language/modules/SchemeModule.hpp>
+#include <language/modules/UtilsModule.hpp>
 #include <language/modules/VTKModule.hpp>
 #include <language/utils/BuiltinFunctionEmbedder.hpp>
 #include <language/utils/ParseError.hpp>
@@ -20,11 +21,12 @@ ModuleRepository::_subscribe(std::unique_ptr<IModule> m)
 
 ModuleRepository::ModuleRepository()
 {
+  this->_subscribe(std::make_unique<LinearSolverModule>());
   this->_subscribe(std::make_unique<MathModule>());
   this->_subscribe(std::make_unique<MeshModule>());
-  this->_subscribe(std::make_unique<VTKModule>());
   this->_subscribe(std::make_unique<SchemeModule>());
-  this->_subscribe(std::make_unique<LinearSolverModule>());
+  this->_subscribe(std::make_unique<UtilsModule>());
+  this->_subscribe(std::make_unique<VTKModule>());
 }
 
 template <typename NameEmbedderMapT, typename EmbedderTableT>
diff --git a/src/language/modules/UtilsModule.cpp b/src/language/modules/UtilsModule.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..8c688e20768b5499549f88334deab4f2ac85a7a9
--- /dev/null
+++ b/src/language/modules/UtilsModule.cpp
@@ -0,0 +1,84 @@
+#include <language/modules/UtilsModule.hpp>
+
+#include <language/utils/ASTDotPrinter.hpp>
+#include <language/utils/ASTPrinter.hpp>
+#include <language/utils/BuiltinFunctionEmbedder.hpp>
+#include <language/utils/SymbolTable.hpp>
+#include <utils/PugsUtils.hpp>
+
+extern const std::unique_ptr<ASTNode>* p_root_node;
+
+UtilsModule::UtilsModule()
+{
+  this->_addBuiltinFunction("getPugsVersion", std::make_shared<BuiltinFunctionEmbedder<std::string(void)>>(
+
+                                                []() -> std::string { return pugsVersion(); }
+
+                                                ));
+
+  this->_addBuiltinFunction("getPugsBuildInfo", std::make_shared<BuiltinFunctionEmbedder<std::string(void)>>(
+
+                                                  []() -> std::string { return pugsBuildInfo(); }
+
+                                                  ));
+
+  this->_addBuiltinFunction("getAST", std::make_shared<BuiltinFunctionEmbedder<std::string(void)>>(
+
+                                        []() -> std::string {
+                                          Assert(p_root_node != nullptr, "unable to find AST's root node");
+
+                                          const auto& root_node = *p_root_node;
+                                          std::ostringstream os;
+                                          os << ASTPrinter{*root_node};
+
+                                          return os.str();
+                                        }
+
+                                        ));
+
+  this->_addBuiltinFunction("saveASTDot", std::make_shared<BuiltinFunctionEmbedder<void(const std::string&)>>(
+
+                                            [](const std::string& dot_filename) -> void {
+                                              Assert(p_root_node != nullptr, "unable to find AST's root node");
+
+                                              const auto& root_node = *p_root_node;
+
+                                              std::ofstream fout(dot_filename);
+
+                                              if (not fout) {
+                                                std::ostringstream os;
+                                                os << "could not create file '" << dot_filename << "'\n";
+                                                throw NormalError(os.str());
+                                              }
+
+                                              ASTDotPrinter dot_printer{*root_node};
+                                              fout << dot_printer;
+
+                                              if (not fout) {
+                                                std::ostringstream os;
+                                                os << "could not write AST to '" << dot_filename << "'\n";
+                                                throw NormalError(os.str());
+                                              }
+                                            }
+
+                                            ));
+
+  this->_addBuiltinFunction("getFunctionAST",
+                            std::make_shared<BuiltinFunctionEmbedder<std::string(const FunctionSymbolId&)>>(
+
+                              [](const FunctionSymbolId& function_symbol_id) -> std::string {
+                                auto& function_table = function_symbol_id.symbolTable().functionTable();
+
+                                const auto& function_descriptor = function_table[function_symbol_id.id()];
+
+                                std::ostringstream os;
+                                os << function_descriptor.name() << ": domain mapping\n";
+                                os << ASTPrinter(function_descriptor.domainMappingNode());
+                                os << function_descriptor.name() << ": definition\n";
+                                os << ASTPrinter(function_descriptor.definitionNode());
+
+                                return os.str();
+                              }
+
+                              ));
+}
diff --git a/src/language/modules/UtilsModule.hpp b/src/language/modules/UtilsModule.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..f7580a0422d3d8dabbd8c931b99dbfc7601f84da
--- /dev/null
+++ b/src/language/modules/UtilsModule.hpp
@@ -0,0 +1,19 @@
+#ifndef UTILS_MODULE_HPP
+#define UTILS_MODULE_HPP
+
+#include <language/modules/BuiltinModule.hpp>
+
+class UtilsModule : public BuiltinModule
+{
+ public:
+  std::string_view
+  name() const final
+  {
+    return "utils";
+  }
+
+  UtilsModule();
+  ~UtilsModule() = default;
+};
+
+#endif   // UTILS_MODULE_HPP
diff --git a/src/language/node_processor/AffectationProcessor.hpp b/src/language/node_processor/AffectationProcessor.hpp
index f3f8af44f38477007a0f85b2a3ee00738a5b82c9..6cdb19dfdd36359f6cd798149c7f2908770aadd0 100644
--- a/src/language/node_processor/AffectationProcessor.hpp
+++ b/src/language/node_processor/AffectationProcessor.hpp
@@ -105,7 +105,7 @@ class AffectationExecutor final : public IAffectationExecutor
               m_lhs = std::to_string(std::get<DataT>(rhs));
             } else {
               std::ostringstream os;
-              os << std::get<DataT>(rhs) << std::ends;
+              os << std::get<DataT>(rhs);
               m_lhs = os.str();
             }
           } else {
@@ -115,7 +115,7 @@ class AffectationExecutor final : public IAffectationExecutor
               m_lhs += std::to_string(std::get<DataT>(rhs));
             } else {
               std::ostringstream os;
-              os << std::get<DataT>(rhs) << std::ends;
+              os << std::get<DataT>(rhs);
               m_lhs += os.str();
             }
           }
@@ -418,7 +418,7 @@ class AffectationToTupleProcessor final : public INodeProcessor
             *m_lhs = std::vector{std::move(std::to_string(v))};
           } else {
             std::ostringstream os;
-            os << v << std::ends;
+            os << v;
             *m_lhs = std::vector<std::string>{os.str()};
           }
         } else if constexpr (std::is_same_v<ValueT, TinyVector<1>> and std::is_arithmetic_v<T>) {
@@ -475,7 +475,7 @@ class AffectationToTupleFromListProcessor final : public INodeProcessor
               tuple_value[i] = std::to_string(child_value);
             } else {
               std::ostringstream os;
-              os << child_value << std::ends;
+              os << child_value;
               tuple_value[i] = os.str();
             }
           } else if constexpr (is_tiny_vector_v<ValueT>) {
@@ -544,7 +544,7 @@ class AffectationToTupleFromListProcessor final : public INodeProcessor
       } else {
         for (size_t i = 0; i < values.size(); ++i) {
           std::ostringstream sout;
-          sout << values[i] << std::ends;
+          sout << values[i];
           v[i] = sout.str();
         }
       }
diff --git a/src/language/node_processor/FunctionArgumentConverter.hpp b/src/language/node_processor/FunctionArgumentConverter.hpp
index 9e7eec757fb2c396125d6d299f5a7529ca420009..e226e5336f4add578c5df2abe102121d067f6402 100644
--- a/src/language/node_processor/FunctionArgumentConverter.hpp
+++ b/src/language/node_processor/FunctionArgumentConverter.hpp
@@ -40,7 +40,7 @@ class FunctionArgumentToStringConverter final : public IFunctionArgumentConverte
           exec_policy.currentContext()[m_argument_id] = v;
         } else {
           std::ostringstream sout;
-          sout << value << std::ends;
+          sout << value;
           exec_policy.currentContext()[m_argument_id] = sout.str();
         }
       },
diff --git a/src/language/utils/PugsFunctionAdapter.hpp b/src/language/utils/PugsFunctionAdapter.hpp
index 4890664880c6e9bd3ce018f03db67c97b0587020..17d97f509c2466b97d3a8ee46cc34ca8ee6b06fe 100644
--- a/src/language/utils/PugsFunctionAdapter.hpp
+++ b/src/language/utils/PugsFunctionAdapter.hpp
@@ -110,7 +110,7 @@ class PugsFunctionAdapter<OutputType(InputType...)>
                     << _getInputDataTypeName() << " -> " << dataTypeName(ast_node_data_type_from<OutputType>)
                     << rang::style::reset << '\n'
                     << "note: provided function " << rang::fgB::magenta << function.name() << ": "
-                    << function.domainMappingNode().string() << rang::style::reset << std::ends;
+                    << function.domainMappingNode().string() << rang::style::reset;
       throw NormalError(error_message.str());
     }
   }
diff --git a/src/utils/BuildInfo.cpp b/src/utils/BuildInfo.cpp
index 7162aeccf69a7b5e6420e23ae7dca6cea6ced9b1..38d46007ef6cfcf470051e8546714db7041a7852 100644
--- a/src/utils/BuildInfo.cpp
+++ b/src/utils/BuildInfo.cpp
@@ -22,7 +22,7 @@ std::string
 BuildInfo::compiler()
 {
   std::stringstream compiler_info;
-  compiler_info << PUGS_BUILD_COMPILER << " (" << PUGS_BUILD_COMPILER_VERSION << ")" << std::ends;
+  compiler_info << PUGS_BUILD_COMPILER << " (" << PUGS_BUILD_COMPILER_VERSION << ")";
   return compiler_info.str();
 }
 
diff --git a/src/utils/ConsoleManager.cpp b/src/utils/ConsoleManager.cpp
index 666caa1b17c5bb397e592132febeabab4846732f..f55004ac7dd2181256404b486ad1eaa0c05abd85 100644
--- a/src/utils/ConsoleManager.cpp
+++ b/src/utils/ConsoleManager.cpp
@@ -11,12 +11,9 @@ ConsoleManager::isTerminal(std::ostream& os)
 void
 ConsoleManager::init(bool colorize)
 {
-  std::cout << "Console management: color ";
   if (colorize) {
     rang::setControlMode(rang::control::Force);
-    std::cout << rang::style::bold << rang::fgB::green << "enabled" << rang::fg::reset << rang::style::reset << '\n';
   } else {
     rang::setControlMode(rang::control::Off);
-    std::cout << "disabled\n";
   }
 }
diff --git a/src/utils/FPEManager.cpp b/src/utils/FPEManager.cpp
index 7430b5bbcc8c17d31b3a9508a170bfb3ed525632..89a75d7c77e8649b6710e32f6d89ed5e9f85c1c0 100644
--- a/src/utils/FPEManager.cpp
+++ b/src/utils/FPEManager.cpp
@@ -62,16 +62,12 @@ fedisableexcept(unsigned int excepts)
 void
 FPEManager::enable()
 {
-  std::cout << "FE management: " << rang::style::bold << rang::fgB::green << "enabled" << rang::fg::reset
-            << rang::style::reset << '\n';
   ::feenableexcept(MANAGED_FPE);
 }
 
 void
 FPEManager::disable()
 {
-  std::cout << "FE management: " << rang::style::bold << rang::fgB::red << "disabled" << rang::fg::reset
-            << rang::style::reset << '\n';
   ::fedisableexcept(MANAGED_FPE);
 }
 
@@ -79,15 +75,11 @@ FPEManager::disable()
 
 void
 FPEManager::enable()
-{
-  std::cout << "FE management: enabled " << rang::fg::red << "[not supported]" << rang::fg::reset << '\n';
-}
+{}
 
 void
 FPEManager::disable()
-{
-  std::cout << "FE management: disable " << rang::fg::red << "[not supported]" << rang::fg::reset << '\n';
-}
+{}
 
 #endif   // PUGS_HAS_FENV_H
 
diff --git a/src/utils/PugsUtils.cpp b/src/utils/PugsUtils.cpp
index d54bd333f93107a1e093038764ebe7f088ace9df..ad9e12e7ec4cdd01305cf8701c327894c7942304 100644
--- a/src/utils/PugsUtils.cpp
+++ b/src/utils/PugsUtils.cpp
@@ -1,15 +1,13 @@
 #include <utils/PugsUtils.hpp>
 
+#include <algebra/PETScWrapper.hpp>
 #include <utils/BuildInfo.hpp>
-#include <utils/RevisionInfo.hpp>
-
-#include <utils/Messenger.hpp>
-
 #include <utils/ConsoleManager.hpp>
 #include <utils/FPEManager.hpp>
+#include <utils/Messenger.hpp>
+#include <utils/RevisionInfo.hpp>
 #include <utils/SignalManager.hpp>
-
-#include <algebra/PETScWrapper.hpp>
+#include <utils/pugs_build_info.hpp>
 
 #include <rang.hpp>
 
@@ -19,43 +17,79 @@
 
 #include <iostream>
 
-// LCOV_EXCL_START
-
-// This function cannot be unit-tested: run once when pugs starts
-
 std::string
-initialize(int& argc, char* argv[])
+pugsVersion()
 {
-  parallel::Messenger::create(argc, argv);
+  std::stringstream os;
 
-  std::cout << "Pugs version: " << rang::style::bold << RevisionInfo::version() << rang::style::reset << '\n';
+  os << "pugs version: " << rang::style::bold << RevisionInfo::version() << rang::style::reset << '\n';
 
-  std::cout << "-------------------- " << rang::fg::green << "git info" << rang::fg::reset
-            << " -------------------------" << '\n';
-  std::cout << "tag:  " << rang::style::bold << RevisionInfo::gitTag() << rang::style::reset << '\n';
-  std::cout << "HEAD: " << rang::style::bold << RevisionInfo::gitHead() << rang::style::reset << '\n';
-  std::cout << "hash: " << rang::style::bold << RevisionInfo::gitHash() << rang::style::reset << "  (";
+  os << "-------------------- " << rang::fg::green << "git info" << rang::fg::reset << " -------------------------"
+     << '\n';
+  os << "tag:  " << rang::style::bold << RevisionInfo::gitTag() << rang::style::reset << '\n';
+  os << "HEAD: " << rang::style::bold << RevisionInfo::gitHead() << rang::style::reset << '\n';
+  os << "hash: " << rang::style::bold << RevisionInfo::gitHash() << rang::style::reset << "  (";
 
+  // LCOV_EXCL_START Cannot cover both situations at same time
   if (RevisionInfo::gitIsClean()) {
-    std::cout << rang::fgB::green << "clean" << rang::fg::reset;
+    os << rang::fgB::green << "clean" << rang::fg::reset;
   } else {
-    std::cout << rang::fgB::red << "dirty" << rang::fg::reset;
+    os << rang::fgB::red << "dirty" << rang::fg::reset;
   }
-  std::cout << ")\n";
-  std::cout << "-------------------- " << rang::fg::green << "build info" << rang::fg::reset
-            << " -----------------------" << '\n';
-  std::cout << "type:     " << rang::style::bold << BuildInfo::type() << rang::style::reset << '\n';
-  std::cout << "compiler: " << rang::style::bold << BuildInfo::compiler() << rang::style::reset << '\n';
-  std::cout << "kokkos:   " << rang::style::bold << BuildInfo::kokkosDevices() << rang::style::reset << '\n';
-  std::cout << "MPI:      " << rang::style::bold << BuildInfo::mpiLibrary() << rang::style::reset << '\n';
-  std::cout << "PETSc:    " << rang::style::bold << BuildInfo::petscLibrary() << rang::style::reset << '\n';
-  std::cout << "-------------------------------------------------------\n";
+  // LCOV_EXCL_STOP
+  os << ")\n";
+  os << "-------------------------------------------------------";
+
+  return os.str();
+}
+
+std::string
+pugsBuildInfo()
+{
+  std::ostringstream os;
+
+  os << "-------------------- " << rang::fg::green << "build info" << rang::fg::reset << " -----------------------"
+     << '\n';
+  os << "type:     " << rang::style::bold << BuildInfo::type() << rang::style::reset << '\n';
+  os << "compiler: " << rang::style::bold << BuildInfo::compiler() << rang::style::reset << '\n';
+  os << "kokkos:   " << rang::style::bold << BuildInfo::kokkosDevices() << rang::style::reset << '\n';
+  os << "MPI:      " << rang::style::bold << BuildInfo::mpiLibrary() << rang::style::reset << '\n';
+  os << "PETSc:    " << rang::style::bold << BuildInfo::petscLibrary() << rang::style::reset << '\n';
+  os << "-------------------------------------------------------";
+
+  return os.str();
+}
+
+void
+setDefaultOMPEnvironment()
+{
+  if constexpr (std::string_view{PUGS_BUILD_KOKKOS_DEVICES} == std::string_view{"OpenMP"}) {
+    setenv("OMP_PROC_BIND", "spread", 0);
+    setenv("OMP_PLACES", "threads", 0);
+  }
+}
+
+// LCOV_EXCL_START
+
+// This function cannot be unit-tested: run once when pugs starts
+
+std::string
+initialize(int& argc, char* argv[])
+{
+  parallel::Messenger::create(argc, argv);
 
   std::string filename;
   {
-    CLI::App app{"Pugs help"};
+    CLI::App app{"pugs help"};
+
+    app.add_option("filename", filename, "pugs script file")->check(CLI::ExistingFile)->required();
 
-    app.add_option("filename", filename, "pugs script file")->required()->check(CLI::ExistingFile);
+    app.set_version_flag("-v,--version", []() {
+      ConsoleManager::init(true);
+      std::stringstream os;
+      os << pugsVersion() << '\n' << pugsBuildInfo();
+      return os.str();
+    });
 
     int threads = -1;
     app.add_option("--threads", threads, "Number of Kokkos threads")
@@ -90,11 +124,17 @@ initialize(int& argc, char* argv[])
 
   PETScWrapper::initialize(argc, argv);
 
+  setDefaultOMPEnvironment();
   Kokkos::initialize(argc, argv);
-  std::cout << "-------------------- " << rang::fg::green << "exec info" << rang::fg::reset
-            << " ------------------------" << '\n';
+  std::cout << "----------------- " << rang::fg::green << "pugs exec info" << rang::fg::reset
+            << " ----------------------" << '\n';
 
   std::cout << rang::style::bold;
+#ifdef PUGS_HAS_MPI
+  std::cout << "MPI number of ranks " << parallel::size() << '\n';
+#else    // PUGS_HAS_MPI
+  std::cout << "Sequential build\n";
+#endif   // PUGS_HAS_MPI
   Kokkos::DefaultExecutionSpace::print_configuration(std::cout);
   std::cout << rang::style::reset;
   std::cout << "-------------------------------------------------------\n";
diff --git a/src/utils/PugsUtils.hpp b/src/utils/PugsUtils.hpp
index ef4eeb72c340b8a6ca654d001c32390973b63c55..7429a75b3be20ec3d62028cc9180a2017f7b4f15 100644
--- a/src/utils/PugsUtils.hpp
+++ b/src/utils/PugsUtils.hpp
@@ -21,6 +21,12 @@ parallel_reduce(size_t size, const ArrayType& array, ReturnType& value, const st
   Kokkos::parallel_reduce(label, size, array, value);
 }
 
+void setDefaultOMPEnvironment();
+
+std::string pugsBuildInfo();
+
+std::string pugsVersion();
+
 std::string initialize(int& argc, char* argv[]);
 
 void finalize();
diff --git a/src/utils/SignalManager.cpp b/src/utils/SignalManager.cpp
index ab08424fa2731bc62d84b461cea850878da9514b..399f3164c2046f4ec25a009160a6f01b11da85ef 100644
--- a/src/utils/SignalManager.cpp
+++ b/src/utils/SignalManager.cpp
@@ -114,11 +114,5 @@ SignalManager::init(bool enable)
     std::signal(SIGINT, SignalManager::handler);
     std::signal(SIGABRT, SignalManager::handler);
     std::signal(SIGPIPE, SignalManager::handler);
-
-    std::cout << "Signal management: " << rang::style::bold << rang::fgB::green << "enabled" << rang::fg::reset
-              << rang::style::reset << '\n';
-  } else {
-    std::cout << "Signal management: " << rang::style::bold << rang::fgB::red << "disabled" << rang::fg::reset
-              << rang::style::reset << '\n';
   }
 }
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index aaf4bfbad904a6466b4442b0d99f35a41840ddb3..0502c414a76e536dc6d2a0d1d49bc260a930b84d 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -80,8 +80,9 @@ add_executable (unit_tests
   test_NameProcessor.cpp
   test_OStreamProcessor.cpp
   test_ParseError.cpp
-  test_PugsFunctionAdapter.cpp
   test_PugsAssert.cpp
+  test_PugsFunctionAdapter.cpp
+  test_PugsUtils.cpp
   test_RevisionInfo.cpp
   test_SparseMatrixDescriptor.cpp
   test_SymbolTable.cpp
@@ -104,11 +105,11 @@ add_library(test_Pugs_MeshDataBase
 
 target_link_libraries (unit_tests
   test_Pugs_MeshDataBase
-  PugsLanguage
   PugsLanguageAST
   PugsLanguageModules
   PugsLanguageAlgorithms
   PugsLanguageUtils
+  PugsLanguage
   PugsMesh
   PugsAlgebra
   PugsUtils
diff --git a/tests/test_AffectationToStringProcessor.cpp b/tests/test_AffectationToStringProcessor.cpp
index 4053e023187944c87333f0dd605404104d039aa2..c432cdd00ddcd41213720feb9217ad4f20dfac5e 100644
--- a/tests/test_AffectationToStringProcessor.cpp
+++ b/tests/test_AffectationToStringProcessor.cpp
@@ -55,17 +55,17 @@ TEST_CASE("ASTAffectationToStringProcessor", "[language]")
     CHECK_AFFECTATION_RESULT(R"(let s : string; s = 2.3;)", "s", std::to_string(2.3));
     {
       std::ostringstream os;
-      os << TinyVector<1>{13} << std::ends;
+      os << TinyVector<1>{13};
       CHECK_AFFECTATION_RESULT(R"(let x : R^1, x = 13; let s : string; s = x;)", "s", os.str());
     }
     {
       std::ostringstream os;
-      os << TinyVector<2>{2, 3} << std::ends;
+      os << TinyVector<2>{2, 3};
       CHECK_AFFECTATION_RESULT(R"(let x : R^2, x = (2,3); let s : string; s = x;)", "s", os.str());
     }
     {
       std::ostringstream os;
-      os << TinyVector<3>{1, 2, 3} << std::ends;
+      os << TinyVector<3>{1, 2, 3};
       CHECK_AFFECTATION_RESULT(R"(let x : R^3, x = (1,2,3); let s : string; s = x;)", "s", os.str());
     }
   }
@@ -82,17 +82,17 @@ TEST_CASE("ASTAffectationToStringProcessor", "[language]")
                              (std::string("foo") + std::to_string(2.3)));
     {
       std::ostringstream os;
-      os << "foo" << TinyVector<1>{13} << std::ends;
+      os << "foo" << TinyVector<1>{13};
       CHECK_AFFECTATION_RESULT(R"(let x : R^1, x = 13; let s : string, s="foo"; s += x;)", "s", os.str());
     }
     {
       std::ostringstream os;
-      os << "foo" << TinyVector<2>{2, 3} << std::ends;
+      os << "foo" << TinyVector<2>{2, 3};
       CHECK_AFFECTATION_RESULT(R"(let x : R^2, x = (2,3); let s : string, s="foo"; s += x;)", "s", os.str());
     }
     {
       std::ostringstream os;
-      os << "foo" << TinyVector<3>{1, 2, 3} << std::ends;
+      os << "foo" << TinyVector<3>{1, 2, 3};
       CHECK_AFFECTATION_RESULT(R"(let x : R^3, x = (1,2,3); let s : string, s="foo"; s += x;)", "s", os.str());
     }
   }
diff --git a/tests/test_AffectationToTupleProcessor.cpp b/tests/test_AffectationToTupleProcessor.cpp
index 3720bc856b30fa39468e90a7a56cc0ce2d01058d..963e931bb0bba74e10063649896e0e11db109601 100644
--- a/tests/test_AffectationToTupleProcessor.cpp
+++ b/tests/test_AffectationToTupleProcessor.cpp
@@ -65,7 +65,7 @@ let s :(string); s = 2.;
 
     const std::string x_string = []() -> std::string {
       std::ostringstream os;
-      os << TinyVector<3, double>{1, 2, 3} << std::ends;
+      os << TinyVector<3, double>{1, 2, 3};
       return os.str();
     }();
 
@@ -101,7 +101,7 @@ let s :(string); s = (2.,3,"foo");
 
     const std::string x_string = []() -> std::string {
       std::ostringstream os;
-      os << TinyVector<2, double>{1, 2} << std::ends;
+      os << TinyVector<2, double>{1, 2};
       return os.str();
     }();
 
@@ -143,7 +143,7 @@ let t :(R^1); t = (x,2);
   {
     const std::string x_string = []() -> std::string {
       std::ostringstream os;
-      os << TinyVector<3, double>{1, 2, 3} << std::ends;
+      os << TinyVector<3, double>{1, 2, 3};
       return os.str();
     }();
 
diff --git a/tests/test_BuildInfo.cpp b/tests/test_BuildInfo.cpp
index 335cc74e420122f61a59067c0df89e416e55c2ba..b63d0eb9b22beab6d810b806284d6fc85f3c0df9 100644
--- a/tests/test_BuildInfo.cpp
+++ b/tests/test_BuildInfo.cpp
@@ -26,7 +26,7 @@ TEST_CASE("BuildInfo", "[utils]")
   SECTION("compiler")
   {
     std::stringstream compiler_info;
-    compiler_info << PUGS_BUILD_COMPILER << " (" << PUGS_BUILD_COMPILER_VERSION << ")" << std::ends;
+    compiler_info << PUGS_BUILD_COMPILER << " (" << PUGS_BUILD_COMPILER_VERSION << ")";
     REQUIRE(BuildInfo::compiler() == compiler_info.str());
   }
 
diff --git a/tests/test_FunctionArgumentConverter.cpp b/tests/test_FunctionArgumentConverter.cpp
index e3d2ee0939c306fb971274c782268b161125bee6..dfa637bf735c66e02dab8c4345ccbf74e7a1e069 100644
--- a/tests/test_FunctionArgumentConverter.cpp
+++ b/tests/test_FunctionArgumentConverter.cpp
@@ -20,7 +20,7 @@ TEST_CASE("FunctionArgumentConverter", "[language]")
     FunctionArgumentToStringConverter converter1{1};
     converter1.convert(execution_policy, X);
     std::ostringstream os_X;
-    os_X << X << std::ends;
+    os_X << X;
 
     const double x = 3.2;
     FunctionArgumentToStringConverter converter2{2};
diff --git a/tests/test_ListAffectationProcessor.cpp b/tests/test_ListAffectationProcessor.cpp
index e0f17e7f593f98a15457686e9e38b0bccfa34938..e2a0919952220b6ba6a2c723a31604674254357d 100644
--- a/tests/test_ListAffectationProcessor.cpp
+++ b/tests/test_ListAffectationProcessor.cpp
@@ -89,19 +89,19 @@ TEST_CASE("ListAffectationProcessor", "[language]")
                                std::to_string(double{3}));
       {
         std::ostringstream os;
-        os << TinyVector<1>{7} << std::ends;
+        os << TinyVector<1>{7};
         CHECK_AFFECTATION_RESULT(R"(let v:R^1, v = 7; let  (x,u,s):R*R^2*string, (x,u,s) = (1.2, (2,3), v);)", "s",
                                  os.str());
       }
       {
         std::ostringstream os;
-        os << TinyVector<2>{6, 3} << std::ends;
+        os << TinyVector<2>{6, 3};
         CHECK_AFFECTATION_RESULT(R"(let v: R^2, v = (6,3); let (x,u,s):R*R^2*string, (x,u,s) = (1.2, (2,3), v);)", "s",
                                  os.str());
       }
       {
         std::ostringstream os;
-        os << TinyVector<3>{1, 2, 3} << std::ends;
+        os << TinyVector<3>{1, 2, 3};
         CHECK_AFFECTATION_RESULT(R"(let v:R^3, v = (1,2,3); let (x,u,s):R*R^2*string, (x,u,s) = (1.2, (2,3), v);)", "s",
                                  os.str());
       }
diff --git a/tests/test_PugsUtils.cpp b/tests/test_PugsUtils.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..b966ae8f0c0bab490c9f7e3e4441ffdd329aeec1
--- /dev/null
+++ b/tests/test_PugsUtils.cpp
@@ -0,0 +1,110 @@
+#include <catch2/catch.hpp>
+
+#include <utils/BuildInfo.hpp>
+#include <utils/PugsUtils.hpp>
+#include <utils/RevisionInfo.hpp>
+#include <utils/pugs_build_info.hpp>
+
+#include <rang.hpp>
+
+#include <string>
+
+// clazy:excludeall=non-pod-global-static
+
+TEST_CASE("PugsUtils", "[utils]")
+{
+  SECTION("checking infos")
+  {
+    const std::string pugs_version = [] {
+      std::stringstream os;
+
+      os << "pugs version: " << rang::style::bold << RevisionInfo::version() << rang::style::reset << '\n';
+
+      os << "-------------------- " << rang::fg::green << "git info" << rang::fg::reset << " -------------------------"
+         << '\n';
+      os << "tag:  " << rang::style::bold << RevisionInfo::gitTag() << rang::style::reset << '\n';
+      os << "HEAD: " << rang::style::bold << RevisionInfo::gitHead() << rang::style::reset << '\n';
+      os << "hash: " << rang::style::bold << RevisionInfo::gitHash() << rang::style::reset << "  (";
+
+      if (RevisionInfo::gitIsClean()) {
+        os << rang::fgB::green << "clean" << rang::fg::reset;
+      } else {
+        os << rang::fgB::red << "dirty" << rang::fg::reset;
+      }
+      os << ")\n";
+      os << "-------------------------------------------------------";
+
+      return os.str();
+    }();
+
+    REQUIRE(pugsVersion() == pugs_version);
+
+    const std::string build_info = [] {
+      std::ostringstream os;
+
+      os << "-------------------- " << rang::fg::green << "build info" << rang::fg::reset << " -----------------------"
+         << '\n';
+      os << "type:     " << rang::style::bold << BuildInfo::type() << rang::style::reset << '\n';
+      os << "compiler: " << rang::style::bold << BuildInfo::compiler() << rang::style::reset << '\n';
+      os << "kokkos:   " << rang::style::bold << BuildInfo::kokkosDevices() << rang::style::reset << '\n';
+      os << "MPI:      " << rang::style::bold << BuildInfo::mpiLibrary() << rang::style::reset << '\n';
+      os << "PETSc:    " << rang::style::bold << BuildInfo::petscLibrary() << rang::style::reset << '\n';
+      os << "-------------------------------------------------------";
+
+      return os.str();
+    }();
+
+    REQUIRE(pugsBuildInfo() == build_info);
+  }
+
+  SECTION("checking OMP environment setting")
+  {
+    if constexpr (std::string_view{PUGS_BUILD_KOKKOS_DEVICES} == std::string_view{"OpenMP"}) {
+      const std::string saved_omp_proc_bind = []() {
+        char* value = getenv("OMP_PROC_BIND");
+        if (value != nullptr) {
+          return std::string{value};
+        } else {
+          return std::string{};
+        }
+      }();
+
+      const std::string saved_omp_places = []() {
+        char* value = getenv("OMP_PLACES");
+        if (value != nullptr) {
+          return std::string{value};
+        } else {
+          return std::string{};
+        }
+      }();
+
+      unsetenv("OMP_PROC_BIND");
+      unsetenv("OMP_PLACES");
+
+      setDefaultOMPEnvironment();
+      REQUIRE(std::string{getenv("OMP_PROC_BIND")} == std::string{"spread"});
+      REQUIRE(std::string{getenv("OMP_PLACES")} == std::string{"threads"});
+
+      unsetenv("OMP_PROC_BIND");
+      unsetenv("OMP_PLACES");
+
+      setenv("OMP_PROC_BIND", "foo", 1);
+      setenv("OMP_PLACES", "bar", 1);
+
+      setDefaultOMPEnvironment();
+      REQUIRE(std::string{getenv("OMP_PROC_BIND")} == std::string{"foo"});
+      REQUIRE(std::string{getenv("OMP_PLACES")} == std::string{"bar"});
+
+      unsetenv("OMP_PROC_BIND");
+      unsetenv("OMP_PLACES");
+
+      if (saved_omp_proc_bind.size() != 0) {
+        setenv("OMP_PROC_BIND", saved_omp_proc_bind.c_str(), 1);
+      }
+
+      if (saved_omp_places.size() != 0) {
+        setenv("OMP_PLACES", saved_omp_places.c_str(), 1);
+      }
+    }
+  }
+}