Skip to content

Instantly share code, notes, and snippets.

@Jacob-Tate
Created December 4, 2019 06:15
Show Gist options
  • Save Jacob-Tate/2283ac49ce9e732a536ae1e3157b3d34 to your computer and use it in GitHub Desktop.
Save Jacob-Tate/2283ac49ce9e732a536ae1e3157b3d34 to your computer and use it in GitHub Desktop.
/**
* @file arclite_getopt.cpp
* @author Jacob I. Tate
* @brief Simple getopt style command line parser with short and long options
*/
#include "arclite_getopt.hpp"
#include <cstdio> // vsnprintf
#include <cstdarg> // va_list
#include <cstring>
#if !defined(_MSC_VER)
#include <strings.h> // strncasecmp
#else
#include <ctype.h> // tolower
#endif
static int str_case_cmp_len(const char* s1, const char* s2, unsigned int len)
{
#if defined(_MSC_VER)
for(unsigned int i = 0; i < len; i++)
{
int c1 = tolower(s1[i]);
int c2 = tolower(s2[i]);
if(c1 < c2) return -1;
if(c1 > c2) return 1;
if(c1 == '\0' && c1 == c2) return 0;
}
return 0;
#else // defined(_MSC_VER)
return strncasecmp(s1, s2, len);
#endif // defined(_MSC_VER)
}
static int str_format(char* buffer, size_t buffer_size, const char* format, ...)
{
va_list args;
va_start(args, format);
int ret = vsnprintf(buffer, buffer_size, format, args);
// This fixes an error lmao?
#if defined(_MSC_VER)
buffer[buffer_size - 1] = '\0';
#endif
va_end(args);
return ret;
}
//
// Exception stuff
//
arclite_getopt_invalid_shortopt::arclite_getopt_invalid_shortopt(const std::string& exception)
: exception_(exception) {}
const char* arclite_getopt_invalid_shortopt::what() const noexcept
{
// you could use std::string_view::data but no guarantee its null terminated
return std::string(exception_).c_str();
}
arclite_getopt::arclite_getopt(int argc, const char** argv, const arclite_getopt_option_t* options)
: context_{ }
{
context_.argc = (argc > 1) ? (argc - 1) : 0; // Take away command name yo!
context_.argv = (argc > 1) ? (argv + 1) : argv; // Take away command name
context_.options = options;
context_.current_index = 0;
context_.current_opt_arg = nullptr;
// Count the epic options
context_.num_options = 0;
const arclite_getopt_option_t* temp_options = options;
while(!(options->name == nullptr && options->name_short == 0))
{
if(options->value == '!' || options->value == '?' ||
options->value == '+' || options->value == -1)
{
throw arclite_getopt_invalid_shortopt(
std::to_string(options->value) + " is not a valid value");
return;
}
context_.num_options++;
options++;
}
}
int arclite_getopt::next()
{
// Have we processed all the commands
if(context_.current_index == context_.argc)
return -1;
// Reset opt-arg
context_.current_opt_arg = nullptr;
const char* current_token = context_.argv[context_.current_index];
// This token have been processed
context_.current_index++;
// Check if the item is a no-option
if(current_token[0] && current_token[0] != '-')
{
context_.current_opt_arg = current_token;
return '+'; // Return 'x' as the identifier for no option
}
const arclite_getopt_option_t* found_option = nullptr;
const char* found_argument = nullptr;
// Check for shortop
if(current_token[1] != '\0' && current_token[1] != '-' && current_token[2] == '\0')
{
for(int i = 0; i < context_.num_options; i++)
{
// Retrieve the option
const arclite_getopt_option_t* temp_option = context_.options + i;
if(temp_option->name_short == current_token[1])
{
found_option = temp_option;
// if there is a value when:
// current_index < argc and value
// in argv[current_index] dont start it with
// a '-'
if((context_.current_index != context_.argc) &&
(context_.argv[context_.current_index][0] != '-')
&&
(temp_option->type == ARCLITE_GETOPT_TYPE_OPTIONAL ||
temp_option->type == ARCLITE_GETOPT_TYPE_REQUIRED))
{
found_argument = context_.argv[context_.current_index++];
}
break;
}
}
}
// Check for long options
else if(current_token[1] == '-' && current_token[2] != '\0')
{
const char* check_option = current_token + 2;
for(int i = 0; i < context_.num_options; i++)
{
// retrieve option
const arclite_getopt_option_t* temp_option = context_.options + i;
auto name_length = (unsigned int)strlen(temp_option->name);
if(str_case_cmp_len(temp_option->name, check_option, name_length) == 0)
{
check_option += name_length;
// Find the argument if it exists
switch(*check_option)
{
case '\0':
{
// Are there more tokens that can contain
// the '=' case
if(context_.current_index < context_.argc)
{
const char* next_token = context_.argv[context_.current_index];
if(next_token[0] == '=')
{
context_.current_index++;
if(next_token[1] != '\0')
found_argument = next_token + 1;
else if(context_.current_index < context_.argc)
found_argument = context_.argv[context_.current_index++];
}
else if(next_token[0] != '-')
{
context_.current_index++;
found_argument = next_token;
}
}
break;
}
case '=':
{
if(check_option[1] != '\0')
{
found_argument = check_option + 1;
}
else if(context_.current_index < context_.argc)
{
found_argument = context_.argv[context_.current_index++];
}
break;
}
default:
continue; // not found but matched ie --test and --testing
}
found_option = temp_option;
break;
}
}
}
// Malformed option '-', '-xyz' or '--'
else
{
context_.current_opt_arg = current_token;
return '!';
}
// No matching options
if(found_option == nullptr)
{
context_.current_opt_arg = current_token;
return '?';
}
if(found_argument != nullptr)
{
switch(found_option->type)
{
case ARCLITE_GETOPT_TYPE_FLAG_SET:
case ARCLITE_GETOPT_TYPE_FLAG_AND:
case ARCLITE_GETOPT_TYPE_FLAG_OR:
case ARCLITE_GETOPT_TYPE_NO_ARGS:
// These types should have no argument the
// user shouldnt have given an argument
context_.current_opt_arg = found_option->name;
return '!';
case ARCLITE_GETOPT_TYPE_OPTIONAL:
case ARCLITE_GETOPT_TYPE_REQUIRED:
context_.current_opt_arg = found_argument;
return found_option->value;
}
}
// No argument was found
else
{
switch(found_option->type)
{
case ARCLITE_GETOPT_TYPE_FLAG_SET:
*found_option->flag = found_option->value;
return 0;
case ARCLITE_GETOPT_TYPE_FLAG_AND:
*found_option->flag &= found_option->value;
return 0;
case ARCLITE_GETOPT_TYPE_FLAG_OR:
*found_option->flag |= found_option->value;
return 0;
case ARCLITE_GETOPT_TYPE_NO_ARGS:
case ARCLITE_GETOPT_TYPE_OPTIONAL:
return found_option->value;
// The option retuires an argument ie --option=argument or -o arg
case ARCLITE_GETOPT_TYPE_REQUIRED:
context_.current_opt_arg = found_option->name;
return '!';
}
}
return -1;
}
const char* arclite_getopt::current_opt_arg()
{
return context_.current_opt_arg;
}
const char* arclite_getopt::create_help_string(char* buffer, size_t buffer_size)
{
size_t buffer_pos = 0;
for(int option_index = 0; option_index < context_.num_options; ++option_index)
{
const arclite_getopt_option_t* temp_option = context_.options + option_index;
size_t outpos;
char long_name[64];
int chars_written = str_format(long_name, 64, "--%s", temp_option->name);
if(chars_written < 0)
return buffer;
outpos = size_t(chars_written);
switch(temp_option->type)
{
case ARCLITE_GETOPT_TYPE_REQUIRED:
str_format(long_name + outpos, 64 - outpos, "=<%s>", temp_option->value_description);
break;
case ARCLITE_GETOPT_TYPE_OPTIONAL:
str_format(long_name + outpos, 64 - outpos, "(=%s)", temp_option->value_description);
break;
default:
break;
}
if(temp_option->name_short == 0x0)
chars_written = str_format(buffer + buffer_pos, buffer_size - buffer_pos, " %-32s - %s\n", long_name, temp_option->description);
else
chars_written = str_format( buffer + buffer_pos, buffer_size - buffer_pos, "-%c %-32s - %s\n", temp_option->name_short, long_name, temp_option->description);
if(chars_written < 0)
return buffer;
buffer_pos += size_t(chars_written);
}
return buffer;
}
/**
* @file arclite_getopt.hpp
* @author Jacob I Tate
* @brief Simple getopt style command line parser with short and long options
*/
#pragma once
#include <stddef.h>
#include <stdint.h>
#include <exception>
#include <string>
/**
* Macro to define the end element in the options array
*/
#define ARCLITE_GETOPT_END { 0, 0, ARCLITE_GETOPT_TYPE_NO_ARGS, 0, 0, 0, 0 }
/**
* Arg types
*/
typedef enum : uint8_t
{
ARCLITE_GETOPT_TYPE_NO_ARGS, ///< The option doesnt have arguments
ARCLITE_GETOPT_TYPE_REQUIRED, ///< The option requires an argument
ARCLITE_GETOPT_TYPE_OPTIONAL, ///< The option argument is optional
ARCLITE_GETOPT_TYPE_FLAG_SET, ///< The option is a flag or val that is to be set
ARCLITE_GETOPT_TYPE_FLAG_AND, ///< The option is a flag or val that is to be and'd
ARCLITE_GETOPT_TYPE_FLAG_OR ///< The option is a flag or val that is to be or'd
} arclite_getopt_type_t;
/**
* Get opt option for creating the actual options
*/
typedef struct
{
const char* name; ///< The long name of the argument
int name_short; ///< The short name of the argument
arclite_getopt_type_t type; ///< Type of the option @sa @ref arclite_getopt_type_t
int* flag; ///< Pointer to the flag to set, only effective if a flag op is set
int value; ///< If flag-type this value will be set/and'd/or'd to the flag, else it will be returned from arclite_getopt
const char* description; ///< Description of the option
const char* value_description; ///< Short description of the valid values Used for auto help generation "--my_option=<value_description_goes_here>"
} arclite_getopt_option_t;
/**
* Exception which will catch someone inputting
* ! ? + -1
* As the short option
*/
struct arclite_getopt_invalid_shortopt : public std::exception
{
/**
* Constructs an arclite_getopt_invalid_shortopt object
* @param exception The exception message
*/
explicit arclite_getopt_invalid_shortopt(const std::string& exception);
/**
* What the error is
* @return The error message
*/
const char* what() const noexcept override;
private:
const std::string& exception_; ///< The exception string
};
/**
* Get opt class
*/
class arclite_getopt
{
public:
/**
* @brief Constructs a getopt object
*
* @param argc argc from `int main(int argc, char** argv)` or equivalent value
* @param argv argv from `int main(int argc, char** argv)` or equivalent value
* @param options Pointer to array with options that should be checked for
*
* @sa @ref arclite_getopt::next
* @since Version 1.1.0
*/
arclite_getopt(int argc, const char** argv, const arclite_getopt_option_t* options);
/**
* Parses the argc/argv given at @ref arclite_getopt ctor. This will attempt to
* parse the next token in the menu context and return an id given the result of
* the parsing
*
* @return
* - '!' on error. The flag name will be stored in @ref arclite_getopt::current_opt_arg.
* This can be caused by missing arguments on a required argument or Argument being found
* when none are expected.
* - '?' if item was an unrecognized option, @ref arclite_getopt::current_opt_arg will be set to the item
* - '+' if item was no option, @ref arclite_getopt::current_opt_arg will be set to item
* - '0' if the opt was a flag and it was set. @ref arclite_getopt::current_opt_arg will be set to flag-name
* - -1 if there are no more items to request
*/
int next();
/**
* Retrieves the current options argument assuming it has one. In the case it doesnt
* a nullptr will be returned.
*
* @return The option argument if one is to be found, nullptr otherwise
*/
const char* current_opt_arg();
/**
* Builds a help string based on the values in the original @a arclite_getopt_option_t
*
* @param buffer The buffer to place the help string in
* @param buffer_size The size of the buffer
*
* @return The help string
*/
const char* create_help_string(char* buffer, size_t buffer_size);
private:
/**
* <b>For internal use<\b>
*
* Context for use while parsing the objects must be initialized by @ref arclite_getopt_create_context.
* In the case this is reused reinitialization is completed via @ref arclite_getopt_create_context again.
*/
typedef struct
{
int argc; ///< Argc from main
const char** argv; ///< Argv from main
const arclite_getopt_option_t* options; ///< Command options
int num_options; ///< Number of options in the command options array
int current_index; ///< Current index in the iteration
const char* current_opt_arg; ///< Used to return values to @ref arclite_getopt_next
} arclite_getopt_context_t;
arclite_getopt_context_t context_;
};
#include "arclite_getopt.h"
static const arclite_getopt_option_t option_list[] =
{
{ "help", 'h', ARCLITE_GETOPT_TYPE_NO_ARGS, nullptr, 'h', "print this help text", nullptr },
{ "version", 'v', ARCLITE_GETOPT_TYPE_NO_ARGS, nullptr, 'v', "prints the version text", nullptr },
ARCLITE_GETOPT_END
};
int main(int argc,const char** argv)
{
arclite_getopt context(argc, argv, option_list);
int opt;
while((opt = context.next()) != -1)
{
switch(opt)
{
case 'v':
{
//std::cout << "Version: " << Version::current().asLongStr() << std::endl;
return 0;
}
case 'h':
{
char buffer[2048];
printf( "%s\n", context.create_help_string(buffer, sizeof( buffer ) ) );
return 0;
}
default:
{
break;
}
}
}
return 0;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment