/* Copyright (c) 1990,1991,1992 Chris and John Downey */
#ifndef lint
static char *sccsid = "@(#)cmdline.c    2.2 (Chris & John Downey) 8/6/92";
#endif

/***

* program name:
    xvi
* function:
    PD version of UNIX "vi" editor, with extensions.
* module name:
    cmdline.c
* module function:
    Command-line handling (i.e. :/? commands) - most
    of the actual command functions are in ex_cmds.c.
* history:
    STEVIE - ST Editor for VI Enthusiasts, Version 3.10
    Originally by Tim Thompson (twitch!tjt)
    Extensive modifications by Tony Andrews     (onecom!wldrdg!tony)
    Heavily modified by Chris & John Downey
    Minor, Amiga specific modifications made by Dan Schmelzer.
    Very minor change by Laurent FAILLIE for the AMIGASER version.

***/

#include "xvi.h"

#ifdef  MEGAMAX
overlay "cmdline"
#endif

/*
 * The next two variables contain the bounds of any range
 * given in a command. If no range was given, both will be NULL.
 * If only a single line was given, u_line will be NULL.
 * The a_line variable is used for those commands which take
 * a third line specifier after the command, e.g. "move", "copy".
 */
static  Line    *l_line, *u_line;
static  Line    *a_line;

/*
 * Definitions for all ex commands.
 */

#define EX_ENOTFOUND    -1              /* command not found */
#define EX_EAMBIGUOUS   -2              /* could be more than one */
#define EX_ECANTFORCE   -3              /* ! given where not appropriate */
#define EX_EBADARGS     -4              /* inappropriate args given */

#define EX_NOCMD        1
#define EX_SHCMD        2
#define EX_UNUSED       3       /* unused */
#define EX_AMPERSAND    4
#define EX_EXBUFFER     5
#define EX_LSHIFT       6
#define EX_EQUALS       7
#define EX_RSHIFT       8
#define EX_COMMENT      9
#define EX_ABBREVIATE   10
#define EX_APPEND       11
#define EX_ARGS         12
#define EX_BUFFER       13
#define EX_CHDIR        14
#define EX_CHANGE       15
#define EX_CLOSE        16
#define EX_COMPARE      17
#define EX_COPY         18
#define EX_DELETE       19
#define EX_ECHO         20
#define EX_EDIT         21
#define EX_EX           22
#define EX_FILE         23
#define EX_GLOBAL       24
#define EX_HELP         25
#define EX_INSERT       26
#define EX_JOIN         27
#define EX_K            28
#define EX_LIST         29
#define EX_MAP          30
#define EX_MARK         31
#define EX_MOVE         32
#define EX_NEXT         33
#define EX_NUMBER       34
#define EX_OPEN         35
#define EX_PRESERVE     36
#define EX_PRINT        37
#define EX_PUT          38
#define EX_QUIT         39
#define EX_READ         40
#define EX_RECOVER      41
#define EX_REWIND       42
#define EX_SET          43
#define EX_SHELL        44
#define EX_SOURCE       45
#define EX_SPLIT        46
#define EX_SUSPEND      47
#define EX_SUBSTITUTE   48
#define EX_TAG          49
#define EX_UNABBREV     50
#define EX_UNDO         51
#define EX_UNMAP        52
#define EX_V            53
#define EX_VERSION      54
#define EX_VISUAL       55
#define EX_WN           56
#define EX_WQ           57
#define EX_WRITE        58
#define EX_XIT          59
#define EX_YANK         60
#define EX_Z            61
#define EX_GOTO         62
#define EX_TILDE        63

/*
 * Table of all ex commands, and whether they take an '!'.
 *
 * Note that this table is in strict order, sorted on
 * the ASCII value of the first character of the command.
 *
 * The priority field is necessary to resolve clashes in
 * the first one or two characters; so each group of commands
 * beginning with the same letter should have at least one
 * priority 1, so that there is a sensible default.
 *
 * Commands with argument type ec_rest need no delimiters;
 * they need only be matched. This is really only used for
 * single-character commands like !, " and &.
 */
static  struct  ecmd    {
    char        *ec_name;
    short       ec_command;
    short       ec_priority;
    unsigned    ec_flags;
    /*
     * Flags: EXCLAM means can use !, FILEXP means do filename
     * expansion, INTEXP means do % and # expansion. EXPALL means
     * do INTEXP and FILEXP (they are done in that order).
     *
     * EC_RANGE0 means that the range specifier (if any)
     * may include line 0.
     */
#   define      EC_EXCLAM       0x1
#   define      EC_FILEXP       0x2
#   define      EC_INTEXP       0x4
#   define      EC_EXPALL       EC_FILEXP|EC_INTEXP
#   define      EC_RANGE0       0x8

    enum {
        ec_none,                /* no arguments after command */
        ec_strings,             /* whitespace-separated strings */
        ec_1string,             /* like ec_strings but only one */
        ec_line,                /* line number or target argument */
        ec_rest,                /* rest of line passed entirely */
        ec_nonalnum,            /* non-alphanumeric delimiter */
        ec_1lower               /* single lower-case letter */
    }   ec_arg_type;
} cmdtable[] = {
/*  name                command      priority   exclam  */

    /*
     * The zero-length string is used for the :linenumber command.
     */
    "",             EX_NOCMD,       1,  EC_RANGE0,              ec_none,
    "!",            EX_SHCMD,       0,  EC_INTEXP,              ec_rest,

    "#",            EX_NUMBER,      0,  0,                      ec_none,
    "&",            EX_AMPERSAND,   0,  0,                      ec_rest,
    "*",            EX_EXBUFFER,    0,  0,                      ec_rest,
    "<",            EX_LSHIFT,      0,  0,                      ec_none,
    "=",            EX_EQUALS,      0,  0,                      ec_none,
    ">",            EX_RSHIFT,      0,  0,                      ec_none,
    "@",            EX_EXBUFFER,    0,  0,                      ec_rest,
    "\"",           EX_COMMENT,     0,  0,                      ec_rest,

    "abbreviate",   EX_ABBREVIATE,  0,  0,                      ec_strings,
    "append",       EX_APPEND,      1,  0,                      ec_none,
    "args",         EX_ARGS,        0,  0,                      ec_none,

    "buffer",       EX_BUFFER,      0,  EC_EXPALL,              ec_1string,

    "cd",           EX_CHDIR,       1,  EC_EXPALL,              ec_1string,
    "change",       EX_CHANGE,      2,  0,                      ec_none,
    "chdir",        EX_CHDIR,       1,  EC_EXPALL,              ec_1string,
    "close",        EX_CLOSE,       1,  EC_EXCLAM,              ec_none,
    "compare",      EX_COMPARE,     0,  0,                      ec_none,
    "copy",         EX_COPY,        1,  0,                      ec_line,

    "delete",       EX_DELETE,      0,  0,                      ec_none,

    "echo",         EX_ECHO,        0,  EC_INTEXP,              ec_strings,
    "edit",         EX_EDIT,        1,  EC_EXCLAM|EC_EXPALL,    ec_1string,
    "ex",           EX_EX,          0,  EC_EXPALL,              ec_1string,

    "file",         EX_FILE,        0,  EC_EXPALL,              ec_1string,

    "global",       EX_GLOBAL,      0,  EC_EXCLAM,              ec_nonalnum,

    "help",         EX_HELP,        0,  0,                      ec_none,

    "insert",       EX_INSERT,      0,  0,                      ec_none,

    "join",         EX_JOIN,        0,  0,                      ec_none,

    "k",            EX_K,           0,  0,                      ec_1lower,

    "list",         EX_LIST,        0,  0,                      ec_none,

    "map",          EX_MAP,         0,  EC_EXCLAM,              ec_strings,
    "mark",         EX_MARK,        0,  0,                      ec_1lower,
    "move",         EX_MOVE,        1,  0,                      ec_line,

    "next",         EX_NEXT,        1,  EC_EXCLAM|EC_EXPALL,    ec_strings,
    "number",       EX_NUMBER,      0,  0,                      ec_none,

    "open",         EX_OPEN,        0,  0,                      ec_none,

    "preserve",     EX_PRESERVE,    0,  0,                      ec_none,
    "print",        EX_PRINT,       1,  0,                      ec_none,
    "put",          EX_PUT,         0,  EC_RANGE0,              ec_none,

    "quit",         EX_QUIT,        0,  EC_EXCLAM,              ec_none,

    "read",         EX_READ,        1,  EC_EXPALL|EC_RANGE0,    ec_1string,
    "recover",      EX_RECOVER,     0,  0,                      ec_none,
    "rewind",       EX_REWIND,      0,  EC_EXCLAM,              ec_none,

    "set",          EX_SET,         0,  0,                      ec_strings,
    "shell",        EX_SHELL,       0,  0,                      ec_none,
    "source",       EX_SOURCE,      0,  EC_EXPALL,              ec_1string,
    "split",        EX_SPLIT,       0,  0,                      ec_none,
    "stop",         EX_SUSPEND,     0,  0,                      ec_none,
    "substitute",   EX_SUBSTITUTE,  1,  0,                      ec_nonalnum,
    "suspend",      EX_SUSPEND,     0,  0,                      ec_none,

    "t",            EX_COPY,        1,  0,                      ec_line,
    "tag",          EX_TAG,         0,  EC_EXCLAM,              ec_1string,

    "unabbreviate", EX_UNABBREV,    0,  0,                      ec_strings,
    "undo",         EX_UNDO,        1,  0,                      ec_none,
    "unmap",        EX_UNMAP,       0,  EC_EXCLAM,              ec_strings,

    "v",            EX_V,           1,  0,                      ec_nonalnum,
    "version",      EX_VERSION,     0,  0,                      ec_none,
    "visual",       EX_VISUAL,      0,  EC_EXCLAM|EC_EXPALL,    ec_1string,

    "wn",           EX_WN,          0,  EC_EXCLAM,              ec_none,
    "wq",           EX_WQ,          0,  EC_EXCLAM|EC_EXPALL,    ec_1string,
    "write",        EX_WRITE,       1,  EC_EXCLAM|EC_EXPALL,    ec_1string,

    "xit",          EX_XIT,         0,  0,                      ec_none,

    "yank",         EX_YANK,        0,  0,                      ec_none,

    "z",            EX_Z,           0,  0,                      ec_none,

    "|",            EX_GOTO,        0,  0,                      ec_none,
    "~",            EX_TILDE,       0,  0,                      ec_rest,

    NULL,           0,              0,  0,                      ec_none,
};

/*
 * Internal routine declarations.
 */
static  int     decode_command P((char **, bool_t *, struct ecmd **));
static  bool_t  get_line P((char **, Line **));
static  bool_t  get_range P((char **));
static  void    badcmd P((bool_t, char *));
static  char    *show_line P((void));
static  char    *expand_percents P((char *));

/*
 * These are used for display mode.
 */
static  Line    *curline;
static  Line    *lastline;
static  bool_t  do_line_numbers;

/*
 * Macro to skip over whitespace during command line interpretation.
 */
#define skipblanks(p)   { while (*(p) != '\0' && is_space(*(p))) (p)++; }

/*
 * do_colon() - process a ':' command.
 *
 * The cmdline argument points to a complete command line to be processed
 * (this does not include the ':' itself).
 */
void
do_colon(cmdline, interactive)
char    *cmdline;                       /* optional command string */
bool_t  interactive;                    /* true if reading from tty */
{
    char        *arg;                   /* ptr to string arg(s) */
    int         argc = 0;               /* arg count for ec_strings */
    char        **argv = NULL;          /* arg vector for ec_strings */
    bool_t      exclam;                 /* true if ! was given */
    int         command;                /* which command it is */
    struct ecmd *ecp;                   /* ptr to command entry */
    unsigned    savecho;                /* previous value of echo */

    /*
     * Clear the range variables.
     */
    l_line = NULL;
    u_line = NULL;

    /*
     * Parse a range, if present (and update the cmdline pointer).
     */
    if (!get_range(&cmdline)) {
        return;
    }

    /*
     * Decode the command.
     */
    skipblanks(cmdline);
    command = decode_command(&cmdline, &exclam, &ecp);

    if (command > 0) {
        /*
         * Check that the range specified,
         * if any, is legal for the command.
         */
        if (!(ecp->ec_flags & EC_RANGE0)) {
            if (l_line == curbuf->b_line0 || u_line == curbuf->b_line0) {
                show_error(curwin,
                    "Specification of line 0 not allowed");
                return;
            }
        }

        switch (ecp->ec_arg_type) {
        case ec_none:
            if (*cmdline != '\0' &&
                    (*cmdline != '!' || !(ecp->ec_flags & EC_EXCLAM))) {
                command = EX_EBADARGS;
            }
            break;

        case ec_line:
            a_line = NULL;
            skipblanks(cmdline);
            if (!get_line(&cmdline, &a_line) || a_line == NULL) {
                command = EX_EBADARGS;
            }
            break;

        case ec_1lower:
            /*
             * One lower-case letter.
             */
            skipblanks(cmdline);
            if (!is_lower(cmdline[0]) || cmdline[1] != '\0') {
                command = EX_EBADARGS;
            } else {
                arg = cmdline;
            }
            break;

        case ec_nonalnum:
        case ec_rest:
        case ec_strings:
        case ec_1string:
            arg = cmdline;
            if (ecp->ec_arg_type == ec_strings ||
                                    ecp->ec_arg_type == ec_1string) {
                if (*arg == '\0') {
                    /*
                     * No args.
                     */
                    arg = NULL;
                } else {
                    /*
                     * Null-terminate the command and skip
                     * whitespace to arg or end of line.
                     */
                    *arg++ = '\0';
                    skipblanks(arg);

                    /*
                     * There was trailing whitespace,
                     * but no args.
                     */
                    if (*arg == '\0') {
                        arg = NULL;
                    }
                }
            } else if (ecp->ec_arg_type == ec_nonalnum && exclam) {
                /*
                 * We don't normally touch the arguments for
                 * this type, but we have to null-terminate
                 * the '!' at least.
                 */
                *arg++ = '\0';
            }

            if (arg != NULL) {
                /*
                 * Perform expansions on the argument string.
                 */
                if (ecp->ec_flags & EC_INTEXP) {
                    arg = expand_percents(arg);
                }
                if (ecp->ec_flags & EC_FILEXP) {
                    arg = fexpand(arg);
                }

                if (ecp->ec_arg_type == ec_strings) {
                    makeargv(arg, &argc, &argv, " \t");
                }
            }
        }
    }

    savecho = echo;

    /*
     * Now do the command.
     */
    switch (command) {
    case EX_SHCMD:
        /*
         * If a line range was specified, this must be a pipe command.
         * Otherwise, it's just a simple shell command.
         */
        if (l_line != NULL) {
            specify_pipe_range(curwin, l_line, u_line);
            do_pipe(curwin, arg);
        } else {
            do_shcmd(curwin, arg);
        }
        break;

    case EX_ARGS:
        do_args(curwin);
        break;

    case EX_BUFFER:
        if (arg != NULL)
            echo &= ~(e_SCROLL | e_REPORT | e_SHOWINFO);
        (void) do_buffer(curwin, arg);
        move_window_to_cursor(curwin);
        update_window(curwin);
        break;

    case EX_CHDIR:
    {
        char    *error;

        if ((error = do_chdir(arg)) != NULL) {
            badcmd(interactive, error);
        } else if (interactive) {
            char        *dirp;

            if ((dirp = alloc(MAXPATHLEN + 2)) != NULL &&
                getcwd(dirp, MAXPATHLEN + 2) != NULL) {
                show_message(curwin, "%s", dirp);
            }
            if (dirp) {
                free(dirp);
            }
        }
        break;
    }

    case EX_CLOSE:
        do_close_window(curwin, exclam);
        break;

    case EX_COMMENT:            /* This one is easy ... */
        break;

    case EX_COMPARE:
        do_compare();
        break;

    case EX_COPY:
        do_cdmy('c', l_line, u_line, a_line);
        break;

    case EX_DELETE:
        do_cdmy('d', l_line, u_line, (Line *) NULL);
        break;

    case EX_ECHO:                       /* echo arguments on command line */
    {
        int     i;

        flexclear(&curwin->w_statusline);
        for (i = 0; i < argc; i++) {
            if (!lformat(&curwin->w_statusline, "%s ", argv[i])
                    || flexlen(&curwin->w_statusline) >= curwin->w_ncols) {
                break;
            }
        }
        update_sline(curwin);
        break;
    }

    case EX_EDIT:
    case EX_VISUAL:                     /* treat :vi as :edit */
        echo &= ~(e_SCROLL | e_REPORT | e_SHOWINFO);
        (void) do_edit(curwin, exclam, arg);
        move_window_to_cursor(curwin);
        update_buffer(curbuf);
#if 0
        show_file_info(curwin);
#endif
        break;

    case EX_FILE:
        if (arg != NULL) {
            if (curbuf->b_filename != NULL)
                free(curbuf->b_filename);
            curbuf->b_filename = strsave(arg);
            if (curbuf->b_tempfname != NULL) {
                free(curbuf->b_tempfname);
                curbuf->b_tempfname = NULL;
            }
        }
        show_file_info(curwin);
        break;

    case EX_GLOBAL:
        do_global(curwin, l_line, u_line, arg, !exclam);
        break;

    case EX_HELP:
        do_help(curwin);
        break;

    case EX_MAP:
        xvi_map(argc, argv, exclam, interactive);
        break;

    case EX_UNMAP:
        xvi_unmap(argc, argv, exclam, interactive);
        break;

    case EX_MARK:
    case EX_K:
    {
        Posn    pos;

        pos.p_index = 0;
        if (l_line == NULL) {
            pos.p_line = curwin->w_cursor->p_line;
        } else {
            pos.p_line = l_line;
        }
        (void) setmark(arg[0], curbuf, &pos);
        break;
    }

    case EX_MOVE:
        do_cdmy('m', l_line, u_line, a_line);
        break;

    case EX_NEXT:
        /*
         * do_next() handles turning off the appropriate bits
         * in echo, & also calls move_window_to_cursor() &
         * update_buffer() as required, so we don't have to
         * do any of that here.
         */
        do_next(curwin, argc, argv, exclam);
        break;

    case EX_PRESERVE:
        if (do_preserve())
            show_file_info(curwin);
        break;

    case EX_LIST:
    case EX_PRINT:
    case EX_NUMBER:
        if (l_line == NULL) {
            curline = curwin->w_cursor->p_line;
            lastline = curline->l_next;
        } else if (u_line ==  NULL) {
            curline = l_line;
            lastline = l_line->l_next;
        } else {
            curline = l_line;
            lastline = u_line->l_next;
        }
        do_line_numbers = (Pb(P_number) || command == EX_NUMBER);
        disp_init(curwin, show_line, (int) curwin->w_ncols,
                                command == EX_LIST || Pb(P_list));
        break;

    case EX_PUT:
    {
        Posn    where;

        if (l_line != NULL) {
            where.p_index = 0;
            where.p_line = l_line;
        } else {
            where.p_index = curwin->w_cursor->p_index;
            where.p_line = curwin->w_cursor->p_line;
        }
        do_put(curwin, &where, FORWARD, '@');
        break;
    }

    case EX_QUIT:
        do_quit(curwin, exclam);
        break;

    case EX_REWIND:
        do_rewind(curwin, exclam);
        break;

    case EX_READ:
        if (arg == NULL) {
            badcmd(interactive, "Unrecognized command");
            break;
        }
        do_read(curwin, arg, (l_line != NULL) ? l_line :
                                            curwin->w_cursor->p_line);
        break;

    case EX_SET:
        do_set(curwin, argc, argv, interactive);
        break;

    case EX_SHELL:
        do_shell(curwin);
        break;

    case EX_SOURCE:
        if (arg == NULL) {
            badcmd(interactive, "Missing filename");
        } else if (do_source(interactive, arg) && interactive) {
            show_file_info(curwin);
        }
        break;

    case EX_SPLIT:
        /*
         * "split".
         */
        do_split_window(curwin);
        break;

    case EX_SUBSTITUTE:
    case EX_AMPERSAND:
    case EX_TILDE:
    {
        long            nsubs;
        register long   (*func) P((Xviwin *, Line *, Line *, char *));

        switch (command) {
        case EX_SUBSTITUTE:
            func = do_substitute;
            break;
        case EX_AMPERSAND:
            func = do_ampersand;
            break;
        case EX_TILDE:
            func = do_tilde;
        }

        nsubs = (*func)(curwin, l_line, u_line, arg);
        update_buffer(curbuf);
        cursupdate(curwin);
        begin_line(curwin, TRUE);
        if (nsubs >= Pn(P_report)) {
            show_message(curwin, "%ld substitution%c",
                        nsubs,
                        (nsubs > 1) ? 's' : ' ');
        }
        break;
    }

    case EX_SUSPEND:
        do_suspend(curwin);
        break;

    case EX_TAG:
        (void) do_tag(curwin, arg, exclam, TRUE, TRUE);
        break;

    case EX_V:
        do_global(curwin, l_line, u_line, arg, FALSE);
        break;

    case EX_VERSION:
        show_message(curwin, Version);
#if defined(AMIGA) && !defined (AMIGASER)
        amiga_version();
#endif
        break;

    case EX_WN:
        /*
         * This is not a standard "vi" command, but it
         * is provided in PC vi, and it's quite useful.
         */
        if (do_write(curwin, (char *) NULL, (Line *) NULL, (Line *) NULL,
                                                                exclam)) {
            /*
             * See comment for EX_NEXT (above).
             */
            do_next(curwin, 0, argv, exclam);
#if 0
            move_window_to_cursor(curwin);
            update_buffer(curbuf);
#endif
        }
        break;

    case EX_WQ:
        do_wq(curwin, arg, exclam);
        break;

    case EX_WRITE:
        (void) do_write(curwin, arg, l_line, u_line, exclam);
        break;

    case EX_XIT:
        do_xit(curwin);
        break;

    case EX_YANK:
        do_cdmy('y', l_line, u_line, (Line *) NULL);
        break;

    case EX_EXBUFFER:
        yp_stuff_input(curwin, arg[0], FALSE);
        break;

    case EX_EQUALS:
        do_equals(curwin, l_line);
        break;

    case EX_LSHIFT:
    case EX_RSHIFT:
        if (l_line == NULL) {
            l_line = curwin->w_cursor->p_line;
        }
        if (u_line == NULL) {
            u_line = l_line;
        }
        tabinout((command == EX_LSHIFT) ? '<' : '>', l_line, u_line);
        begin_line(curwin, TRUE);
        update_buffer(curbuf);
        break;

    case EX_NOCMD:
        /*
         * If we got a line, but no command, then go to the line.
         */
        if (l_line != NULL) {
            if (l_line == curbuf->b_line0) {
                l_line = l_line->l_next;
            }
            move_cursor(curwin, l_line, 0);
            begin_line(curwin, TRUE);
        }
        break;

    case EX_ENOTFOUND:
        badcmd(interactive, "Unrecognized command");
        break;

    case EX_EAMBIGUOUS:
        badcmd(interactive, "Ambiguous command");
        break;

    case EX_ECANTFORCE:
        badcmd(interactive, "Inappropriate use of !");
        break;

    case EX_EBADARGS:
        badcmd(interactive, "Inappropriate arguments given");
        break;

    default:
        /*
         * Decoded successfully, but unknown to us. Whoops!
         */
        badcmd(interactive, "Internal error - unimplemented command.");
        break;

    case EX_ABBREVIATE:
    case EX_APPEND:
    case EX_CHANGE:
    case EX_EX:
    case EX_GOTO:
    case EX_INSERT:
    case EX_JOIN:
    case EX_OPEN:
    case EX_RECOVER:
    case EX_UNABBREV:
    case EX_UNDO:
    case EX_Z:
        badcmd(interactive, "Unimplemented command.");
        break;
    }

    echo = savecho;

    if (argc > 0 && argv != NULL) {
        free((char *) argv);
    }
}

