blob: fe3afac754cb84522c39e7bf6fb477d01601eb9d [file] [log] [blame]
/**
* Copyright © 2019 IBM Corporation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "config.h"
#include "../bcd_time.hpp"
#include "../json_utils.hpp"
#include "../paths.hpp"
#include "../pel.hpp"
#include "../pel_types.hpp"
#include "../pel_values.hpp"
#include <Python.h>
#include <CLI/CLI.hpp>
#include <bitset>
#include <fstream>
#include <iostream>
#include <phosphor-logging/log.hpp>
#include <regex>
#include <string>
#include <xyz/openbmc_project/Common/File/error.hpp>
namespace fs = std::filesystem;
using namespace phosphor::logging;
using namespace openpower::pels;
using sdbusplus::exception::SdBusError;
namespace file_error = sdbusplus::xyz::openbmc_project::Common::File::Error;
namespace message = openpower::pels::message;
namespace pv = openpower::pels::pel_values;
const uint8_t critSysTermSeverity = 0x51;
using PELFunc = std::function<void(const PEL&, bool hexDump)>;
message::Registry registry(getPELReadOnlyDataPath() / message::registryFileName,
false);
namespace service
{
constexpr auto logging = "xyz.openbmc_project.Logging";
} // namespace service
namespace interface
{
constexpr auto deleteObj = "xyz.openbmc_project.Object.Delete";
constexpr auto deleteAll = "xyz.openbmc_project.Collection.DeleteAll";
} // namespace interface
namespace object_path
{
constexpr auto logEntry = "/xyz/openbmc_project/logging/entry/";
constexpr auto logging = "/xyz/openbmc_project/logging";
} // namespace object_path
/**
* @brief helper function to get PEL commit timestamp from file name
* @retrun BCDTime - PEL commit timestamp
* @param[in] std::string - file name
*/
BCDTime fileNameToTimestamp(const std::string& fileName)
{
std::string token = fileName.substr(0, fileName.find("_"));
int i = 0;
BCDTime tmp;
if (token.length() >= 14)
{
try
{
tmp.yearMSB = std::stoi(token.substr(i, 2), 0, 16);
}
catch (std::exception& err)
{
std::cout << "Conversion failure: " << err.what() << std::endl;
}
i += 2;
try
{
tmp.yearLSB = std::stoi(token.substr(i, 2), 0, 16);
}
catch (std::exception& err)
{
std::cout << "Conversion failure: " << err.what() << std::endl;
}
i += 2;
try
{
tmp.month = std::stoi(token.substr(i, 2), 0, 16);
}
catch (std::exception& err)
{
std::cout << "Conversion failure: " << err.what() << std::endl;
}
i += 2;
try
{
tmp.day = std::stoi(token.substr(i, 2), 0, 16);
}
catch (std::exception& err)
{
std::cout << "Conversion failure: " << err.what() << std::endl;
}
i += 2;
try
{
tmp.hour = std::stoi(token.substr(i, 2), 0, 16);
}
catch (std::exception& err)
{
std::cout << "Conversion failure: " << err.what() << std::endl;
}
i += 2;
try
{
tmp.minutes = std::stoi(token.substr(i, 2), 0, 16);
}
catch (std::exception& err)
{
std::cout << "Conversion failure: " << err.what() << std::endl;
}
i += 2;
try
{
tmp.seconds = std::stoi(token.substr(i, 2), 0, 16);
}
catch (std::exception& err)
{
std::cout << "Conversion failure: " << err.what() << std::endl;
}
i += 2;
try
{
tmp.hundredths = std::stoi(token.substr(i, 2), 0, 16);
}
catch (std::exception& err)
{
std::cout << "Conversion failure: " << err.what() << std::endl;
}
}
return tmp;
}
/**
* @brief helper function to get PEL id from file name
* @retrun uint32_t - PEL id
* @param[in] std::string - file name
*/
uint32_t fileNameToPELId(const std::string& fileName)
{
uint32_t num = 0;
try
{
num = std::stoi(fileName.substr(fileName.find("_") + 1), 0, 16);
}
catch (std::exception& err)
{
std::cout << "Conversion failure: " << err.what() << std::endl;
}
return num;
}
/**
* @brief helper function to check string suffix
* @retrun bool - true with suffix matches
* @param[in] std::string - string to check for suffix
* @param[in] std::string - suffix string
*/
bool ends_with(const std::string& str, const std::string& end)
{
size_t slen = str.size(), elen = end.size();
if (slen < elen)
return false;
while (elen)
{
if (str[--slen] != end[--elen])
return false;
}
return true;
}
/**
* @brief get data form raw PEL file.
* @param[in] std::string Name of file with raw PEL
* @return std::vector<uint8_t> char vector read from raw PEL file.
*/
std::vector<uint8_t> getFileData(const std::string& name)
{
std::ifstream file(name, std::ifstream::in);
if (file.good())
{
std::vector<uint8_t> data{std::istreambuf_iterator<char>(file),
std::istreambuf_iterator<char>()};
return data;
}
else
{
return {};
}
}
/**
* @brief Initialize Python interpreter and gather all UD parser modules under
* the paths found in Python sys.path and the current user directory.
* This is to prevent calling a non-existant module which causes Python
* to print an import error message and breaking JSON output.
*
* @return std::vector<std::string> Vector of plugins found in filesystem
*/
std::vector<std::string> getPlugins()
{
Py_Initialize();
std::vector<std::string> plugins;
std::vector<std::string> siteDirs;
std::array<std::string, 2> parserDirs = {"udparsers", "srcparsers"};
PyObject* pName = PyUnicode_FromString("sys");
PyObject* pModule = PyImport_Import(pName);
Py_XDECREF(pName);
PyObject* pDict = PyModule_GetDict(pModule);
Py_XDECREF(pModule);
PyObject* pResult = PyDict_GetItemString(pDict, "path");
PyObject* pValue = PyUnicode_FromString(".");
PyList_Append(pResult, pValue);
Py_XDECREF(pValue);
auto list_size = PyList_Size(pResult);
for (auto i = 0; i < list_size; i++)
{
PyObject* item = PyList_GetItem(pResult, i);
PyObject* pBytes = PyUnicode_AsEncodedString(item, "utf-8", "~E~");
const char* output = PyBytes_AS_STRING(pBytes);
Py_XDECREF(pBytes);
std::string tmpStr(output);
siteDirs.push_back(tmpStr);
}
for (const auto& dir : siteDirs)
{
for (const auto& parserDir : parserDirs)
{
if (fs::exists(dir + "/" + parserDir))
{
for (const auto& entry :
fs::directory_iterator(dir + "/" + parserDir))
{
if (entry.is_directory() and
fs::exists(entry.path().string() + "/" +
entry.path().stem().string() + ".py"))
{
plugins.push_back(entry.path().stem());
}
}
}
}
}
return plugins;
}
/**
* @brief Creates JSON string of a PEL entry if fullPEL is false or prints to
* stdout the full PEL in JSON if fullPEL is true
* @param[in] itr - std::map iterator of <uint32_t, BCDTime>
* @param[in] hidden - Boolean to include hidden PELs
* @param[in] includeInfo - Boolean to include informational PELs
* @param[in] critSysTerm - Boolean to include critical error and system
* termination PELs
* @param[in] fullPEL - Boolean to print full JSON representation of PEL
* @param[in] foundPEL - Boolean to check if any PEL is present
* @param[in] scrubRegex - SRC regex object
* @param[in] plugins - Vector of strings of plugins found in filesystem
* @param[in] hexDump - Boolean to print hexdump of PEL instead of JSON
* @return std::string - JSON string of PEL entry (empty if fullPEL is true)
*/
template <typename T>
std::string genPELJSON(T itr, bool hidden, bool includeInfo, bool critSysTerm,
bool fullPEL, bool& foundPEL,
const std::optional<std::regex>& scrubRegex,
const std::vector<std::string>& plugins, bool hexDump)
{
std::size_t found;
std::string val;
char tmpValStr[50];
std::string listStr;
char name[50];
sprintf(name, "%.2X%.2X%.2X%.2X%.2X%.2X%.2X%.2X_%.8X", itr.second.yearMSB,
itr.second.yearLSB, itr.second.month, itr.second.day,
itr.second.hour, itr.second.minutes, itr.second.seconds,
itr.second.hundredths, itr.first);
std::string fileName(name);
fileName = EXTENSION_PERSIST_DIR "/pels/logs/" + fileName;
try
{
std::vector<uint8_t> data = getFileData(fileName);
if (data.empty())
{
log<level::ERR>("Empty PEL file",
entry("FILENAME=%s", fileName.c_str()));
return listStr;
}
PEL pel{data};
if (!pel.valid())
{
return listStr;
}
if (!includeInfo && pel.userHeader().severity() == 0)
{
return listStr;
}
if (critSysTerm && pel.userHeader().severity() != critSysTermSeverity)
{
return listStr;
}
std::bitset<16> actionFlags{pel.userHeader().actionFlags()};
if (!hidden && actionFlags.test(hiddenFlagBit))
{
return listStr;
}
if (pel.primarySRC() && scrubRegex)
{
val = pel.primarySRC().value()->asciiString();
if (std::regex_search(trimEnd(val), scrubRegex.value(),
std::regex_constants::match_not_null))
{
return listStr;
}
}
if (hexDump)
{
std::cout << dumpHex(std::data(pel.data()), pel.size(), 0, false)
<< std::endl;
}
else if (fullPEL)
{
if (!foundPEL)
{
std::cout << "[\n";
foundPEL = true;
}
else
{
std::cout << ",\n\n";
}
pel.toJSON(registry, plugins);
}
else
{
// id
listStr += " \"" +
getNumberString("0x%X", pel.privateHeader().id()) +
"\": {\n";
// ASCII
if (pel.primarySRC())
{
val = pel.primarySRC().value()->asciiString();
jsonInsert(listStr, "SRC", trimEnd(val), 2);
// Registry message
auto regVal = pel.primarySRC().value()->getErrorDetails(
registry, DetailLevel::message, true);
if (regVal)
{
val = regVal.value();
jsonInsert(listStr, "Message", val, 2);
}
}
else
{
jsonInsert(listStr, "SRC", "No SRC", 2);
}
// platformid
jsonInsert(listStr, "PLID",
getNumberString("0x%X", pel.privateHeader().plid()), 2);
// creatorid
std::string creatorID =
getNumberString("%c", pel.privateHeader().creatorID());
val = pv::creatorIDs.count(creatorID) ? pv::creatorIDs.at(creatorID)
: "Unknown Creator ID";
jsonInsert(listStr, "CreatorID", val, 2);
// subsystem
std::string subsystem = pv::getValue(pel.userHeader().subsystem(),
pel_values::subsystemValues);
jsonInsert(listStr, "Subsystem", subsystem, 2);
// commit time
sprintf(tmpValStr, "%02X/%02X/%02X%02X %02X:%02X:%02X",
pel.privateHeader().commitTimestamp().month,
pel.privateHeader().commitTimestamp().day,
pel.privateHeader().commitTimestamp().yearMSB,
pel.privateHeader().commitTimestamp().yearLSB,
pel.privateHeader().commitTimestamp().hour,
pel.privateHeader().commitTimestamp().minutes,
pel.privateHeader().commitTimestamp().seconds);
jsonInsert(listStr, "Commit Time", tmpValStr, 2);
// severity
std::string severity = pv::getValue(pel.userHeader().severity(),
pel_values::severityValues);
jsonInsert(listStr, "Sev", severity, 2);
// compID
jsonInsert(listStr, "CompID",
getNumberString(
"0x%X", pel.privateHeader().header().componentID),
2);
found = listStr.rfind(",");
if (found != std::string::npos)
{
listStr.replace(found, 1, "");
listStr += " },\n";
}
foundPEL = true;
}
}
catch (std::exception& e)
{
log<level::ERR>("Hit exception while reading PEL File",
entry("FILENAME=%s", fileName.c_str()),
entry("ERROR=%s", e.what()));
}
return listStr;
}
/**
* @brief Print a list of PELs or a JSON array of PELs
* @param[in] order - Boolean to print in reverse orser
* @param[in] hidden - Boolean to include hidden PELs
* @param[in] includeInfo - Boolean to include informational PELs
* @param[in] critSysTerm - Boolean to include critical error and system
* termination PELs
* @param[in] fullPEL - Boolean to print full PEL into a JSON array
* @param[in] scrubRegex - SRC regex object
* @param[in] hexDump - Boolean to print hexdump of PEL instead of JSON
*/
void printPELs(bool order, bool hidden, bool includeInfo, bool critSysTerm,
bool fullPEL, const std::optional<std::regex>& scrubRegex,
bool hexDump)
{
std::string listStr;
std::map<uint32_t, BCDTime> PELs;
std::vector<std::string> plugins;
listStr = "{\n";
for (auto it = fs::directory_iterator(EXTENSION_PERSIST_DIR "/pels/logs");
it != fs::directory_iterator(); ++it)
{
if (!fs::is_regular_file((*it).path()))
{
continue;
}
else
{
PELs.emplace(fileNameToPELId((*it).path().filename()),
fileNameToTimestamp((*it).path().filename()));
}
}
bool foundPEL = false;
if (fullPEL && !hexDump)
{
plugins = getPlugins();
}
auto buildJSON = [&listStr, &hidden, &includeInfo, &critSysTerm, &fullPEL,
&foundPEL, &scrubRegex, &plugins,
&hexDump](const auto& i) {
listStr += genPELJSON(i, hidden, includeInfo, critSysTerm, fullPEL,
foundPEL, scrubRegex, plugins, hexDump);
};
if (order)
{
std::for_each(PELs.rbegin(), PELs.rend(), buildJSON);
}
else
{
std::for_each(PELs.begin(), PELs.end(), buildJSON);
}
if (hexDump)
{
return;
}
if (foundPEL)
{
if (fullPEL)
{
std::cout << "]" << std::endl;
}
else
{
std::size_t found;
found = listStr.rfind(",");
if (found != std::string::npos)
{
listStr.replace(found, 1, "");
listStr += "}\n";
printf("%s", listStr.c_str());
}
}
}
else
{
std::string emptyJSON = fullPEL ? "[]" : "{}";
std::cout << emptyJSON << std::endl;
}
}
/**
* @brief Calls the function passed in on the PEL with the ID
* passed in.
*
* @param[in] id - The string version of the PEL or BMC Log ID, either with or
* without the 0x prefix.
* @param[in] func - The std::function<void(const PEL&, bool hexDump)> function
* to run.
* @param[in] useBMC - if true, search by BMC Log ID, else search by PEL ID
* @param[in] hexDump - Boolean to print hexdump of PEL instead of JSON
*/
void callFunctionOnPEL(const std::string& id, const PELFunc& func,
bool useBMC = false, bool hexDump = false)
{
std::string pelID{id};
if (!useBMC)
{
std::transform(pelID.begin(), pelID.end(), pelID.begin(), toupper);
if (pelID.find("0X") == 0)
{
pelID.erase(0, 2);
}
}
bool found = false;
for (auto it = fs::directory_iterator(EXTENSION_PERSIST_DIR "/pels/logs");
it != fs::directory_iterator(); ++it)
{
// The PEL ID is part of the filename, so use that to find the PEL if
// "useBMC" is set to false, otherwise we have to search within the PEL
if (!fs::is_regular_file((*it).path()))
{
continue;
}
if ((ends_with((*it).path(), pelID) && !useBMC) || useBMC)
{
auto data = getFileData((*it).path());
if (!data.empty())
{
PEL pel{data};
if (!useBMC ||
(useBMC && pel.obmcLogID() == std::stoul(id, nullptr, 0)))
{
found = true;
try
{
func(pel, hexDump);
break;
}
catch (std::exception& e)
{
std::cerr << " Internal function threw an exception: "
<< e.what() << "\n";
exit(1);
}
}
}
else
{
std::cerr << "Could not read PEL file\n";
exit(1);
}
}
}
if (!found)
{
std::cerr << "PEL not found\n";
exit(1);
}
}
/**
* @brief Delete a PEL file.
*
* @param[in] id - The PEL ID to delete.
*/
void deletePEL(const std::string& id)
{
std::string pelID{id};
std::transform(pelID.begin(), pelID.end(), pelID.begin(), toupper);
if (pelID.find("0X") == 0)
{
pelID.erase(0, 2);
}
for (auto it = fs::directory_iterator(EXTENSION_PERSIST_DIR "/pels/logs");
it != fs::directory_iterator(); ++it)
{
if (ends_with((*it).path(), pelID))
{
fs::remove((*it).path());
}
}
}
/**
* @brief Delete all PEL files.
*/
void deleteAllPELs()
{
log<level::INFO>("peltool deleting all event logs");
for (const auto& entry :
fs::directory_iterator(EXTENSION_PERSIST_DIR "/pels/logs"))
{
fs::remove(entry.path());
}
}
/**
* @brief Display a single PEL
*
* @param[in] pel - the PEL to display
* @param[in] hexDump - Boolean to print hexdump of PEL instead of JSON
*/
void displayPEL(const PEL& pel, bool hexDump)
{
if (pel.valid())
{
if (hexDump)
{
std::string dstr =
dumpHex(std::data(pel.data()), pel.size(), 0, false);
std::cout << dstr << std::endl;
}
else
{
auto plugins = getPlugins();
pel.toJSON(registry, plugins);
}
}
else
{
std::cerr << "PEL was malformed\n";
exit(1);
}
}
/**
* @brief Print number of PELs
* @param[in] hidden - Bool to include hidden logs
* @param[in] includeInfo - Bool to include informational logs
* @param[in] critSysTerm - Bool to include CritSysTerm
* @param[in] scrubRegex - SRC regex object
*/
void printPELCount(bool hidden, bool includeInfo, bool critSysTerm,
const std::optional<std::regex>& scrubRegex)
{
std::size_t count = 0;
for (auto it = fs::directory_iterator(EXTENSION_PERSIST_DIR "/pels/logs");
it != fs::directory_iterator(); ++it)
{
if (!fs::is_regular_file((*it).path()))
{
continue;
}
std::vector<uint8_t> data = getFileData((*it).path());
if (data.empty())
{
continue;
}
PEL pel{data};
if (!pel.valid())
{
continue;
}
if (!includeInfo && pel.userHeader().severity() == 0)
{
continue;
}
if (critSysTerm && pel.userHeader().severity() != critSysTermSeverity)
{
continue;
}
std::bitset<16> actionFlags{pel.userHeader().actionFlags()};
if (!hidden && actionFlags.test(hiddenFlagBit))
{
continue;
}
if (pel.primarySRC() && scrubRegex)
{
std::string val = pel.primarySRC().value()->asciiString();
if (std::regex_search(trimEnd(val), scrubRegex.value(),
std::regex_constants::match_not_null))
{
continue;
}
}
count++;
}
std::cout << "{\n"
<< " \"Number of PELs found\": "
<< getNumberString("%d", count) << "\n}\n";
}
/**
* @brief Generate regex pattern object from file contents
* @param[in] scrubFile - File containing regex pattern
* @return std::regex - SRC regex object
*/
std::regex genRegex(std::string& scrubFile)
{
std::string pattern;
std::ifstream contents(scrubFile);
if (contents.fail())
{
std::cerr << "Can't open \"" << scrubFile << "\"\n";
exit(1);
}
std::string line;
while (std::getline(contents, line))
{
if (!line.empty())
{
pattern.append(line + "|");
}
}
try
{
std::regex scrubRegex(pattern, std::regex::icase);
return scrubRegex;
}
catch (std::regex_error& e)
{
if (e.code() == std::regex_constants::error_collate)
std::cerr << "Invalid collating element request\n";
else if (e.code() == std::regex_constants::error_ctype)
std::cerr << "Invalid character class\n";
else if (e.code() == std::regex_constants::error_escape)
std::cerr << "Invalid escape character or trailing escape\n";
else if (e.code() == std::regex_constants::error_backref)
std::cerr << "Invalid back reference\n";
else if (e.code() == std::regex_constants::error_brack)
std::cerr << "Mismatched bracket ([ or ])\n";
else if (e.code() == std::regex_constants::error_paren)
{
// to catch return code error_badrepeat when error_paren is retured
// instead
size_t pos = pattern.find_first_of("*+?{");
while (pos != std::string::npos)
{
if (pos == 0 || pattern.substr(pos - 1, 1) == "|")
{
std::cerr
<< "A repetition character (*, ?, +, or {) was not "
"preceded by a valid regular expression\n";
exit(1);
}
pos = pattern.find_first_of("*+?{", pos + 1);
}
std::cerr << "Mismatched parentheses (( or ))\n";
}
else if (e.code() == std::regex_constants::error_brace)
std::cerr << "Mismatched brace ({ or })\n";
else if (e.code() == std::regex_constants::error_badbrace)
std::cerr << "Invalid range inside a { }\n";
else if (e.code() == std::regex_constants::error_range)
std::cerr << "Invalid character range (e.g., [z-a])\n";
else if (e.code() == std::regex_constants::error_space)
std::cerr << "Insufficient memory to handle regular expression\n";
else if (e.code() == std::regex_constants::error_badrepeat)
std::cerr << "A repetition character (*, ?, +, or {) was not "
"preceded by a valid regular expression\n";
else if (e.code() == std::regex_constants::error_complexity)
std::cerr << "The requested match is too complex\n";
else if (e.code() == std::regex_constants::error_stack)
std::cerr << "Insufficient memory to evaluate a match\n";
exit(1);
}
}
static void exitWithError(const std::string& help, const char* err)
{
std::cerr << "ERROR: " << err << std::endl << help << std::endl;
exit(-1);
}
int main(int argc, char** argv)
{
CLI::App app{"OpenBMC PEL Tool"};
std::string fileName;
std::string idPEL;
std::string bmcId;
std::string idToDelete;
std::string scrubFile;
std::optional<std::regex> scrubRegex;
bool listPEL = false;
bool listPELDescOrd = false;
bool hidden = false;
bool includeInfo = false;
bool critSysTerm = false;
bool deleteAll = false;
bool showPELCount = false;
bool fullPEL = false;
bool hexDump = false;
app.set_help_flag("--help", "Print this help message and exit");
app.add_option("--file", fileName, "Display a PEL using its Raw PEL file");
app.add_option("-i, --id", idPEL, "Display a PEL based on its ID");
app.add_option("--bmc-id", bmcId,
"Display a PEL based on its BMC Event ID");
app.add_flag("-a", fullPEL, "Display all PELs");
app.add_flag("-l", listPEL, "List PELs");
app.add_flag("-n", showPELCount, "Show number of PELs");
app.add_flag("-r", listPELDescOrd, "Reverse order of output");
app.add_flag("-h", hidden, "Include hidden PELs");
app.add_flag("-f,--info", includeInfo, "Include informational PELs");
app.add_flag("-t, --termination", critSysTerm,
"List only critical system terminating PELs");
app.add_option("-d, --delete", idToDelete, "Delete a PEL based on its ID");
app.add_flag("-D, --delete-all", deleteAll, "Delete all PELs");
app.add_option("-s, --scrub", scrubFile,
"File containing SRC regular expressions to ignore");
app.add_flag("-x", hexDump, "Display PEL(s) in hexdump instead of JSON");
CLI11_PARSE(app, argc, argv);
if (!fileName.empty())
{
std::vector<uint8_t> data = getFileData(fileName);
if (!data.empty())
{
PEL pel{data};
if (hexDump)
{
std::string dstr =
dumpHex(std::data(pel.data()), pel.size(), 0, false);
std::cout << dstr << std::endl;
}
else
{
auto plugins = getPlugins();
pel.toJSON(registry, plugins);
}
}
else
{
exitWithError(app.help("", CLI::AppFormatMode::All),
"Raw PEL file can't be read.");
}
}
else if (!idPEL.empty())
{
callFunctionOnPEL(idPEL, displayPEL, false, hexDump);
}
else if (!bmcId.empty())
{
callFunctionOnPEL(bmcId, displayPEL, true, hexDump);
}
else if (fullPEL || listPEL)
{
if (!scrubFile.empty())
{
scrubRegex = genRegex(scrubFile);
}
printPELs(listPELDescOrd, hidden, includeInfo, critSysTerm, fullPEL,
scrubRegex, hexDump);
}
else if (showPELCount)
{
if (!scrubFile.empty())
{
scrubRegex = genRegex(scrubFile);
}
printPELCount(hidden, includeInfo, critSysTerm, scrubRegex);
}
else if (!idToDelete.empty())
{
deletePEL(idToDelete);
}
else if (deleteAll)
{
deleteAllPELs();
}
else
{
std::cout << app.help("", CLI::AppFormatMode::All) << std::endl;
}
Py_Finalize();
return 0;
}