Scott M. Mcdermott

UNIX Systems & Network Administrator
available for contract or salaried positions

command.c

#define _GNU_SOURCE

#include <stdlib.h>
#include <stdio.h>
#include <assert.h>
#include <wordexp.h>

#include <readline/readline.h>
#include <readline/history.h>

#include <dbllist.h>
#include <safe.h>
#include <log.h>

#include "string.h"
#include "command.h"

#define MAX_CMD_LEN     256

bool libcommand_use_as_many_as_unique;
char *libcommand_eof_command;
char *libcommand_default_command;

/*
 * this is called for each completion attempt: first with state == 0, then
 * repeatedly until no more matches are possible, with state == 1.
 */
static char *
command_completer (const char *text, int state)
{
        static int n;
        char *cmdname;

        if (state == 0)
                n = 0;
        while ((cmdname = commands[n++].identifier))
                if (!strncmp(text, cmdname, strlen(text)))
                        return xstrdup(cmdname);
        return NULL;
}

static char *
get_command (const char *prompt)
{
        static char *gotline;
        char **m;
        int mlen;

        /* every time readline() is invoked it gives us a new buffer */
        if (gotline) {
                free(gotline);
                gotline = NULL;
        }

        if (libcommand_use_as_many_as_unique) {
                rl_num_chars_to_read = 1;
                rl_completion_append_character = '\0';
                rl_completion_entry_function = command_completer;
        } else {
                rl_num_chars_to_read = 0;
                rl_completion_entry_function = command_completer;
        }

        while ((gotline = readline(prompt))) {

                /* enter/return given */
                if (!*gotline)
                        return NULL;

                if (contains_only_whitespace(gotline))
                        return NULL;

                if (libcommand_use_as_many_as_unique) {
                        m = rl_completion_matches(gotline, command_completer);
                        if (!m)
                                /* no matches at all, but return it so we can
                                 * try to execute and fail, so the user will
                                 * know and won't just get another prompt */
                                return gotline;

                        /* one or more matches; do a complete on it */
                        rl_complete(0, '!');
                        /* rl_redisplay(); */

                        /* how many exactly? */
                        for (mlen = 0; m[mlen]; mlen++);

                        /* do we have the exact match? */
                        if (mlen == 1) {
                                /* yep, return it */
                                gotline = xxrealloc(gotline, rl_end + 1);
                                strncpy(gotline, rl_line_buffer, rl_end);
                                *(gotline + rl_end) = '\0';
                                return gotline;
                        }

                        /* no, there are multiple matches */
                        continue;
                }
                add_history(gotline);
                return gotline;
        }

        /* terminal EOF (ctrl-d) given, just map to the quit action */
        if (!gotline) {
                if (libcommand_eof_command)
                        return xstrdup(libcommand_eof_command);
                else
                        return NULL;
        }

        if (libcommand_default_command)
                return xstrdup(libcommand_default_command);
        else
                return NULL;
}

static char *
check_token (char *p)
{
        unsigned c;

        if (strnlen(p, MAX_CMD_LEN) > MAX_CMD_LEN) {
                xlog(0, "token exceeds maximum characters\n");
                return NULL;
        }
        while ((c = *p++)) {
                if (!isprint(c)) {
                        xlog(0, "token contains non-glyph characters\n");
                        return NULL;
                }
        }
        return p;
}

static command_t *
lookup_command (char *p)
{
        int n;  /* offset into the commands array */
        char *id;

        for (n = 0; (id = commands[n].identifier); n++)
                if (!strcmp(id, p))
                        return &commands[n];

        return NULL;
}

/*
 * returns NULL on error
 * XXX verify that
 * XXX verify callers
 */
static command_t *
make_command (char *cmdline)
{
        int n, err;
        char *tok;
        list_head_t *list;
        command_t *docmd, *command;
        static wordexp_t words;

        assert(cmdline);
        assert(*cmdline);

        err = wordexp(cmdline, &words, WRDE_REUSE|WRDE_SHOWERR|WRDE_UNDEF);
        if (err) {
                xlog(0, "wordexp error with input string\n");
                switch (err) {
                case WRDE_BADCHAR:
                        xlog(0, "bad char "
                                "WRDE_BADCHAR\n");
                        break;
                case WRDE_BADVAL:
                        xlog(0, "undef'd shell var forbidden "
                                "WRDE_BADVAL\n");
                        break;
                case WRDE_CMDSUB:
                        xlog(0, "command substitution forbidden "
                                "WRDE_CMDSUB\n");
                        break;
                case WRDE_NOSPACE:
                        xlog(0, "alloc error, partial result "
                                "WRDE_NOSPACE\n");
                        break;
                case WRDE_SYNTAX:
                        xlog(0, "syntax error "
                                "WRDE_SYNTAX\n");
                        break;
                default:
                        xlog(0, "impossible default error from "
                                "wordexp()\n");
                        break;
                }
                return NULL;
        }

        tok = words.we_wordv[0];

        if (!check_token(tok)) {
                xlog(0, "%s: command name is not sane\n", __FUNCTION__);
                return NULL;
        }
        if (!(command = lookup_command(tok))) {
                xlog(0, "%s: command is not registered\n", __FUNCTION__);
                return NULL;
        }
        docmd = xmalloc(sizeof(command_t));
        memcpy(docmd, command, sizeof(command_t));

        list = list_create();
        for (n = 1; n < words.we_wordc; n++) {
                /* XXX do we really require this additional
                 * check? it's limiting */
                tok = words.we_wordv[n];
                if (!check_token(tok)) {
                        xlog(0, "invalid argument token\n");
                        return NULL;
                }
                list_add(list, strdup(tok));
        }
        docmd->arguments = list;

        return docmd;
}

static bool
free_arg (list_node_t *list)
{
        assert(list);
        assert(list->item);

        free(list->item);

        return true;
}

static bool
do_command (command_t *docmd)
{
        unsigned ret, errors;

        ret = docmd->callback(docmd->arguments);
        errors = list_iterate_all(docmd->arguments, free_arg);
        list_destroy(docmd->arguments);

        if (ret == false || errors == false )
                return false;

        return true;
}

/*
 * maybe this command should return status and let the
 * caller decide if it should exit?
 */
void
command_loop (const char *prompt, int maxchars)
{
        char *got;
        command_t *command;

        for (;;) {
                if (!(got = get_command(prompt)))
                        continue;
                if (!(command = make_command(got))) {
                        xlog(0, "%s: %s: invalid command\n", __FUNCTION__, got);
                        continue;
                }
                if (!do_command(command))
                        exit(EXIT_FAILURE);
        }
}

/*
 * XXX around where we start exceeding BUFSIZ this probably
 * will fuck up
 */
char *
command_availables (void)
{
        static char buf[BUFSIZ];
        char *p;
        int n;

        for (n = 0, p = buf; commands[n].identifier; n++) {
                p = stpncpy(p, commands[n].identifier, BUFSIZ - (p - buf));
                p = stpncpy(p, "\t", BUFSIZ - (p - buf));
                p = stpncpy(p, commands[n].shortdesc, BUFSIZ - (p - buf));
                p = stpncpy(p, "\n", BUFSIZ - (p - buf));
        }
        return buf;
}