/*
 * Find the correct command for the given string, and return it.
 *
 * Updates string pointer to point to 1st char after command.
 *
 * Updates boolean pointed to by forcep according
 * to whether an '!' was given after the command;
 * if an '!' is given for a command which can't take it,
 * this is an error, and EX_ECANTFORCE is returned.
 * For unknown commands, EX_ENOTFOUND is returned.
 * For ambiguous commands, EX_EAMBIGUOUS is returned.
 *
 * Also updates *cmdp to point at entry in command table.
 */
static int
decode_command(str, forcep, cmdp)
char            **str;
bool_t          *forcep;
struct  ecmd    **cmdp;
{
    struct ecmd *cmdptr;
    struct ecmd *last_match = NULL;
    bool_t      last_exclam = FALSE;
    int         nmatches = 0;
    char        *last_delimiter = *str;

    for (cmdptr = cmdtable; cmdptr->ec_name != NULL; cmdptr++) {
        char    *name = cmdptr->ec_name;
        char    *strp;
        bool_t  matched;

        strp = *str;

        while (*strp == *name && *strp != '\0') {
            strp++;
            name++;
        }

        matched = (
            /*
             * we've got a full match, or ...
             */
            *strp == '\0'
            ||
            /*
             * ... a whitespace delimiter, or ...
             */
            is_space(*strp)
            ||
            (
                /*
                 * ... at least one character has been
                 * matched, and ...
                 */
                name > cmdptr->ec_name
                &&
                (
                    (
                        /*
                         * ... this command can accept a
                         * '!' qualifier, and we've found
                         * one ...
                         */
                        (cmdptr->ec_flags & EC_EXCLAM)
                        &&
                        *strp == '!'
                    )
                    ||
                    (
                        /*
                         * ... or it can take a
                         * non-alphanumeric delimiter
                         * (like '/') ...
                         */
                        cmdptr->ec_arg_type == ec_nonalnum
                        &&
                        !is_alnum(*strp)
                    )
                    ||
                    (
                        /*
                         * ... or it doesn't need any
                         * delimiter ...
                         */
                        cmdptr->ec_arg_type == ec_rest
                    )
                    ||
                    (
                        /*
                         * ... or we've got a full match,
                         * & the command expects a single
                         * lower-case letter as an
                         * argument, and we've got one ...
                         */
                        cmdptr->ec_arg_type == ec_1lower
                        &&
                        *name == '\0'
                        &&
                        is_lower(*strp)
                    )
                    ||
                    (
                        /*
                         * ... or we've got a partial match,
                         * and the command expects a line
                         * specifier as an argument, and the
                         * next character is not alphabetic.
                         */
                        cmdptr->ec_arg_type == ec_line
                        &&
                        !is_alpha(*strp)
                    )
                )
            )
        );

        if (!matched)
            continue;

        if (last_match == NULL ||
                (last_match != NULL &&
                 last_match->ec_priority < cmdptr->ec_priority)) {
            /*
             * No previous match, or this one is better.
             */
            last_match = cmdptr;
            last_exclam = (*strp == '!');
            last_delimiter = strp;
            nmatches = 1;

            /*
             * If this is a complete match, we don't
             * need to search the rest of the table.
             */
            if (*name == '\0')
                break;

        } else if (last_match != NULL &&
               last_match->ec_priority == cmdptr->ec_priority) {
            /*
             * Another match with the same priority - remember it.
             */
            nmatches++;
        }
    }

