3
\$\begingroup\$

I'm a computer science student, and in 2 of my courses this semester we are writing short C programs to demonstrate the things we are learning about. All of these programs require command-line flags & arguments, so I decided to write a short single-header library for myself to be able to add simple flags, integer, and string arguments without having to write the same getopt patterns over and over again. It's mostly a toy thing but I'm using it as a learning experience :).


Header Component

Included in all files that include "opt.h".

/*
 * Jake Grossman <[email protected]>
 * This is free software released in the public domain.
 * See LICENSE.txt for more information.
 */
#ifndef opt_h
#define opt_h

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <getopt.h>

These are the definitions for the library:

// typedefs to help emulate template-like behavior in ADD_OPTION
typedef int OPT_INT;
typedef int OPT_FLAG;
typedef char* OPT_STR;

// start an options block with a given argc and argv
#define START_OPTIONS(argc, argv) OPT_start_options(argc, argv);

// add an option
// type is one of FLAG, INT, or STR
#define ADD_OPTION(type, flag, varname) \
    OPT_##type varname = 0; \
    int OPT_has_##varname = 0; \
    OPT_add_option(OPT_H_##type, flag, &OPT_has_##varname, &varname);

// end the option block
#define END_OPTIONS() OPT_process_options();

#define OPT_H_NONE 0 // flag is not defined
#define OPT_H_FLAG 1 // flag has no argument
#define OPT_H_INT  2 // flag has an integer argument
#define OPT_H_STR  3 // flag has a string argument

void
OPT_start_options(int argc, char** argv);

void
OPT_add_option(int type, char flag, int* has_data_flag, void* data);

void
OPT_process_options();

#endif // end header file

These are the public facing components of the API. The START_OPTIONS() macro is used to setup the variables used with getopt. ADD_OPTION() registers a single character option (e.g., -v) and creates associated variables. END_OPTIONS() uses getopt and the options registered with ADD_OPTION() to populate the create variables. Each is explained in more detail with it's implementation.


Implementation

Included in a single file where OPT_H_IMPLEMENTATION is defined.

This is exactly like the stb public domain/MIT single-file libraries.

#ifdef OPT_H_IMPLEMENTATION

char   OPT_flags[26] = { OPT_H_NONE }; // list of declared flags (a-z only)
int    OPT_num_flags = 0; // number of flags currently added in option block
void*  OPT_flag_values[26] = { NULL }; // pointers to flag value variables
int*   OPT_found_flags[26] = { NULL }; // pointers to `OPT_has_varname` flags

// which argc/argv to use for getopt
// NOTE: this has all the side
// effects for argv that getopt
// normally does
int OPT_argc;
char** OPT_argv;

// flag indicating whether we are in an options block
int OPT_in_progress = 0;

When an option is declared with ADD_OPTION(), it registers:

  • the type of option into OPT_flags (one of OPT_H_FLAG, OPT_H_INT, or OPT_H_STR)

  • a pointer to the variable created to store the value of the option in OPT_flag_values

  • a pointer to the OPT_has_##varname variable used to detect whether options were found in OPT_found_flags

as well as incrementing OPT_num_flags.

OPT_argc and OPT_argv are set by START_OPTIONS() to allow the usage of an arbitrary argc and argv.


// cleanup any leftover data and prepare to start reading options
void
OPT_start_options(int argc, char** argv)
{
    // clear all existing data
    for (int i = 0; i < 26; i++) {
        OPT_flags[i] = OPT_H_NONE;
        OPT_flag_values[i] = NULL;
        OPT_in_progress = 0;
    }

    // save argument count and string array pointer
    OPT_argc = argc;
    OPT_argv = argv;

    OPT_in_progress = 1;
}

This resets all the of stored flag data, but does not make any attempt to clean up allocated memory, since it may be possible that the user wants to keep using a variable after starting a new options block. So, the user is responsible for their own strings.

void
OPT_add_option(int type, char flag, int* has_data_flag, void* data)
{
    if (!OPT_in_progress) {
        fprintf(stderr, "OPT_add_option(): called outside of an options block\n");
        exit(EXIT_FAILURE);
    }

    // convert opt character to alphabet index
    int index = (int)flag % 32;

    // check that this flag has not been seen before
    if (OPT_flags[index] != OPT_H_NONE) {
        fprintf(stderr, "OPT_add_option(): Duplicate declaration -- '%c'\n", flag);
        exit(EXIT_FAILURE);
    }

    // save pointer to data variable
    if (data) {
        if (!type == OPT_H_INT && !type == OPT_H_STR && !type == OPT_H_FLAG) {
            fprintf(stderr, "OPT_add_option(): Unexpected type value: %d\n", type);
            exit(EXIT_FAILURE);
        }

        // save pointer to data variable
        OPT_flag_values[index] = data;
    }

    // store pointer to indicator flag variable
    OPT_found_flags[index] = has_data_flag;
    OPT_num_flags++;

    // assign appropriate value to flags field
    OPT_flags[index] = type;
}
  • int type is created by expanding one of FLAG, INT, or STR to OPT_H_FLAG, OPT_H_INT, or OPT_H_STR. This value is how the data type of the registered option is known.

  • char flag is the literal character passed to ADD_OPTION().

  • int* has_data_flag is a pointer to the OPT_has_##varname variable that indicates the presence of a flag on the command line input.

  • void* data is a pointer to the created variable that is saved in OPT_flag_values so that END_OPTIONS() knows where to store the data

After checking that OPT_in_progress is set and that this flag has not been declared before, the information about the command is registered in the appropriate table.

// read and process arguments that the user has specified
void
OPT_process_options()
{
    int opt;
    char* arg_string;

    // eagerly allocate the maximum size
    // (2 characters for each flag, 'a:')
    arg_string = malloc(OPT_num_flags * 2 + 1);
    if (!arg_string) {
        perror("OPT_process_options");
        exit(EXIT_FAILURE);
    }
    memset(arg_string, 0, OPT_num_flags*2+1);

    char* ptr = arg_string;
    for (size_t i = 0; i < sizeof(OPT_flags); i++) {
        int type = OPT_flags[i];

        if (type == OPT_H_NONE) // flag not defined
            continue;

        *(ptr++) = i + 'a' - 1; // translate alphabet index to lowercase letter

        if (type == OPT_H_INT || type == OPT_H_STR)
            *(ptr++) = ':'; // indicate required value
    }

    // process argument string
    while ((opt = getopt(OPT_argc, OPT_argv, arg_string)) != -1) {
        int index = (int)opt % 32; // convert opt character to alphabet index
        int type = OPT_flags[index];
        switch (type) {
            case OPT_H_INT: {
                // integer flag
                int* ptr = (int*)OPT_flag_values[index];

                *ptr = atoi(optarg);
                break;
            }
            case OPT_H_STR: {
                // string flag
                char** ptr = (char**)OPT_flag_values[index];

                *ptr = malloc(strlen(optarg)+1);

                if (!(*ptr)) {
                    perror("OPT_process_options");
                    exit(EXIT_FAILURE);
                }

                strcpy(*ptr, optarg);
                break;
            }
            case OPT_H_FLAG: {
                // regular flag (no data)
                int* ptr = (int*)OPT_flag_values[index];

                *ptr = 1;
                break;
            }
            default: {
                // getopt will have already printed an error on stderr
                exit(EXIT_FAILURE);
            }
        }

        // successfully processed flag
        if (OPT_found_flags[index])
            *(OPT_found_flags[index]) = 1;
    }

    free(arg_string);
    OPT_in_progress = 0;
}

#endif

This first uses the registered information from OPT_flags to create an option string for getopt. It uses OPT_num_flags to eagerly allocate enough memory in the string for every option to have a required argument. So, if a, i, c, and v are registered options, it will allocate enough space for the longest possible optstring "a:i:c:v:". Then, it constructs the string by assuming flag variables have no arguments, while integers and strings do ("a" v.s. "a:"). Then, it will repeatedly call getopt, using the character value returned as an index to the flag data by converting it to it's alphabetical index (a -> 0, b->1, ..., z->25).

This way, it is able to switch over the value stored in OPT_flags to appropriately cast the void pointer to the correct type to store in the created variable.


I'm not too convinced that there isn't a better way to achieve the generic-ish effect from ADD_OPTION. previous, similar implementation used three separate macros to achieve the same goal without typedef.

// add a flag option
#define ADD_FLAG_OPT(flag, varname) \
    int varname = 0; \
    int OPT_has_##varname = 0; \
    OPT_add_option(OPT_H_FLAG, flag, no_argument, &varname, NULL);

// add an integer option
#define ADD_INT_OPT(flag, varname) \
    int varname = 0; \
    int OPT_has_##varname = 0; \
    OPT_add_option(OPT_H_INT, flag, required_argument, &OPT_has_##varname, &varname);

// add a string option
#define ADD_STR_OPT(flag, varname) \
    char* varname = NULL; \
    int OPT_has_##varname = 0; \
    OPT_add_option(OPT_H_STR, flag, required_argument, &OPT_has_##varname, &varname);

I didn't like how each variant was nearly identical, which is why I changed the interface, but something about the wierd typedef'ing makes me thing I'm using an ineffective solution for this.


Usage

This minimal example shows the complete usage:

#include <stdio.h>

#define OPT_H_IMPLEMENTATION
#include "opt.h"

int
main(int argc, char** argv)
{
    START_OPTIONS(argc, argv);
    ADD_OPTION(FLAG, 'v', is_verbose);
    ADD_OPTION(INT, 't', time_to_live);
    ADD_OPTION(STR, 'i', input_filename);
    END_OPTIONS();

    printf("Verbose: %d\n", is_verbose);

    if (OPT_has_time_to_live) {
      printf("Time To Live: %d\n", time_to_live);
    }

    if (OPT_has_input_filename) {
        printf("Input Filename: %s\n", input_filename);
        free(input_filename); // free memory malloced by opt.h
    }

    return 0;
}
\$\endgroup\$
0

0

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.