jacob-tate
12/4/2019 - 6:15 AM

Getopt in C++

/**
 * @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_;
};
/**
 * @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;
}
#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;
}