    /*
     * For us to have found a good match, there must have been
     * exactly one match at a certain priority, and if an '!'
     * was used, it must be allowed by that match.
     */
    if (last_match == NULL) {
        return(EX_ENOTFOUND);
    } else if (nmatches != 1) {
        return(EX_EAMBIGUOUS);
    } else if (last_exclam && ! (last_match->ec_flags & EC_EXCLAM)) {
        return(EX_ECANTFORCE);
    } else {
        *forcep = last_exclam;
        *str = last_delimiter;
        *cmdp = last_match;
        return(last_match->ec_command);
    }
}

/*
 * get_range - parse a range specifier
 *
 * Ranges are of the form
 *
 * ADDRESS1,ADDRESS2
 * ADDRESS              (equivalent to "ADDRESS,ADDRESS")
 * ADDRESS,             (equivalent to "ADDRESS,.")
 * ,ADDRESS             (equivalent to ".,ADDRESS")
 * ,                    (equivalent to ".")
 * %                    (equivalent to "1,$")
 *
 * where ADDRESS is
 *
 * /regular expression/ [INCREMENT]
 * ?regular expression? [INCREMENT]
 * $   [INCREMENT]
 * 'x  [INCREMENT]      (where x denotes a currently defined mark)
 * .   [INCREMENT]
 * INCREMENT            (equivalent to . INCREMENT)
 * number [INCREMENT]
 *
 * and INCREMENT is
 *
 * + number
 * - number
 * +                    (equivalent to +1)
 * -                    (equivalent to -1)
 * ++                   (equivalent to +2)
 * --                   (equivalent to -2)
 *
 * etc.
 *
 * The pointer *cpp is updated to point to the first character following
 * the range spec. If an initial address is found, but no second, the
 * upper bound is equal to the lower, except if it is followed by ','.
 *
 * Return FALSE if an error occurred, otherwise TRUE.
 */
