pleascach/include/csys/argument_parser.h

517 lines
17 KiB
C++

// Copyright (c) 2020-present, Roland Munguia & Tristan Florian Bouchard.
// Distributed under the MIT License (http://opensource.org/licenses/MIT)
#ifndef CSYS_ARGUMENT_PARSER_H
#define CSYS_ARGUMENT_PARSER_H
#pragma once
#include "csys/api.h"
#include "csys/string.h"
#include "csys/exceptions.h"
#include <vector>
#include <stdexcept>
#include <string_view>
namespace csys
{
namespace
{
inline const std::string_view s_Reserved("\\[]\""); //!< All the reserved chars
inline constexpr char s_ErrMsgReserved[] = "Reserved chars '\\, [, ], \"' must be escaped with \\"; //!< Error message for reserved chars
}
/*!
* \brief
* Helper class to check for parsing arguments that involve checking for reserved chars
*/
struct CSYS_API Reserved
{
/*!
* \brief
* Checks if the current char 'c' is the escaping char
* \param c
* Char to compare against with the escaping char
* \return
* Returns true if the char 'c' is the escaping char
*/
static inline bool IsEscapeChar(char c)
{ return c == '\\'; }
/*!
* \brief
* Checks if the current char is reserved
* \param c
* Char to check
* \return
* Returns true if the current char 'c' is reserved
*/
static inline bool IsReservedChar(char c)
{
for (auto rc : s_Reserved) if (c == rc) return true;
return false;
}
/*!
* \brief
* Checks if a char is escaping another
* \param input
* Chars to check for it escaping
* \param pos
* Char that is escaping
* \return
* Returns true if the current char is the escaping char and is escaping
*/
static inline bool IsEscaping(std::string &input, size_t pos)
{
return pos < input.size() - 1 && IsEscapeChar(input[pos]) && IsReservedChar(input[pos + 1]);
}
/*!
* \brief
* Checks if a certain char is being escaped
* \param input
* Chars to check
* \param pos
* The current char to check if its being escaped
* \return
* Returns true if the current char at 'pos' is being escaped
*/
static inline bool IsEscaped(std::string &input, size_t pos)
{
bool result = false;
// Go through checking if the prev char is getting escaped and toggle between true and false
// Edge case is escaping the escaping char before another
for (size_t i = pos; i > 0; --i)
if (IsReservedChar(input[i]) && IsEscapeChar(input[i - 1]))
result = !result;
else
break;
return result;
}
// Delete unwanted operations
Reserved() = delete;
~Reserved() = delete;
Reserved(Reserved&&) = delete;
Reserved(const Reserved&) = delete;
Reserved& operator=(Reserved&&) = delete;
Reserved& operator=(const Reserved&) = delete;
};
/*!
* \brief
* Used for parsing arguments of different types
* \tparam T
* Argument type
*/
template<typename T>
struct CSYS_API ArgumentParser
{
/*!
* \brief
* Constructor that parses the argument and sets value T
* \param input
* Command line set of arguments to be parsed
* \param start
* Start in 'input' to where this argument should be parsed from
*/
inline ArgumentParser(String &input, size_t &start);
T m_Value; //!< Value of parsed argument
};
/*!
* \brief
* Macro for template specialization, used for cleaner code reuse
*/
#define ARG_PARSE_BASE_SPEC(TYPE) \
template<> \
struct CSYS_API ArgumentParser<TYPE> \
{ \
inline ArgumentParser(String &input, size_t &start); \
TYPE m_Value = 0; \
}; \
inline ArgumentParser<TYPE>::ArgumentParser(String &input, size_t &start)
/*!
* \brief
* Macro for getting the sub-string within a range, used for readability
*/
#define ARG_PARSE_SUBSTR(range) input.m_String.substr(range.first, range.second - range.first)
/*!
* \brief
* Macro for build-int types that already have functions in the stl for parsing them from a string
*/
#define ARG_PARSE_GENERAL_SPEC(TYPE, TYPE_NAME, FUNCTION) \
ARG_PARSE_BASE_SPEC(TYPE) \
{ \
auto range = input.NextPoi(start); \
try \
{ \
m_Value = (TYPE)FUNCTION(ARG_PARSE_SUBSTR(range), &range.first); \
} \
catch (const std::out_of_range&) \
{ \
throw Exception(std::string("Argument too large for ") + TYPE_NAME, \
input.m_String.substr(range.first, range.second)); \
} \
catch (const std::invalid_argument&) \
{ \
throw Exception(std::string("Missing or invalid ") + TYPE_NAME + " argument", \
input.m_String.substr(range.first, range.second)); } \
}
/*!
* \brief
* Template specialization for string argument parsing
*/
ARG_PARSE_BASE_SPEC(csys::String)
{
m_Value.m_String.clear(); // Empty string before using
// Lambda for getting a word from the string and checking for reserved chars
static auto GetWord = [](std::string &str, size_t start, size_t end)
{
// For issues with reserved chars
static std::string invalid_chars;
invalid_chars.clear();
std::string result;
// Go through the str from start to end
for (size_t i = start; i < end; ++i)
// general case, not reserved char
if (!Reserved::IsReservedChar(str[i]))
result.push_back(str[i]);
// is a reserved char
else
{
// check for \ char and if its escaping
if (Reserved::IsEscapeChar(str[i]) && Reserved::IsEscaping(str, i))
result.push_back(str[++i]);
// reserved char but not being escaped
else
throw Exception(s_ErrMsgReserved, str.substr(start, end - start));
}
return result;
};
// Go to the start of the string argument
auto range = input.NextPoi(start);
// If its a single string
if (input.m_String[range.first] != '"')
m_Value = GetWord(input.m_String, range.first, range.second);
// Multi word string
else
{
++range.first; // move past the first "
while (true)
{
// Get the next non-escaped "
range.second = input.m_String.find('"', range.first);
while (range.second != std::string::npos && Reserved::IsEscaped(input.m_String, range.second))
range.second = input.m_String.find('"', range.second + 1);
// Check for closing "
if (range.second == std::string::npos)
{
range.second = input.m_String.size();
throw Exception("Could not find closing '\"'", ARG_PARSE_SUBSTR(range));
}
// Add word to already existing string
m_Value.m_String += GetWord(input.m_String, range.first, range.second);
// Go to next word
range.first = range.second + 1;
// End of string check
if (range.first < input.m_String.size() && !std::isspace(input.m_String[range.first]))
{
// joining two strings together
if (input.m_String[range.first] == '"')
++range.first;
}
else
// End of input
break;
}
}
// Finished parsing
start = range.second + 1;
}
/*!
* \brief
* Template specialization for boolean argument parsing
*/
ARG_PARSE_BASE_SPEC(bool)
{
// Error messages
static const char *s_err_msg = "Missing or invalid boolean argument";
static const char *s_false = "false";
static const char *s_true = "true";
// Get argument
auto range = input.NextPoi(start);
// check if the length is between the len of "true" and "false"
input.m_String[range.first] = char(std::tolower(input.m_String[range.first]));
// true branch
if (range.second - range.first == 4 && input.m_String[range.first] == 't')
{
// Go through comparing grabbed arg to "true" char by char, bail if not the same
for (size_t i = range.first + 1; i < range.second; ++i)
if ((input.m_String[i] = char(std::tolower(input.m_String[i]))) != s_true[i - range.first])
throw Exception(s_err_msg + std::string(", expected true"), ARG_PARSE_SUBSTR(range));
m_Value = true;
}
// false branch
else if (range.second - range.first == 5 && input.m_String[range.first] == 'f')
{
// Go through comparing grabbed arg to "false" char by char, bail if not the same
for (size_t i = range.first + 1; i < range.second; ++i)
if ((input.m_String[i] = char(std::tolower(input.m_String[i]))) != s_false[i - range.first])
throw Exception(s_err_msg + std::string(", expected false"), ARG_PARSE_SUBSTR(range));
m_Value = false;
}
// anything else, not true or false
else
throw Exception(s_err_msg, ARG_PARSE_SUBSTR(range));
}
/*!
* \brief
* Template specialization for char argument parsing
*/
ARG_PARSE_BASE_SPEC(char)
{
// Grab the argument
auto range = input.NextPoi(start);
size_t len = range.second - range.first;
// Check if its 3 or more letters
if (len > 2 || len <= 0)
throw Exception("Too many or no chars were given", ARG_PARSE_SUBSTR(range));
// potential reserved char
else if (len == 2)
{
// Check if the first char is \ and the second is a reserved char
if (!Reserved::IsEscaping(input.m_String, range.first))
throw Exception("Too many chars were given", ARG_PARSE_SUBSTR(range));
// is correct
m_Value = input.m_String[range.first + 1];
}
// if its one char and reserved
else if (Reserved::IsReservedChar(input.m_String[range.first]))
throw Exception(s_ErrMsgReserved, ARG_PARSE_SUBSTR(range));
// one char, not reserved
else
m_Value = input.m_String[range.first];
}
/*!
* \brief
* Template specialization for unsigned char argument parsing
*/
ARG_PARSE_BASE_SPEC(unsigned char)
{
// Grab the argument
auto range = input.NextPoi(start);
size_t len = range.second - range.first;
// Check if its 3 or more letters
if (len > 2 || len <= 0)
throw Exception("Too many or no chars were given", ARG_PARSE_SUBSTR(range));
// potential reserved char
else if (len == 2)
{
// Check if the first char is \ and the second is a reserved char
if (!Reserved::IsEscaping(input.m_String, range.first))
throw Exception("Too many chars were given", ARG_PARSE_SUBSTR(range));
// is correct
m_Value = static_cast<unsigned char>(input.m_String[range.first + 1]);
}
// if its one char and reserved
else if (Reserved::IsReservedChar(input.m_String[range.first]))
throw Exception(s_ErrMsgReserved, ARG_PARSE_SUBSTR(range));
// one char, not reserved
else
m_Value = static_cast<unsigned char>(input.m_String[range.first]);
}
/*!
* \brief
* Template specialization for short argument parsing
*/
ARG_PARSE_GENERAL_SPEC(short, "signed short", std::stoi)
/*!
* \brief
* Template specialization for unsigned short argument parsing
*/
ARG_PARSE_GENERAL_SPEC(unsigned short, "unsigned short", std::stoul)
/*!
* \brief
* Template specialization for int argument parsing
*/
ARG_PARSE_GENERAL_SPEC(int, "signed int", std::stoi)
/*!
* \brief
* Template specialization for unsigned int argument parsing
*/
ARG_PARSE_GENERAL_SPEC(unsigned int, "unsigned int", std::stoul)
/*!
* \brief
* Template specialization for long argument parsing
*/
ARG_PARSE_GENERAL_SPEC(long, "long", std::stol)
/*!
* \brief
* Template specialization for unsigned long argument parsing
*/
ARG_PARSE_GENERAL_SPEC(unsigned long, "unsigned long", std::stoul)
/*!
* \brief
* Template specialization for long long argument parsing
*/
ARG_PARSE_GENERAL_SPEC(long long, "long long", std::stoll)
/*!
* \brief
* Template specialization for unsigned long long argument parsing
*/
ARG_PARSE_GENERAL_SPEC(unsigned long long, "unsigned long long", std::stoull)
/*!
* \brief
* Template specialization for float argument parsing
*/
ARG_PARSE_GENERAL_SPEC(float, "float", std::stof)
/*!
* \brief
* Template specialization for double argument parsing
*/
ARG_PARSE_GENERAL_SPEC(double, "double", std::stod)
/*!
* \brief
* Template specialization for long double argument parsing
*/
ARG_PARSE_GENERAL_SPEC(long double, "long double", std::stold)
/*!
* \brief
* Template specialization for vector argument parsing
* \tparam T
* Type that the vector holds
*
*/
template<typename T>
struct CSYS_API ArgumentParser<std::vector<T>>
{
/*!
* \brief
* Grabs a vector argument of type T from 'input' starting from 'start'
* \param input
* Input to the command for this class to parse its argument
* \param start
* Start of this argument
*/
ArgumentParser(String &input, size_t &start);
std::vector<T> m_Value; //!< Vector of data parsed
};
/*!
* \brief
* Grabs a vector argument of type T from 'input' starting from 'start'
* \tparam T
* Type of vector
* \param input
* Input to the command for this class to parse its argument
* \param start
* Start of this argument
*/
template<typename T>
ArgumentParser<std::vector<T>>::ArgumentParser(String &input, size_t &start)
{
// Clean out vector before use
m_Value.clear();
// Grab the start of the vector argument
auto range = input.NextPoi(start);
// Empty
if (range.first == input.End()) return;
// Not starting with [
if (input.m_String[range.first] != '[')
throw Exception("Invalid vector argument missing opening [", ARG_PARSE_SUBSTR(range));
// Erase [
input.m_String[range.first] = ' ';
while (true)
{
// Get next argument in vector
range = input.NextPoi(range.first);
// No more, empty vector
if (range.first == input.End()) return;
// Is a nested vector, go deeper
else if (input.m_String[range.first] == '[')
m_Value.push_back(ArgumentParser<T>(input, range.first).m_Value);
else
{
// Find first non-escaped ]
range.second = input.m_String.find(']', range.first);
while (range.second != std::string::npos && Reserved::IsEscaped(input.m_String, range.second))
range.second = input.m_String.find(']', range.second + 1);
// Check for closing ]
if (range.second == std::string::npos)
{
range.second = input.m_String.size();
throw Exception("Invalid vector argument missing closing ]", ARG_PARSE_SUBSTR(range));
}
// Erase ]
input.m_String[range.second] = ' ';
start = range.first;
// Parse all arguments contained within the vector
while (true)
{
// If end of parsing, get out
if ((range.first = input.NextPoi(range.first).first) >= range.second)
{
start = range.first;
return;
}
// Parse argument and go to next
m_Value.push_back(ArgumentParser<T>(input, start).m_Value);
range.first = start;
}
}
}
}
}
#endif //CSYS_ARGUMENT_PARSER_H