preprocessor/src/ConfigFile.cc

780 lines
29 KiB
C++

/*
* Copyright © 2010-2021 Dynare Team
*
* This file is part of Dynare.
*
* Dynare is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Dynare is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Dynare. If not, see <https://www.gnu.org/licenses/>.
*/
#include <iostream>
#include <fstream>
#include <utility>
#include <vector>
#include <filesystem>
#include "ConfigFile.hh"
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wold-style-cast"
#include <boost/algorithm/string/trim.hpp>
#include <boost/algorithm/string/split.hpp>
#include <boost/tokenizer.hpp>
#pragma GCC diagnostic pop
using namespace std;
Hook::Hook(string global_init_file_arg)
{
if (global_init_file_arg.empty())
{
cerr << "ERROR: The Hook must have a Global Initialization File argument." << endl;
exit(EXIT_FAILURE);
}
hooks["global_init_file"] = move(global_init_file_arg);
}
Path::Path(vector<string> includepath_arg)
{
if (includepath_arg.empty())
{
cerr << "ERROR: The Path must have an Include argument." << endl;
exit(EXIT_FAILURE);
}
paths["include"] = move(includepath_arg);
}
SlaveNode::SlaveNode(string computerName_arg, string port_arg, int minCpuNbr_arg, int maxCpuNbr_arg, string userName_arg,
string password_arg, string remoteDrive_arg, string remoteDirectory_arg,
string programPath_arg, string programConfig_arg, string matlabOctavePath_arg, bool singleCompThread_arg,
int numberOfThreadsPerJob_arg, string operatingSystem_arg) :
computerName{move(computerName_arg)},
port{move(port_arg)},
minCpuNbr{minCpuNbr_arg},
maxCpuNbr{maxCpuNbr_arg},
userName{move(userName_arg)},
password{move(password_arg)},
remoteDrive{move(remoteDrive_arg)},
remoteDirectory{move(remoteDirectory_arg)},
programPath{move(programPath_arg)},
programConfig{move(programConfig_arg)},
matlabOctavePath{move(matlabOctavePath_arg)},
singleCompThread{singleCompThread_arg},
numberOfThreadsPerJob{numberOfThreadsPerJob_arg},
operatingSystem{move(operatingSystem_arg)}
{
if (computerName.empty())
{
cerr << "ERROR: The node must have a ComputerName." << endl;
exit(EXIT_FAILURE);
}
if (!operatingSystem.empty())
if (operatingSystem.compare("windows") != 0 && operatingSystem.compare("unix") != 0)
{
cerr << "ERROR: The OperatingSystem must be either 'unix' or 'windows' (Case Sensitive)." << endl;
exit(EXIT_FAILURE);
}
}
Cluster::Cluster(member_nodes_t member_nodes_arg) :
member_nodes{move(member_nodes_arg)}
{
if (member_nodes.empty())
{
cerr << "ERROR: The cluster must have at least one member node." << endl;
exit(EXIT_FAILURE);
}
}
ConfigFile::ConfigFile(bool parallel_arg, bool parallel_test_arg,
bool parallel_slave_open_mode_arg, bool parallel_use_psexec_arg,
string cluster_name_arg) :
parallel{parallel_arg}, parallel_test{parallel_test_arg},
parallel_slave_open_mode{parallel_slave_open_mode_arg},
parallel_use_psexec{parallel_use_psexec_arg},
cluster_name{move(cluster_name_arg)}
{
}
void
ConfigFile::getConfigFileInfo(const string &config_file)
{
using namespace boost;
ifstream configFile;
if (config_file.empty())
{
string defaultConfigFile;
// Test OS and try to open default file
#if defined(_WIN32) || defined(__CYGWIN32__)
if (getenv("APPDATA") == nullptr)
{
if (parallel || parallel_test)
cerr << "ERROR: ";
else
cerr << "WARNING: ";
cerr << "APPDATA environment variable not found." << endl;
if (parallel || parallel_test)
exit(EXIT_FAILURE);
}
else
{
defaultConfigFile += getenv("APPDATA");
defaultConfigFile += "\\dynare.ini";
}
#else
if (getenv("HOME") == nullptr)
{
if (parallel || parallel_test)
cerr << "ERROR: ";
else
cerr << "WARNING: ";
cerr << "HOME environment variable not found." << endl;
if (parallel || parallel_test)
exit(EXIT_FAILURE);
}
else
{
defaultConfigFile += getenv("HOME");
defaultConfigFile += "/.dynare";
}
#endif
configFile.open(defaultConfigFile, fstream::in);
if (!configFile.is_open())
if (parallel || parallel_test)
{
cerr << "ERROR: Could not open the default config file (" << defaultConfigFile << ")" << endl;
exit(EXIT_FAILURE);
}
else
return;
}
else
{
configFile.open(config_file, fstream::in);
if (!configFile.is_open())
{
cerr << "ERROR: Couldn't open file " << config_file << endl;;
exit(EXIT_FAILURE);
}
}
string name, computerName, port, userName, password, remoteDrive,
remoteDirectory, programPath, programConfig, matlabOctavePath,
operatingSystem, global_init_file;
vector<string> includepath;
int minCpuNbr{0}, maxCpuNbr{0};
int numberOfThreadsPerJob{1};
bool singleCompThread{false};
member_nodes_t member_nodes;
bool inHooks{false}, inNode{false}, inCluster{false}, inPaths{false};
while (configFile.good())
{
string line;
getline(configFile, line);
trim(line);
if (line.empty() || !line.compare(0, 1, "#"))
continue;
if (!line.compare("[node]")
|| !line.compare("[cluster]")
|| !line.compare("[hooks]")
|| !line.compare("[paths]"))
{
if (!global_init_file.empty())
// we were just in [hooks]
addHooksConfFileElement(global_init_file);
else if (!includepath.empty())
// we were just in [paths]
addPathsConfFileElement(includepath);
else
// we were just in [node] or [cluster]
addParallelConfFileElement(inNode, inCluster, member_nodes, name,
computerName, port, minCpuNbr, maxCpuNbr, userName,
password, remoteDrive, remoteDirectory,
programPath, programConfig, matlabOctavePath, singleCompThread,
numberOfThreadsPerJob, operatingSystem);
//! Reset communication vars / option defaults
if (!line.compare("[hooks]"))
{
inHooks = true;
inNode = false;
inCluster = false;
inPaths = false;
}
else if (!line.compare("[node]"))
{
inHooks = false;
inNode = true;
inCluster = false;
inPaths = false;
}
else if (!line.compare("[paths]"))
{
inHooks = false;
inNode = false;
inCluster = false;
inPaths = true;
}
else
{
inHooks = false;
inNode = false;
inCluster = true;
inPaths = false;
}
name = userName = computerName = port = password = remoteDrive
= remoteDirectory = programPath = programConfig = matlabOctavePath
= operatingSystem = global_init_file = "";
includepath.clear();
minCpuNbr = maxCpuNbr = 0;
numberOfThreadsPerJob = 1;
singleCompThread = false;
member_nodes.clear();
}
else
{
vector<string> tokenizedLine;
split(tokenizedLine, line, is_any_of("="));
if (tokenizedLine.size() != 2)
{
cerr << "ERROR (in config file): Options should be formatted as 'option = value'." << endl;
exit(EXIT_FAILURE);
}
trim(tokenizedLine.front());
trim(tokenizedLine.back());
if (inHooks)
if (!tokenizedLine.front().compare("GlobalInitFile"))
if (global_init_file.empty())
global_init_file = tokenizedLine.back();
else
{
cerr << "ERROR: May not have more than one GlobalInitFile option in [hooks] block." << endl;
exit(EXIT_FAILURE);
}
else
{
cerr << "ERROR: Unrecognized option " << tokenizedLine.front() << " in [hooks] block." << endl;
exit(EXIT_FAILURE);
}
else if (inPaths)
if (!tokenizedLine.front().compare("Include"))
if (includepath.empty())
{
vector<string> tokenizedPath;
split(tokenizedPath, tokenizedLine.back(), is_any_of(":"), token_compress_on);
for (auto &it : tokenizedPath)
if (!it.empty())
{
trim(it);
includepath.push_back(it);
}
}
else
{
cerr << "ERROR: May not have more than one Include option in [paths] block." << endl;
exit(EXIT_FAILURE);
}
else
{
cerr << "ERROR: Unrecognized option " << tokenizedLine.front() << " in [paths] block." << endl;
exit(EXIT_FAILURE);
}
else
if (!tokenizedLine.front().compare("Name"))
name = tokenizedLine.back();
else if (!tokenizedLine.front().compare("CPUnbr"))
{
vector<string> tokenizedCpuNbr;
split(tokenizedCpuNbr, tokenizedLine.back(), is_any_of(":"));
try
{
if (tokenizedCpuNbr.size() == 1)
{
minCpuNbr = 1;
maxCpuNbr = stoi(tokenizedCpuNbr.front());
}
else if (tokenizedCpuNbr.size() == 2
&& tokenizedCpuNbr[0].at(0) == '['
&& tokenizedCpuNbr[1].at(tokenizedCpuNbr[1].size()-1) == ']')
{
tokenizedCpuNbr[0].erase(0, 1);
tokenizedCpuNbr[1].erase(tokenizedCpuNbr[1].size()-1, 1);
minCpuNbr = stoi(tokenizedCpuNbr[0]);
maxCpuNbr = stoi(tokenizedCpuNbr[1]);
}
}
catch (const invalid_argument &)
{
cerr << "ERROR: Could not convert value to integer for CPUnbr." << endl;
exit(EXIT_FAILURE);
}
if (minCpuNbr <= 0 || maxCpuNbr <= 0)
{
cerr << "ERROR: Syntax for the CPUnbr option is as follows:" << endl
<< " 1) CPUnbr = <int>" << endl
<< " or 2) CPUnbr = [<int>:<int>]" << endl
<< " where <int> is an Integer > 0." << endl;
exit(EXIT_FAILURE);
}
minCpuNbr--;
maxCpuNbr--;
if (minCpuNbr > maxCpuNbr)
{
int tmp = maxCpuNbr;
maxCpuNbr = minCpuNbr;
minCpuNbr = tmp;
}
}
else if (!tokenizedLine.front().compare("Port"))
port = tokenizedLine.back();
else if (!tokenizedLine.front().compare("ComputerName"))
computerName = tokenizedLine.back();
else if (!tokenizedLine.front().compare("UserName"))
userName = tokenizedLine.back();
else if (!tokenizedLine.front().compare("Password"))
password = tokenizedLine.back();
else if (!tokenizedLine.front().compare("RemoteDrive"))
remoteDrive = tokenizedLine.back();
else if (!tokenizedLine.front().compare("RemoteDirectory"))
remoteDirectory = tokenizedLine.back();
else if (!tokenizedLine.front().compare("DynarePath")
|| !tokenizedLine.front().compare("ProgramPath"))
programPath = tokenizedLine.back();
else if (!tokenizedLine.front().compare("ProgramConfig"))
programConfig = tokenizedLine.back();
else if (!tokenizedLine.front().compare("MatlabOctavePath"))
matlabOctavePath = tokenizedLine.back();
else if (!tokenizedLine.front().compare("NumberOfThreadsPerJob"))
numberOfThreadsPerJob = stoi(tokenizedLine.back());
else if (!tokenizedLine.front().compare("SingleCompThread"))
if (tokenizedLine.back().compare("true") == 0)
singleCompThread = true;
else if (tokenizedLine.back().compare("false") == 0)
singleCompThread = false;
else
{
cerr << "ERROR (in config file): The value passed to SingleCompThread may only be 'true' or 'false'." << endl;
exit(EXIT_FAILURE);
}
else if (!tokenizedLine.front().compare("OperatingSystem"))
operatingSystem = tokenizedLine.back();
else if (!tokenizedLine.front().compare("Members"))
{
char_separator<char> sep(" ,;", "()", drop_empty_tokens);
tokenizer<char_separator<char>> tokens(tokenizedLine.back(), sep);
bool begin_weight = false;
string node_name;
for (const auto &token : tokens)
{
if (token.compare("(") == 0)
{
begin_weight = true;
continue;
}
else if (token.compare(")") == 0)
{
node_name.clear();
begin_weight = false;
continue;
}
if (!begin_weight)
{
if (!node_name.empty())
if (member_nodes.find(node_name) != member_nodes.end())
{
cerr << "ERROR (in config file): Node entered twice in specification of cluster." << endl;
exit(EXIT_FAILURE);
}
else
member_nodes[node_name] = 1.0;
node_name = token;
}
else
try
{
auto weight = stod(token);
if (weight <= 0)
{
cerr << "ERROR (in config file): Misspecification of weights passed to Members option." << endl;
exit(EXIT_FAILURE);
}
member_nodes[node_name] = weight;
}
catch (const invalid_argument &)
{
cerr << "ERROR (in config file): Misspecification of weights passed to Members option." << endl;
exit(EXIT_FAILURE);
}
}
if (!node_name.empty())
if (member_nodes.find(node_name) == member_nodes.end())
member_nodes[node_name] = 1.0;
else
{
cerr << "ERROR (in config file): Node entered twice in specification of cluster." << endl;
exit(EXIT_FAILURE);
}
}
else
{
cerr << "ERROR (in config file): Option " << tokenizedLine.front() << " is invalid." << endl;
exit(EXIT_FAILURE);
}
}
}
if (!global_init_file.empty())
addHooksConfFileElement(global_init_file);
else if (!includepath.empty())
addPathsConfFileElement(includepath);
else
addParallelConfFileElement(inNode, inCluster, member_nodes, name,
computerName, port, minCpuNbr, maxCpuNbr, userName,
password, remoteDrive, remoteDirectory,
programPath, programConfig, matlabOctavePath, singleCompThread,
numberOfThreadsPerJob, operatingSystem);
configFile.close();
}
void
ConfigFile::addHooksConfFileElement(string global_init_file)
{
if (global_init_file.empty())
{
cerr << "ERROR: The global initialization file must be passed to the GlobalInitFile option." << endl;
exit(EXIT_FAILURE);
}
else
hooks.emplace_back(move(global_init_file));
}
void
ConfigFile::addPathsConfFileElement(vector<string> includepath)
{
if (includepath.empty())
{
cerr << "ERROR: The path to be included must be passed to the Include option." << endl;
exit(EXIT_FAILURE);
}
else
paths.emplace_back(move(includepath));
}
void
ConfigFile::addParallelConfFileElement(bool inNode, bool inCluster, const member_nodes_t &member_nodes, const string &name,
const string &computerName, const string &port, int minCpuNbr, int maxCpuNbr,
const string &userName, const string &password, const string &remoteDrive,
const string &remoteDirectory, const string &programPath, const string &programConfig,
const string &matlabOctavePath, bool singleCompThread, int numberOfThreadsPerJob,
const string &operatingSystem)
{
//! ADD NODE
if (inNode)
if (!member_nodes.empty())
{
cerr << "Invalid option passed to [node]." << endl;
exit(EXIT_FAILURE);
}
else
if (name.empty() || slave_nodes.find(name) != slave_nodes.end())
{
cerr << "ERROR: Every node must be assigned a unique name." << endl;
exit(EXIT_FAILURE);
}
else
slave_nodes.emplace(name, SlaveNode{computerName, port, minCpuNbr, maxCpuNbr, userName,
password, remoteDrive, remoteDirectory, programPath, programConfig,
matlabOctavePath, singleCompThread, numberOfThreadsPerJob,
operatingSystem});
//! ADD CLUSTER
else if (inCluster)
if (minCpuNbr > 0 || maxCpuNbr > 0 || !userName.empty()
|| !password.empty() || !remoteDrive.empty() || !remoteDirectory.empty()
|| !programPath.empty() || !programConfig.empty()
|| !matlabOctavePath.empty() || !operatingSystem.empty())
{
cerr << "Invalid option passed to [cluster]." << endl;
exit(EXIT_FAILURE);
}
else
if (name.empty() || clusters.find(name) != clusters.end())
{
cerr << "ERROR: The cluster must be assigned a unique name." << endl;
exit(EXIT_FAILURE);
}
else
{
if (clusters.empty())
firstClusterName = name;
clusters.emplace(name, Cluster{member_nodes});
}
}
void
ConfigFile::checkPass(WarningConsolidation &warnings) const
{
bool global_init_file_declared = false;
for (const auto &hook : hooks)
{
for (const auto &mapit : hook.get_hooks())
if (mapit.first.compare("global_init_file") == 0)
if (global_init_file_declared == true)
{
cerr << "ERROR: Only one global initialization file may be provided." << endl;
exit(EXIT_FAILURE);
}
else
global_init_file_declared = true;
}
if (!parallel && !parallel_test)
return;
//! Check Slave Nodes
if (slave_nodes.empty())
{
cerr << "ERROR: At least one node must be defined in the config file." << endl;
exit(EXIT_FAILURE);
}
for (const auto &slave_node : slave_nodes)
{
#if !defined(_WIN32) && !defined(__CYGWIN32__)
//For Linux/Mac, check that cpuNbr starts at 0
if (slave_node.second.minCpuNbr != 0)
warnings << "WARNING: On Unix-based operating systems, you cannot specify the CPU that is "
<< "used in parallel processing. This will be adjusted for you such that the "
<< "same number of CPUs are used." << endl;
#endif
if (!slave_node.second.port.empty())
try
{
stoi(slave_node.second.port);
}
catch (const invalid_argument &)
{
cerr << "ERROR (node " << slave_node.first << "): the port must be an integer." << endl;
exit(EXIT_FAILURE);
}
if (!slave_node.second.computerName.compare("localhost")) // We are working locally
{
if (!slave_node.second.remoteDrive.empty())
{
cerr << "ERROR (node " << slave_node.first << "): the RemoteDrive option may not be passed for a local node." << endl;
exit(EXIT_FAILURE);
}
if (!slave_node.second.remoteDirectory.empty())
{
cerr << "ERROR (node " << slave_node.first << "): the RemoteDirectory option may not be passed for a local node." << endl;
exit(EXIT_FAILURE);
}
}
else
{
if (slave_node.second.userName.empty())
{
cerr << "ERROR (node " << slave_node.first << "): the UserName option must be passed for every remote node." << endl;
exit(EXIT_FAILURE);
}
if (slave_node.second.operatingSystem.compare("windows") == 0)
{
if (slave_node.second.password.empty())
{
cerr << "ERROR (node " << slave_node.first << "): the Password option must be passed under Windows for every remote node." << endl;
exit(EXIT_FAILURE);
}
if (slave_node.second.remoteDrive.empty())
{
cerr << "ERROR (node " << slave_node.first << "): the RemoteDrive option must be passed under Windows for every remote node." << endl;
exit(EXIT_FAILURE);
}
}
#if defined(_WIN32) || defined(__CYGWIN32__)
if (slave_node.second.operatingSystem.empty())
{
if (slave_node.second.password.empty())
{
cerr << "ERROR (node " << slave_node.first << "): the Password option must be passed under Windows for every remote node." << endl;
exit(EXIT_FAILURE);
}
if (slave_node.second.remoteDrive.empty())
{
cerr << "ERROR (node " << slave_node.first << "): the RemoteDrive option must be passed under Windows for every remote node." << endl;
exit(EXIT_FAILURE);
}
}
#endif
if (slave_node.second.remoteDirectory.empty())
{
cerr << "ERROR (node " << slave_node.first << "): the RemoteDirectory must be specified for every remote node." << endl;
exit(EXIT_FAILURE);
}
}
}
//! Check Clusters
if (clusters.empty())
{
cerr << "ERROR: At least one cluster must be defined in the config file." << endl;
exit(EXIT_FAILURE);
}
if (!cluster_name.empty() && clusters.find(cluster_name) == clusters.end())
{
cerr << "ERROR: Cluster Name " << cluster_name << " was not found in the config file." << endl;
exit(EXIT_FAILURE);
}
for (const auto &cluster : clusters)
for (const auto &itmn : cluster.second.member_nodes)
if (slave_nodes.find(itmn.first) == slave_nodes.end())
{
cerr << "Error: node " << itmn.first << " specified in cluster " << cluster.first << " was not found" << endl;
exit(EXIT_FAILURE);
}
}
void
ConfigFile::transformPass()
{
if (!parallel && !parallel_test)
return;
#if !defined(_WIN32) && !defined(__CYGWIN32__)
//For Linux/Mac, check that cpuNbr starts at 0
for (auto &it : slave_nodes)
if (it.second.minCpuNbr != 0)
{
it.second.maxCpuNbr = it.second.maxCpuNbr - it.second.minCpuNbr;
it.second.minCpuNbr = 0;
}
#endif
auto cluster_it = cluster_name.empty() ? clusters.find(firstClusterName) : clusters.find(cluster_name);
double weight_denominator{0.0};
for (const auto &it : cluster_it->second.member_nodes)
weight_denominator += it.second;
for (auto &member_node : cluster_it->second.member_nodes)
member_node.second /= weight_denominator;
}
vector<filesystem::path>
ConfigFile::getIncludePaths() const
{
vector<filesystem::path> include_paths;
for (auto path : paths)
for (const auto &mapit : path.get_paths())
for (const auto &vecit : mapit.second)
include_paths.emplace_back(vecit);
return include_paths;
}
void
ConfigFile::writeHooks(ostream &output) const
{
for (auto hook : hooks)
for (const auto &mapit : hook.get_hooks())
output << "options_." << mapit.first << " = '" << mapit.second << "';" << endl;
}
void
ConfigFile::writeCluster(ostream &output) const
{
if (!parallel && !parallel_test)
return;
auto cluster_it = cluster_name.empty() ? clusters.find(firstClusterName) : clusters.find(cluster_name);
int i{1};
for (const auto &slave_node : slave_nodes)
{
bool slave_node_in_member_nodes = false;
for (const auto &itmn : cluster_it->second.member_nodes)
if (!slave_node.first.compare(itmn.first))
slave_node_in_member_nodes = true;
if (!slave_node_in_member_nodes)
continue;
output << "options_.parallel";
if (i > 1)
output << "(" << i << ")";
i++;
output << " = struct('Local', ";
if (slave_node.second.computerName.compare("localhost"))
output << "0, ";
else
output << "1, ";
output << "'ComputerName', '" << slave_node.second.computerName << "', "
<< "'Port', '" << slave_node.second.port << "', "
<< "'CPUnbr', [" << slave_node.second.minCpuNbr << ":" << slave_node.second.maxCpuNbr << "], "
<< "'UserName', '" << slave_node.second.userName << "', "
<< "'Password', '" << slave_node.second.password << "', "
<< "'RemoteDrive', '" << slave_node.second.remoteDrive << "', "
<< "'RemoteDirectory', '" << slave_node.second.remoteDirectory << "', "
// The following should be switched back to “ProgramPath” once we move to Dragonfly
<< "'DynarePath', '" << slave_node.second.programPath << "', "
<< "'ProgramConfig', '" << slave_node.second.programConfig << "', "
<< "'MatlabOctavePath', '" << slave_node.second.matlabOctavePath << "', "
<< "'OperatingSystem', '" << slave_node.second.operatingSystem << "', "
<< "'NodeWeight', '" << (cluster_it->second.member_nodes.find(slave_node.first))->second << "', "
<< "'NumberOfThreadsPerJob', " << slave_node.second.numberOfThreadsPerJob << ", ";
if (slave_node.second.singleCompThread)
output << "'SingleCompThread', 'true');" << endl;
else
output << "'SingleCompThread', 'false');" << endl;
}
// Default values for the following two are both in DynareMain.cc and matlab/default_option_values.m
if (parallel_slave_open_mode)
output << "options_.parallel_info.leaveSlaveOpen = 1;" << endl;
if (!parallel_use_psexec)
output << "options_.parallel_use_psexec = false;" << endl;
output << "options_.parallel_info.console_mode= isoctave;" << endl;
output << "InitializeComputationalEnvironment();" << endl;
if (parallel_test)
output << "ErrorCode = AnalyseComputationalEnvironment(options_.parallel, options_.parallel_info);" << endl
<< "disp(['AnalyseComputationalEnvironment returned with Error Code: ' num2str(ErrorCode)]);" << endl
<< "diary off;" << endl
<< "return;" << endl;
}
void
ConfigFile::writeEndParallel(ostream &output) const
{
if ((!parallel && !parallel_test) || !parallel_slave_open_mode)
return;
output << "if options_.parallel_info.leaveSlaveOpen == 1" << endl
<< " closeSlave(options_.parallel,options_.parallel_info.RemoteTmpFolder);" << endl
<< "end" << endl;
}