static bool_t
get_range(cpp)
register char   **cpp;
{
    register char       *cp;
    char                sepchar;

#define skipp   { cp = *cpp; skipblanks(cp); *cpp = cp; }

    skipp;

    if (*cp == '%') {
        /*
         * "%" is the same as "1,$".
         */
        l_line = curbuf->b_file;
        u_line = curbuf->b_lastline->l_prev;
        ++*cpp;
        return TRUE;
    }

    if (!get_line(cpp, &l_line)) {
        return FALSE;
    }

    skipp;

    sepchar = *cp;
    if (sepchar != ',' && sepchar != ';') {
        u_line = l_line;
        return TRUE;
    }

    ++*cpp;

    if (l_line == NULL) {
        /*
         * So far, we've got ",".
         *
         * ",address" is equivalent to ".,address".
         */
        l_line = curwin->w_cursor->p_line;
    }

    if (sepchar == ';') {
        move_cursor(curwin, (l_line != curbuf->b_line0) ?
                    l_line : l_line->l_next, 0);
    }

    skipp;
    if (!get_line(cpp, &u_line)) {
        return FALSE;
    }
    if (u_line == NULL) {
        /*
         * "address," is equivalent to "address,.".
         */
        u_line = curwin->w_cursor->p_line;
    }

    /*
     * Check for reverse ordering.
     */
    if (earlier(u_line, l_line)) {
        Line    *tmp;

        tmp = l_line;
        l_line = u_line;
        u_line = tmp;
    }

    skipp;
    return TRUE;
}

