tclreadline.c at tip
Not logged in

File tclreadline.c from the latest check-in


 /* ================================================================== *
  * FILE: tclreadline.c
  * $Id$
  * ---
  * tclreadline -- gnu readline for tcl
  * http://www.zellner.org/tclreadline/
  * Copyright (c) 1998 - 2014, Johannes Zellner <johannes@zellner.org>
  * This software is copyright under the BSD license.
  * ================================================================== */

#ifdef HAVE_CONFIG_H
#  include "config.h"
#endif

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

#if defined (READLINE_LIBRARY)
#  include <readline.h>
#  include <history.h>
#else
#  include <readline/readline.h>
#  include <readline/history.h>
#endif


/*
 * this prototype may be missing
 * in readline.h
 */
void rl_extend_line_buffer(int len);

#ifdef EXECUTING_MACRO_HACK
/**
 * this prototype may be private in readline's file `macro.c'.
 * We need it here to decide, if we should read more
 * characters from a macro. Dirty, but it should work.
 */
extern char* EXECUTING_MACRO_NAME;
#endif

#include "tclreadline.h"
static const char* tclrl_library = TCLRL_LIBRARY;
static const char* tclrl_version_str = TCLRL_VERSION_STR;
static const char* tclrl_patchlevel_str = TCLRL_PATCHLEVEL_STR;

#define MALLOC(size) malloc(size)
#define FREE(ptr) free(ptr); ptr = NULL

enum {
    _CMD_SET = (1 << 0),
    _CMD_GET = (1 << 1)
};


typedef struct cmds_t {
    struct cmds_t* prev;
    char**         cmd;
    struct cmds_t* next;
} cmds_t;


#define ISWHITE(c) ((' ' == c) || ('\t' == c) || ('\n' == c))

/**
 * forward declarations
 */
static char*  stripleft(char* in);
static char*  stripright(char* in);
static char*  stripwhite(char* in);
static int    TclReadlineLineComplete(void);
static void   TclReadlineTerminate(int state);
static char*  TclReadlineQuote(char* text, char* quotechars);
static int    TclReadlineCmd(ClientData clientData, Tcl_Interp *interp, int objc,
                             Tcl_Obj *CONST objv[]);
static void   TclReadlineReadHandler(ClientData clientData, int mask);
static void   TclReadlineLineCompleteHandler(char* ptr);
static int    TclReadlineInitialize(Tcl_Interp* interp, char* historyfile);
static int    blank_line(char* str);
static char** TclReadlineCompletion(char* text, int start, int end);
static char*  TclReadline0generator(char* text, int state);
static char*  TclReadlineKnownCommands(char* text, int state, int mode);
static int    TclReadlineParse(char** args, int maxargs, char* buf);


enum {
    LINE_PENDING  = -1,
    LINE_EOF      = (1 << 8),
    LINE_COMPLETE = (1 << 9)
};

/**
 * global variables
 */
static int   tclrl_state                 = TCL_OK;
static char* tclrl_eof_string            = (char*) NULL;
static char* tclrl_custom_completer      = (char*) NULL;
static char* tclrl_last_line             = (char*) NULL;
static int   tclrl_use_builtin_completer = 1;
static int   tclrl_history_length        = -1;
Tcl_Interp*  tclrl_interp                = (Tcl_Interp*) NULL;

static char* tclrl_license =
"   Copyright (c) 1998 - 2000, Johannes Zellner <johannes@zellner.org>\n"
"   All rights reserved.\n"
"   \n"
"   Redistribution and use in source and binary forms, with or without\n"
"   modification, are permitted provided that the following conditions\n"
"   are met:\n"
"   \n"
"     * Redistributions of source code must retain the above copyright\n"
"       notice, this list of conditions and the following disclaimer.\n"
"     * Redistributions in binary form must reproduce the above copyright\n"
"       notice, this list of conditions and the following disclaimer in the\n"
"       documentation and/or other materials provided with the distribution.\n"
"     * Neither the name of Johannes Zellner nor the names of contributors\n"
"       to this software may be used to endorse or promote products derived\n"
"       from this software without specific prior written permission.\n"
"       \n"
"   THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n"
"   ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n"
"   LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n"
"   A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR\n"
"   CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,\n"
"   EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,\n"
"   PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR\n"
"   PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF\n"
"   LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING\n"
"   NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\n"
"   SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.";



static char*
stripleft(char* in)
{
    char* ptr = in;
    while (*ptr && *ptr <= ' ')
        ptr++;
    if (in != ptr)
        memmove(in, ptr, strlen(ptr) + 1);
    return in;
}

static char*
stripright(char* in)
{
    char* ptr;
    for (ptr = strchr(in, '\0') - 1; ptr >= in && *ptr <= ' '; ptr--)
        *ptr = '\0';
    return in;
}

static char*
stripwhite(char* in)
{
    stripleft(in);
    stripright(in);
    return in;
}

static int
TclReadlineLineComplete(void)
{
    return !(tclrl_state == LINE_PENDING);
}

static void
TclReadlineTerminate(int state)
{
    tclrl_state = state;
    rl_callback_handler_remove();
}

static char*
TclReadlineQuote(char* text, char* quotechars)
{
    char* ptr;
    char* result_c;
    int i, len = strlen(quotechars);
    Tcl_DString result;

    Tcl_DStringInit(&result);
    for (ptr = text; ptr && *ptr; ptr++) {
        for (i = 0; i < len; i++) {
            if (quotechars[i] == *ptr) {
                Tcl_DStringAppend(&result, "\\", 1);
                break;
            }
        }
        Tcl_DStringAppend(&result, ptr, 1);
    }
    result_c = strdup(Tcl_DStringValue(&result));
    return result_c;
}

static int
TclReadlineCmd(ClientData clientData, Tcl_Interp *interp, int objc,
               Tcl_Obj *CONST objv[])
{
    int obj_idx, status;

    static char *subCmds[] = {
        "read", "initialize", "write", "add", "complete",
        "customcompleter", "builtincompleter", "eofchar",
        "reset-terminal", "bell", "text", "update",
        (char *) NULL
    };
    enum SubCmdIdx {
        TCLRL_READ, TCLRL_INITIALIZE, TCLRL_WRITE, TCLRL_ADD, TCLRL_COMPLETE,
        TCLRL_CUSTOMCOMPLETER, TCLRL_BUILTINCOMPLETER, TCLRL_EOFCHAR,
        TCLRL_RESET_TERMINAL, TCLRL_BELL, TCLRL_TEXT, TCLRL_UPDATE
    };

    Tcl_ResetResult(interp); /* clear the result space */

    if (objc < 2) {
        Tcl_WrongNumArgs(interp, 1, objv, "option ?arg arg ...?");
        return TCL_ERROR;
    }

    status = Tcl_GetIndexFromObj
        (interp, objv[1], subCmds, "option", 0, (int *) &obj_idx);

    if (status != TCL_OK)
        return status;

    switch (obj_idx) {

        case TCLRL_READ:

            rl_callback_handler_install(
                       objc == 3 ? Tcl_GetStringFromObj(objv[2], 0)
                       : "% ", TclReadlineLineCompleteHandler);

            Tcl_CreateFileHandler(0, TCL_READABLE,
                TclReadlineReadHandler, (ClientData) NULL);

            /**
             * Main Loop.
             * XXX each modification of the global variables
             *     which terminates the main loop must call
             *     rl_callback_handler_remove() to leave
             *     readline in a defined state.          XXX
             */
            tclrl_state = LINE_PENDING;

            while (!TclReadlineLineComplete()) {
#ifdef EXECUTING_MACRO_NAME
                /**
                 * check first, if more characters are
                 * available from _rl_executing_macro,
                 * because Tcl_DoOneEvent() will (naturally)
                 * not detect this `event'.
                 */
                if (EXECUTING_MACRO_NAME)
                    TclReadlineReadHandler((ClientData) NULL, TCL_READABLE);
                else
#endif
                    Tcl_DoOneEvent(TCL_ALL_EVENTS);
            }

            Tcl_DeleteFileHandler(0);

            switch (tclrl_state) {

                case LINE_COMPLETE:
                    return TCL_OK;
                    /* NOTREACHED */
                    break;

                case LINE_EOF:
                    if (tclrl_eof_string)
                    return Tcl_Eval(interp, tclrl_eof_string);
                    else
                    return TCL_OK;
                    /* NOTREACHED */
                    break;

                default:
                    return tclrl_state;
                    /* NOTREACHED */
                    break;
            }
            break;

        case TCLRL_INITIALIZE:
            if (3 != objc) {
                Tcl_WrongNumArgs(interp, 2, objv, "historyfile");
                return TCL_ERROR;
            } else {
                return TclReadlineInitialize(interp,
                                 Tcl_GetStringFromObj(objv[2], 0));
            }
            break;

        case TCLRL_WRITE:
            if (3 != objc) {
                Tcl_WrongNumArgs(interp, 2, objv, "historyfile");
                return TCL_ERROR;
            }  else if (write_history(Tcl_GetStringFromObj(objv[2], 0))) {
                Tcl_AppendResult(interp, "unable to write history to `",
                    Tcl_GetStringFromObj(objv[2], 0), "'\n", (char*) NULL);
                return TCL_ERROR;
            }
            if (tclrl_history_length >= 0) {
                history_truncate_file(Tcl_GetStringFromObj(objv[2], 0),
                              tclrl_history_length);
            }
            return TCL_OK;
            break;

        case TCLRL_ADD:
            if (3 != objc) {
                Tcl_WrongNumArgs(interp, 2, objv, "completerLine");
                return TCL_ERROR;
            } else if (TclReadlineKnownCommands(
                         Tcl_GetStringFromObj(objv[2], 0),
                         (int) 0, _CMD_SET)) {
                Tcl_AppendResult(interp, "unable to add command \"",
                    Tcl_GetStringFromObj(objv[2], 0), "\"\n", (char*) NULL);
            }
            break;

        case TCLRL_COMPLETE:
            if (3 != objc) {
                Tcl_WrongNumArgs(interp, 2, objv, "line");
                return TCL_ERROR;
            } else if (Tcl_CommandComplete(Tcl_GetStringFromObj(objv[2], 0))) {
                Tcl_AppendResult(interp, "1", (char*) NULL);
            } else {
                Tcl_AppendResult(interp, "0", (char*) NULL);
            }
            break;

        case TCLRL_CUSTOMCOMPLETER:
            if (objc > 3) {
                Tcl_WrongNumArgs(interp, 2, objv, "?scriptCompleter?");
                return TCL_ERROR;
            } else if (3 == objc) {
                if (tclrl_custom_completer)
                    FREE(tclrl_custom_completer);
                if (!blank_line(Tcl_GetStringFromObj(objv[2], 0)))
                    tclrl_custom_completer =
                         stripwhite(strdup(Tcl_GetStringFromObj(objv[2], 0)));
            }
            Tcl_AppendResult(interp, tclrl_custom_completer, (char*) NULL);
            break;

        case TCLRL_BUILTINCOMPLETER:
            if (objc > 3) {
                Tcl_WrongNumArgs(interp, 2, objv, "?boolean?");
                return TCL_ERROR;
            } else if (3 == objc) {
                int bool = tclrl_use_builtin_completer;
                if (TCL_OK != Tcl_GetBoolean(interp,
                                 Tcl_GetStringFromObj(objv[2], 0),
                                 &bool)) {
                    Tcl_AppendResult(interp,
                        "wrong # args: should be a boolean value.",
                        (char*) NULL);
                    return TCL_ERROR;
                } else {
                    tclrl_use_builtin_completer = bool;
                }
            }
            Tcl_AppendResult(interp, tclrl_use_builtin_completer ? "1" : "0",
                (char*) NULL);
            break;

        case TCLRL_EOFCHAR:
            if (objc > 3) {
                Tcl_WrongNumArgs(interp, 2, objv, "?script?");
                return TCL_ERROR;
            } else if (3 == objc) {
                if (tclrl_eof_string)
                    FREE(tclrl_eof_string);
                if (!blank_line(Tcl_GetStringFromObj(objv[2], 0)))
                    tclrl_eof_string =
                        stripwhite(strdup(Tcl_GetStringFromObj(objv[2], 0)));
            }
            Tcl_AppendResult(interp, tclrl_eof_string, (char*) NULL);
            break;

        case TCLRL_RESET_TERMINAL:
            /* TODO: add this to the completer */
            if (objc > 3) {
                Tcl_WrongNumArgs(interp, 2, objv, "?terminal-name?");
                return TCL_ERROR;
            }
            if (3 == objc) {
                /*
                 * - tcl8.0 doesn't have Tcl_GetStringFromObj()
                 * - rl_reset_terminal() might be defined
                 *   to take no arguments. This might produce
                 *   a compiler warning.
                 */
                rl_reset_terminal(Tcl_GetStringFromObj(objv[2], 0));
#ifdef CLEANUP_AFER_SIGNAL
            } else {
                rl_cleanup_after_signal();
#endif
            }
            break;

        case TCLRL_BELL:
            if (objc != 2) {
                Tcl_WrongNumArgs(interp, 2, objv, "");
                return TCL_ERROR;
            }


            /*
             * ring the terminal bell obeying the current
             * settings -- audible or visible.
             */

            rl_ding();
            break;

        case TCLRL_UPDATE:
            if (objc != 2) {
                Tcl_WrongNumArgs(interp, 2, objv, "");
                return TCL_ERROR;
            }

            /* Update the input line */

            if (rl_line_buffer) {
                rl_forced_update_display();
            }

            break;


        case TCLRL_TEXT:
            if (objc != 2) {
                Tcl_WrongNumArgs(interp, 2, objv, "");
                return TCL_ERROR;
            }

            /* Return the current input line */
            Tcl_SetObjResult(interp,
               Tcl_NewStringObj(rl_line_buffer ? rl_line_buffer : "", -1));
            break;

        default:
            goto BAD_COMMAND;
            /* NOTREACHED */
            break;
    }

    return TCL_OK;

BAD_COMMAND:
    Tcl_AppendResult(interp,
        "wrong # args: should be \"readline option ?arg ...?\"",
        (char*) NULL);
    return TCL_ERROR;

}

static void
TclReadlineReadHandler(ClientData clientData, int mask)
{
    if (mask & TCL_READABLE) {
#ifdef EXECUTING_MACRO_NAME
        do {
#endif
            rl_callback_read_char();
#ifdef EXECUTING_MACRO_NAME
        /**
         * check, if we're inside a macro and
         * if so, read all macro characters
         * until the next eol.
         */
        } while (EXECUTING_MACRO_NAME && !TclReadlineLineComplete());
#endif
    }
}

static void
TclReadlineLineCompleteHandler(char* ptr)
{
    if (!ptr) { /* <c-d> */

        TclReadlineTerminate(LINE_EOF);

    } else {

        /**
         * From version 0.9.3 upwards, all lines are
         * returned, even empty lines. (Only non-empty
         * lines are stuffed in readline's history.)
         * The calling script is responsible for handling
         * empty strings.
         */

        char* expansion = (char*) NULL;
        int status = history_expand(ptr, &expansion);

        if (status >= 2) {
            /* TODO: make this a valid tcl output */
            printf("%s\n", expansion);
            FREE(ptr);
            FREE(expansion);
            return;
        } else if (status <= -1) {
            Tcl_AppendResult
                (tclrl_interp, "error in history expansion: ", expansion, "\n", (char*) NULL);
                TclReadlineTerminate(TCL_ERROR);
            FREE(ptr);
            FREE(expansion);
            return;
        } else {
            Tcl_AppendResult(tclrl_interp, expansion, (char*) NULL);
        }

    #ifdef EXECUTING_MACRO_NAME
        /**
         * don't stuff macro lines
         * into readline's history.
         */
        if(!EXECUTING_MACRO_NAME) {
    #endif
            /**
             * don't stuff empty lines
             * into readline's history.
             * don't stuff twice the same
             * line into readline's history.
             */
            if (expansion && *expansion && (!tclrl_last_line ||
                    strcmp(tclrl_last_line, expansion))) {
                add_history(expansion);
            }
            if (tclrl_last_line)
                FREE(tclrl_last_line);
            tclrl_last_line = strdup(expansion);
    #ifdef EXECUTING_MACRO_NAME
        }
    #endif
        /**
         * tell the calling routines to terminate.
         */
        TclReadlineTerminate(LINE_COMPLETE);
        FREE(ptr);
        FREE(expansion);
    }
}

int
Tclreadline_SafeInit(Tcl_Interp *interp)
{
    return Tclreadline_Init(interp);
}

int
Tclreadline_Init(Tcl_Interp *interp)
{
    int status;
    Tcl_CreateObjCommand(interp, "::tclreadline::readline", TclReadlineCmd,
        (ClientData) NULL, (Tcl_CmdDeleteProc *) NULL);
    tclrl_interp = interp;
    if (TCL_OK != (status = Tcl_LinkVar(interp, "::tclreadline::historyLength",
            (char*) &tclrl_history_length, TCL_LINK_INT)))
        return status;

    if (TCL_OK != (status = Tcl_LinkVar(interp, "::tclreadline::library",
            (char*) &tclrl_library, TCL_LINK_STRING | TCL_LINK_READ_ONLY)))
        return status;
    if (TCL_OK != (status = Tcl_LinkVar(interp, "::tclreadline::version",
            (char*) &tclrl_version_str, TCL_LINK_STRING | TCL_LINK_READ_ONLY)))
        return status;
    if (TCL_OK != (status = Tcl_LinkVar(interp, "::tclreadline::patchLevel",
            (char*) &tclrl_patchlevel_str, TCL_LINK_STRING | TCL_LINK_READ_ONLY)))
        return status;
    if (TCL_OK != (status = Tcl_LinkVar(interp, "::tclreadline::license",
            (char*) &tclrl_license, TCL_LINK_STRING | TCL_LINK_READ_ONLY)))
        return status;

    if (TCL_OK != (status = Tcl_LinkVar(interp, "tclreadline_library",
            (char*) &tclrl_library, TCL_LINK_STRING | TCL_LINK_READ_ONLY)))
        return status;
    if (TCL_OK != (status = Tcl_LinkVar(interp, "tclreadline_version",
            (char*) &tclrl_version_str, TCL_LINK_STRING | TCL_LINK_READ_ONLY)))
        return status;
    if (TCL_OK != (status = Tcl_LinkVar(interp, "tclreadline_patchLevel",
            (char*) &tclrl_patchlevel_str, TCL_LINK_STRING | TCL_LINK_READ_ONLY)))
        return status;

    return Tcl_PkgProvide(interp, "tclreadline", (char*)tclrl_version_str);
}

