5
\$\begingroup\$

Proper input handling is a pain in the butt in C, so as part of a project of mine, this header is used to handle reoccurring command line input tasks such as input loops, reading lines and tokenizing strings in a safe and robust way. Would be great if some C veterans could share their opinion about potential flaws.

You can check the updated version here.

#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <limits.h>
#include <stdint.h>
#include <stdio.h>

#define TRUE 1


/*
 * Contains functions for input related operations
 */


/// Wraps fgets to handle errors and safely replaces the newline
/// \param line - The buffer which holds the string
/// \param length - The size of the buffer
void get_line(char *line, const int length) {
    if (fgets(line, length, stdin)) {
        char *newline = strchr(line, '\n');
        if (newline) {
            *newline = '\0';
        }
    } else {
        fprintf(stderr, "Could not read line from stdin!");
        exit(EXIT_FAILURE);
    }
}


/// Tokenizes a line into EXACTLY number_of_tokens tokens using the " " delimiter. Excess tokens are omitted. This
/// function will alter the line parameter, so be sure to pass a copy if you want to preserve it.
/// \param line - The string to be processed
/// \param tokens - The buffer holding the tokens
/// \return EXIT_FAILURE if the supplied line does not contain enough tokens.
int get_tokens(char *line, char **tokens, const size_t number_of_tokens) {
    for (size_t i = 0; i < number_of_tokens; ++i) {
        char *token = strtok(line, " ");
        if (token) {
            tokens[i] = token;
            line = NULL;
        } else {
            // If token is NULL that means not enough tokens found
            //fprintf(stderr, "Not enough tokens!\n");
            return EXIT_FAILURE;
        }
    }
    return EXIT_SUCCESS;
}


/// Tokenizes a line into UP TO the specified number of tokens using the " " delimiter. Excess tokens are omitted. This
/// function will alter the line parameter, so be sure to pass a copy if you want to preserve it.
/// You will have to check if each token is NULL before accessing them!
/// \param line - The string to be processed
/// \param tokens - The buffer holding the tokens
void get_tokens_optional(char *line, char **tokens, const size_t number_of_tokens) {
    for (size_t i = 0; i < number_of_tokens; ++i) {
        char *token = strtok(line, " ");
        tokens[i] = token;
        line = NULL;
    }
}


/// Not using atoi() since it can't detect errors (ERR34-C)
/// \param string - The string to be interpreted
/// \param integer - A pointer to the returned int value
/// \return EXIT_FAILURE if the string value is out of range or some error occurs
int string_to_integer(char *string, int *integer) {
    char *p;
    const long value = strtol(string, &p, 10);

    if (p == string) {
        fprintf(stderr, "%s is not a decimal number\n", string);
    } else if (*p != '\0') {
        fprintf(stderr, "%s has extra characters at end of input: %s\n", string, p);
    } else if ((LONG_MIN == value || LONG_MAX == value) && ERANGE == errno) {
        fprintf(stderr, "%s is out of range of type int\n", string);
    } else if (value > INT_MAX) {
        fprintf(stderr, "%ld is greater than INT_MAX\n", value);
    } else if (value < INT_MIN) {
        fprintf(stderr, "%ld is less than INT_MIN\n", value);
    } else {
        *integer = (int) value;
        return EXIT_SUCCESS;
    }
    return EXIT_FAILURE;
}


/// Converts a string to a size_t
/// \param string - The string to be interpreted
/// \param size_t - A pointer to the returned size_t value
/// \return EXIT_FAILURE if the string value is out of range or some error occurs
int string_to_size_t(char *string, size_t *size) {
    char *p;
    const long value = strtol(string, &p, 10);

    if (p == string) {
        fprintf(stderr, "%s is not a decimal number\n", string);
    } else if (*p != '\0') {
        fprintf(stderr, "%s has extra characters at end of input: %s\n", string, p);
    } else if ((LONG_MIN == value || LONG_MAX == value) && ERANGE == errno) {
        fprintf(stderr, "%s is out of range of type size_t\n", string);
    } else if (value < 0) {
        fprintf(stderr, "%ld is less than 0\n", value);
    } else if ((size_t) value > SIZE_MAX) {
        fprintf(stderr, "%ld is greater than SIZE_MAX\n", value);
    } else {
        *size = (size_t) value;
        return EXIT_SUCCESS;
    }
    return EXIT_FAILURE;
}


/// Prompts for an integer value once
/// \param value - A pointer to the integer
/// \return EXIT_FAILURE if the supplied input doesn't match
int get_integer(int *value) {
    char buffer[BUFSIZ];
    get_line(buffer, BUFSIZ);

    char **tokens = malloc(sizeof(char*));
    if (get_tokens(buffer, tokens, 1)) {
        return EXIT_FAILURE;
    }

    int result;
    if (string_to_integer(tokens[0], &result)) {
        return EXIT_FAILURE;
    }

    *value = result;
    free(tokens);
    return EXIT_SUCCESS;
}


/// Prompts for a size_t value once
/// \param value - A pointer to the size_t
/// \return EXIT_FAILURE if the supplied input doesn't match
int get_size_t(size_t *value) {
    char buffer[BUFSIZ];
    get_line(buffer, BUFSIZ);

    char **tokens = malloc(sizeof(char*));
    if (get_tokens(buffer, tokens, 1)) {
        return EXIT_FAILURE;
    }

    size_t result;
    if (string_to_size_t(tokens[0], &result)) {
        return EXIT_FAILURE;
    }

    *value = result;
    free(tokens);
    return EXIT_SUCCESS;
}