static bool_t
get_line(cpp, lpp)
    char                **cpp;
    Line                **lpp;
{
    Line                *pos;
    char                *cp;
    char                c;
    long                lnum;

    cp = *cpp;

    /*
     * Determine the basic form, if present.
     */
    switch (c = *cp++) {

    case '/':
    case '?':
        pos = linesearch(curwin, (c == '/') ? FORWARD : BACKWARD,
                                    &cp);
        if (pos == NULL) {
            return FALSE;
        }
        break;

    case '$':
        pos = curbuf->b_lastline->l_prev;
        break;

        /*
         * "+n" is equivalent to ".+n".
         */
    case '+':
    case '-':
        cp--;
         /* fall through ... */
    case '.':
        pos = curwin->w_cursor->p_line;
        break;

    case '\'': {
        Posn            *lp;

        lp = getmark(*cp++, curbuf);
        if (lp == NULL) {
            show_error(curwin, "Unknown mark");
            return FALSE;
        }
        pos = lp->p_line;
        break;
    }
    case '0': case '1': case '2': case '3': case '4':
    case '5': case '6': case '7': case '8': case '9':
        for (lnum = c - '0'; is_digit(*cp); cp++)
            lnum = lnum * 10 + *cp - '0';

        if (lnum == 0) {
            pos = curbuf->b_line0;
        } else {
            pos = gotoline(curbuf, lnum);
        }
        break;

    default:
        return TRUE;
    }

    skipblanks(cp);

    if (*cp == '-' || *cp == '+') {
        char    dirchar = *cp++;

        skipblanks(cp);
        if (is_digit(*cp)) {
            for (lnum = 0; is_digit(*cp); cp++) {
                lnum = lnum * 10 + *cp - '0';
            }
        } else {
            for (lnum = 1; *cp == dirchar; cp++) {
                lnum++;
            }
        }

        if (dirchar == '-')
            lnum = -lnum;

        pos = gotoline(curbuf, lineno(curbuf, pos) + lnum);
    }

    *cpp = cp;
    *lpp = pos;
    return TRUE;
}