static int
TclReadlineInitialize(Tcl_Interp* interp, char* historyfile)
{
    rl_readline_name = "tclreadline";
    /*    rl_special_prefixes = "${\"["; */
    rl_special_prefixes = "$";
    /**
     * default is " \t\n\"\\'`@$><=;|&{("
     * removed "(" <-- arrays
     * removed "{" <-- `${' variables
     * removed "<" <-- completion lists with < ... >
     * added "[]"
     * added "}"
     */
    /* 11.Sep rl_basic_word_break_characters = " \t\n\"\\@$}=;|&[]"; */
    /* besser (11. Sept) 2. (removed \") */
    /* rl_basic_word_break_characters = " \t\n\\@$}=;|&[]"; */
    /* besser (11. Sept) 3. (removed }) */
    rl_basic_word_break_characters = " \t\n\\@$=;|&[]";
#if 0
    rl_basic_quote_characters = "\"{"; /* XXX ??? XXX */
    rl_completer_quote_characters = "\"";
#endif
    /*
       rl_filename_quote_characters
       = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";

       rl_filename_quoting_function
       = (CPFunction*) TclReadlineFilenameQuotingFunction;
     */
    /*
       rl_filename_quoting_desired = 1;
     */

    using_history();
    if (!tclrl_eof_string)
        tclrl_eof_string = strdup("puts {}; exit");

    /*
     * try to read historyfile in home
     * directory. If this failes, this
     * is *not* an error.
     */
    rl_attempted_completion_function = (CPPFunction *) TclReadlineCompletion;
    if (read_history(historyfile)) {
        if (write_history(historyfile)) {
            Tcl_AppendResult (interp, "warning: `",
                historyfile, "' is not writable.", (char*) NULL);
        }
    }
    return TCL_OK;
}

static int
blank_line(char* str)
{
    char* ptr;
    for (ptr = str; ptr && *ptr; ptr++) {
        if (!ISWHITE(*ptr))
            return 0;
    }
    return 1;
}

static char**
TclReadlineCompletion(char* text, int start, int end)
{
    char** matches = (char**) NULL;
    int status;
    rl_completion_append_character = ' '; /* reset, just in case ... */

    if (text && ('!' == text[0]
            || (start && rl_line_buffer[start - 1] == '!' /* for '$' */))) {
        char* expansion = (char*) NULL;
        int oldlen = strlen(rl_line_buffer);
        status = history_expand(rl_line_buffer, &expansion);
        if (status >= 1) {
            rl_extend_line_buffer(strlen(expansion) + 1);
            strcpy(rl_line_buffer, expansion);
            rl_end = strlen(expansion);
            rl_point += strlen(expansion) - oldlen;
            FREE(expansion);
            /*
             * TODO:
             * because we return 0 == matches,
             * the filename completer will still beep.
             rl_inhibit_completion = 1;
             */
            return matches;
        }
        FREE(expansion);
    }

    if (tclrl_custom_completer) {
        char start_s[BUFSIZ], end_s[BUFSIZ];
        Tcl_Obj* obj;
        Tcl_Obj** objv;
        int objc;
        int state;
        char* quoted_text = TclReadlineQuote(text, "$[]{}\"");
        char* quoted_rl_line_buffer = TclReadlineQuote(rl_line_buffer, "$[]{}\"");
        sprintf(start_s, "%d", start);
        sprintf(end_s, "%d", end);
        Tcl_ResetResult(tclrl_interp); /* clear result space */
        state = Tcl_VarEval(tclrl_interp, tclrl_custom_completer,
            " \"", quoted_text, "\" ", start_s, " ", end_s,
            " \"", quoted_rl_line_buffer, "\"", (char*) NULL);
        if (TCL_OK != state) {
            Tcl_AppendResult (tclrl_interp, " `", tclrl_custom_completer,
                " \"", quoted_text, "\" ", start_s, " ", end_s,
                " \"", quoted_rl_line_buffer, "\"' failed.", (char*) NULL);
            TclReadlineTerminate(state);
            FREE(quoted_text);
            FREE(quoted_rl_line_buffer);
            return matches;
        }
        FREE(quoted_text);
        FREE(quoted_rl_line_buffer);
        obj = Tcl_GetObjResult(tclrl_interp);
        status = Tcl_ListObjGetElements(tclrl_interp, obj, &objc, &objv);
        if (TCL_OK != status)
            return matches;

        if (objc) {
            int i, length;
            matches = (char**) MALLOC(sizeof(char*) * (objc + 1));
            for (i = 0; i < objc; i++) {
                matches[i] = strdup(Tcl_GetStringFromObj(objv[i], &length));
                if (1 == objc && !strlen(matches[i])) {
                    FREE(matches[i]);
                    FREE(matches);
                    Tcl_ResetResult(tclrl_interp); /* clear result space */
                    return (char**) NULL;
                }
            }

            /**
             * this is a special one:
             * if the script returns exactly two arguments
             * and the second argument is the empty string,
             * the rl_completion_append_character is set
             * temporarily to NULL.
             */
            if (2 == objc && !strlen(matches[1])) {
                i--;
                FREE(matches[1]);
                rl_completion_append_character = '\0';
            }

            matches[i] = (char*) NULL; /* terminate */
        }
        Tcl_ResetResult(tclrl_interp); /* clear result space */
    }

    if (!matches && tclrl_use_builtin_completer) {
        matches = rl_completion_matches(text, (rl_compentry_func_t *)TclReadline0generator);
    }

    return matches;
}

