Simple interactive menu

deceptikon 3 Tallied Votes 571 Views Share

I was going to post this in a thread, but it turned into something a bit more serious and may be useful as a code snippet.

The code shows three files:

  • menu.h: The header file for the menu library.
  • menu.c: Implementation of the menu library functions.
  • main.c: A sample driver that uses the library.

The menu library is fairly generic in that you can provide a format string of menu options and possible matching input values. See main.c for an example of how to use it.

/* menu.h */
#ifndef MENU_H
#define MENU_H

#define PROMPT_SIZ 128
#define ALT_SIZ    128
#define MENU_ALTS  11

typedef struct menuitem {
    char alt[MENU_ALTS][ALT_SIZ];
    char prompt[PROMPT_SIZ];
} menuitem_t;

#ifdef __cplusplus
extern "C" {
#endif

extern int menu(menuitem_t **items, char *selection, size_t limit);
extern menuitem_t **extract_menu_items(const char *options);
extern void destroy_menu_items(menuitem_t **items);

#ifdef __cplusplus
}
#endif

#endif

/* menu.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "menu.h"

/*
    @description:
        Displays the prompts for a collection of menu items and gathers an interactive selection.

    @returns: True if a matching selection was detected, false otherwise.
*/
extern int menu(menuitem_t **items, char *selection, size_t limit)
{
    size_t i, j;

    if (items) {
        /* Display the prompts */
        for (i = 0; items[i]; i++)
            puts(items[i]->prompt);

        fputs("> ", stdout);
        fflush(stdout);
    
        /* Process an interactive selection */
        if (fgets(selection, limit, stdin)) {
            char *newline = strchr(selection, '\n');

            if (newline)
                *newline = '\0'; /* Trim the newline for convenience */
            else {
                int ch;

                /* Discard extraneous line characters */
                do
                    ch = fgetc(stdin);
                while (ch != '\n' && ch != EOF);

                clearerr(stdin);
            }

            for (i = 0; items[i]; i++) {
                /* Check all of the alternatives for a matching prefix */
                for (j = 0; j < MENU_ALTS && items[i]->alt[j][0]; j++) {
                    size_t alt_len = strlen(items[i]->alt[j]);

                    /* If the first part of the string matches an alternative, it's valid */
                    if (strncmp(selection, items[i]->alt[j], alt_len) == 0)
                        return 1;
                }
            }
        }
    }

    return 0; /* No matches, end-of-file, or stream error */
}

/*
    @description:
        Parses and extracts a collection of menu items from a formatted options string.

        The options string shall be in the following format:
            
            <prompt1>;<prompt1.match1>|<prompt1.match2>;<prompt2>;<prompt2.match1>|<prompt2.match2>...

        The prompt may not be an empty field, but the match strings may be empty.

        Example 1 (prompts and matches):    "A) Option A;A|a;B) Option B;B|b;Q) Quit;Q|q"
        Example 2 (prompts and no matches): "A) Option A;;B) Option B;;Q) Quit;"

    @remarks:
        Memory is dynamically allocated for the item collection. destroy_menu_items
        is provided as a convenience function for releasing this memory.
*/
extern menuitem_t **extract_menu_items(const char *options)
{
    menuitem_t **mem;

    /* Always allocate at least one spot for a trailing null pointer */
    mem = (menuitem_t**)malloc(sizeof *mem);

    if (mem) {
        char copy[BUFSIZ] = "";
        size_t args = 0;
        char *tok;

        strcat(copy, options);
        tok = strtok(copy, ";");

        while (tok) {
            size_t tok_len = strlen(tok);
            menuitem_t **temp;

            mem[args] = (menuitem_t*)malloc(sizeof *mem[args]);

            if (!mem[args])
                break;

            /* Assume the first hit is a prompt */
            strcpy(mem[args]->prompt, tok);

            /* Assume the second hit is the alt list */
            if (tok[tok_len + 1] == ';') {
                /*
                    Kind of tricky. Introspectively looked ahead because strtok won't,
                    and handle an empty alt list field by setting the first alt to "".
                */
                mem[args]->alt[0][0] = '\0';
            }
            else {
                char *alt_tok;
                size_t n = 0;

                /* Not an empty alt list, so we can grab the token with strtok */
                tok = strtok(NULL, ";");
                tok_len = strlen(tok);

                /*
                    Now reuse strtok to process the alt list (awkward).
                */
                alt_tok = strtok(tok, "|");

                while (n < MENU_ALTS - 1 && alt_tok) {
                    strcpy(mem[args]->alt[n++], alt_tok);
                    alt_tok = strtok(NULL, "|");
                }

                /* Make sure to terminate the alt list with a blank string */
                mem[args]->alt[n][0] = '\0';
            }

            /* Make room for the next menu item (or null pointer if we're done) */
            temp = (menuitem_t**)realloc(mem, (++args + 1) * sizeof *temp);

            if (!temp)
                break;

            mem = temp;

            /*
                Restart strtok at the next option token because we 
                may have used strtok to handle a non-empty alt list.
            */
            tok = strtok(tok + tok_len + 1, ";");
        }

        mem[args] = NULL;
    }

    return mem;
}

/*
    @description:
        Releases memory for a collection of menu items returned by extract_menu_items.
*/
extern void destroy_menu_items(menuitem_t **items)
{
    size_t i;

    for (i = 0; items[i]; i++)
        free(items[i]);

    free(items);
}

/* main.c (sample usage) */
#include <ctype.h>
#include <stdio.h>
#include "menu.h"

int main(void)
{
    menuitem_t **items = extract_menu_items("A) Option A;A|a;B) Option B;B|b;Q) Quit;Q|q");
    int done = 0;
    
    while (!done) {
        char selection[BUFSIZ];
        int rc = menu(items, selection, sizeof selection);

        printf("Full selection: '%s'\n", selection);

        if (!rc)
            puts("Invalid selection");
        else {
            switch (toupper(selection[0])) {
            case 'A':
                puts("You chose option A");
                break;
            case 'B':
                puts("You chose option B");
                break;
            case 'Q':
                puts("You chose to quit");
                done = 1;
                break;
            }
        }
    }

    destroy_menu_items(items);

    return 0;
}
MareoRaft 0 Junior Poster in Training

I am not sure I know enough to really understand your code. I am trying to learn Objective-C. One thing I noticed is common is that in the creation of a class, it is done in a header file like so...

FILE 1 | class.h | declares the class, the variables it holds, and the methods we can use on it
FILE 2 | class.m | defines the methods that were declared in class.h
FILE 3 | main.m | an actual program which has "#import "class.h" and then uses the class

However, the schema did not work for me. The only way I got my class to work was to combine the code in class.h and class.m into one big class.h file. Is there something I'm missing here? It actually makes sense to me that the 3 file scheme would NOT work because when is class.m imported into main? It's not.

deceptikon 1,790 Code Sniper Team Colleague Featured Poster

Is there something I'm missing here?

Unfortunately, I'm not familiar with Objective-C, so your best bet would be to post your current code to the Objective-C forum so that the experts there can help out.

Be a part of the DaniWeb community

We're a friendly, industry-focused community of developers, IT pros, digital marketers, and technology enthusiasts meeting, networking, learning, and sharing knowledge.