/// Prompts for a size_t until a valid value has been entered
/// \return The size_t value
size_t prompt_for_size_t() {
    size_t result;
    while (get_size_t(&result));
    return result;
}


/// Prompts for an integer until a valid value has been entered
/// \return The int value
int prompt_for_int() {
    int result;
    while (get_integer(&result));
    return result;
}


/// Prompts for at least number_of_tokens tokens until valid tokens have been entered
/// \param line - The buffer which holds the string
/// \param tokens - The buffer holding the tokens
void prompt_for_tokens(char *line, char**tokens, const size_t number_of_tokens) {
    do {
        get_line(line, BUFSIZ);
    } while (get_tokens(line, tokens, number_of_tokens));
}
\$\endgroup\$
  • \$\begingroup\$ Take a look at the readline library if you want inspiration, it has similar goals to your project :) \$\endgroup\$ – jacwah Nov 13 '16 at 14:51
3
\$\begingroup\$

I see some things that I think could help you improve your code.

Eliminate unused definitions

The top of the code includes this line:

#define TRUE 1

However, it's not used within the code and should be removed. Also, rather than defining your own values for TRUE and FALSE, use #include <stdbool.h> instead.

Don't rely on implementation-defined values

It seems that you are using EXIT_FAILURE and EXIT_SUCCESS as return values for your functions. This is an error because the value of these constants are implementation defined. This means that lines like this:

if (string_to_integer(tokens[0], &result)) {

May or may not work as you intend because the value of EXIT_SUCCESS is not required to be 0. In short, don't use EXIT_FAILURE and EXIT_SUCCESS except perhaps as argument to exit().

Don't allocate memory if you don't need to

This code calls malloc several places but doesn't always match each one with a call to free. This means that the routines are leaking memory. For instance, the existing implementation of get_integer looks like this:

int get_integer(int *value) {
    char buffer[BUFSIZ];
    get_line(buffer, BUFSIZ);

    char **tokens = malloc(sizeof(char*));
    if (get_tokens(buffer, tokens, 1)) {
        return EXIT_FAILURE;
    }

    int result;
    if (string_to_integer(tokens[0], &result)) {
        return EXIT_FAILURE;
    }

    *value = result;
    free(tokens);
    return EXIT_SUCCESS;
}

The problem, however, is that if the call to malloc succeeds, but the call to string_to_integer fails, the allocated memory is not freed and worse, the pointer to it is lost. There's no need to call malloc here, though as shown in the next suggestion.

Don't allocate memory when you don't need to

The memory leak shown in the previous suggestion can easily be addressed by simply not explicitly allocating memory. One way to rewrite this code would be this:

int get_integer(int *value) {
    char buffer[BUFSIZ];
    get_line(buffer, BUFSIZ);
    char *tokens;
    if (get_tokens(buffer, &tokens, 1)) {
        return 1;
    }
    int result;
    if (string_to_integer(tokens, &result)) {
        return 2;
    }
    *value = result;
    return 0;
}

This also uses different constants 1 and 2 so that the calling code can determine which error occurred. In real code, one would use named constants. It appears that the code has taken care to only alter value if the input succeeds. If this is not important, one can read the value directly into value.

Use const where practical

The string passed to string_to_size_t and to string_to_integer is not modified by the function, so it should be declared as const:

int string_to_size_t(const char *string, size_t *size);

Think of the user

The error messages might make sense to a C programmer, but are not particularly helpful otherwise. For example, this line:

fprintf(stderr, "%ld is greater than INT_MAX\n", value);

Seems to assume that the user knows what INT_MAX is and what value it has, which are both unlikely to be true for those who aren't C programmers familiar with your particular compiler.

Choose better names

By the name prompt_for_size_t and prompt_for_integer, I would expect them to actually provide a prompt, but they actually don't. What they do instead is only provide some message if there is some flaw detected in the input. Also size_t names a type, but integer does not, so they're not consistent. You might consider passing in a const char * string so that it could actually be used as a prompt. Then one could use it like this:

prompt_for_size_t("Please enter a positive integer");

Reconsider the interface

Some of the functions print to stderr if they encounter an error and some do not. I'd suggest that it might be better and more consistent to have most functions simply return an error code and let the calling function provide any prompts, if needed. If you find that you are often using the same messages for certain kinds of input, you can create wrapper functions such as string_to_size_t to handle that. It might also be useful to be able to ask for an integer in a particular range, so an interface passing in low and/or high values might be handy.

\$\endgroup\$
  • \$\begingroup\$ Thanks! I didn't know that exit values were implementation-defined. Some seem to suggest using EXIT_XXX instead tho. Since portability is not an issue here, I will use 0/1 tho. \$\endgroup\$ – AdHominem Nov 13 '16 at 15:56
  • \$\begingroup\$ wow thanks man :) this actually helped me out too. Would it be considered bad practice btw to set errno to EINVAL for the greater than INT_MAX error? \$\endgroup\$ – Corvus Crypto Nov 13 '16 at 20:10

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.