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 ofOPT_H_FLAG,OPT_H_INT, orOPT_H_STR)a pointer to the variable created to store the value of the option in
OPT_flag_valuesa pointer to the
OPT_has_##varnamevariable used to detect whether options were found inOPT_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 typeis created by expanding one ofFLAG,INT, orSTRtoOPT_H_FLAG,OPT_H_INT, orOPT_H_STR. This value is how the data type of the registered option is known.char flagis the literal character passed toADD_OPTION().int* has_data_flagis a pointer to theOPT_has_##varnamevariable that indicates the presence of a flag on the command line input.void* datais a pointer to the created variable that is saved inOPT_flag_valuesso thatEND_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;
}