static char*
TclReadline0generator(char* text, int state)
{
    return TclReadlineKnownCommands(text, state, _CMD_GET);
}

static char*
TclReadlineKnownCommands(char* text, int state, int mode)
{
    static int len;
    static cmds_t *cmds = (cmds_t *) NULL, *new;
    char* tmp;
    char* args[256];
    int i, argc;
    char** name;

    char* local_line = (char*) NULL;
    int sub;


    switch (mode) {

        case _CMD_SET:

            new = (cmds_t *) MALLOC(sizeof(cmds_t));
            new->next = (cmds_t *) NULL;

            if (!cmds) {
                cmds = new;
                cmds->prev = new;
            } else {
                cmds->prev->next = new;
                cmds->prev = new;
            }

            tmp = strdup(text);
            argc = TclReadlineParse(args, sizeof(args), tmp);

            new->cmd = (char**) MALLOC(sizeof(char*) * (argc + 1));

            for (i = 0; i < argc; i++)
                new->cmd[i] = args[i];

            new->cmd[argc] = (char*) NULL;

            return (char*) NULL;
            /* NOTREACHED */
            break;


        case _CMD_GET:

            local_line = strdup(rl_line_buffer);
            sub = TclReadlineParse(args, sizeof(args), local_line);

            if (0 == sub || (1 == sub && '\0' != text[0])) {
                if (!state) {
                    new = cmds;
                    len = strlen(text);
                }
                while (new && (name = new->cmd)) {
                    new = new->next;
                    if (!strncmp(name[0], text, len))
                        return strdup(name[0]);
                }
                return (char*) NULL;
            } else {

                if (!state) {

                    new = cmds;
                    len = strlen(text);

                    while (new && (name = new->cmd)) {
                        if (!strcmp(name[0], args[0]))
                            break;
                        new = new->next;
                    }

                    if (!new)
                        return (char*) NULL;

                    for (i = 0; new->cmd[i]; i++) /* EMPTY */;

                    if (sub < i && !strncmp(new->cmd[sub], text, len))
                        return strdup(new->cmd[sub]);
                    else
                        return (char*) NULL;

                }
                else
                    return (char*) NULL;
            }

            /* NOTREACHED */
            break;


        default:
            return (char*) NULL;
            /* NOTREACHED */
            break;

    }
}

static int
TclReadlineParse(char** args, int maxargs, char* buf)
{
    int nr = 0;

    while (*buf != '\0' && nr < maxargs) {
        /*
         * Strip whitespace.  Use nulls, so
         * that the previous argument is terminated
         * automatically.
         */
        while (ISWHITE(*buf))
            *buf++ = '\0';

        if (!(*buf)) /* don't count the terminating NULL */
            break;

        *args++ = buf;
        nr++;

        while (('\0' != *buf) && !ISWHITE(*buf))
            buf++;
    }

    *args = '\0';
    return nr;
}