/*
 * This routine is called for ambiguous, unknown,
 * badly defined or unimplemented commands.
 */
static void
badcmd(interactive, str)
bool_t  interactive;
char    *str;
{
    if (interactive) {
        show_error(curwin, str);
    }
}

static char *
show_line()
{
    Line        *lp;

    if (curline == lastline) {
        return(NULL);
    }

    lp = curline;
    curline = curline->l_next;

    if (do_line_numbers) {
        static Flexbuf  nu_line;

        flexclear(&nu_line);
        (void) lformat(&nu_line, NUM_FMT, lineno(curbuf, lp));
        (void) lformat(&nu_line, "%s", lp->l_text);
        return flexgetstr(&nu_line);
    } else {
        return(lp->l_text);
    }
}

static char *
expand_percents(str)
char    *str;
{
    static Flexbuf      newstr;
    register char       *from;
    register bool_t     escaped;

    if (strpbrk(str, "%#") == (char *) NULL) {
        return str;
    }
    flexclear(&newstr);
    escaped = FALSE;
    for (from = str; *from != '\0'; from++) {
        if (!escaped) {
            if (*from == '%' && curbuf->b_filename != NULL) {
                (void) lformat(&newstr, "%s", curbuf->b_filename);
            } else if (*from == '#' && altfilename != NULL) {
                (void) lformat(&newstr, "%s", altfilename);
            } else if (*from == '\\') {
                escaped = TRUE;
            } else {
                (void) flexaddch(&newstr, *from);
            }
        } else {
            if (*from != '%' && *from != '#') {
                (void) flexaddch(&newstr, '\\');
            }
            (void) flexaddch(&newstr, *from);
            escaped = FALSE;
        }
    }
    return flexgetstr(&newstr);
}
