4.4BSD/usr/src/contrib/nvi/nvi/ex/ex.c
/*-
* Copyright (c) 1992, 1993
* The Regents of the University of California. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* 3. All advertising materials mentioning features or use of this software
* must display the following acknowledgement:
* This product includes software developed by the University of
* California, Berkeley and its contributors.
* 4. Neither the name of the University nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
*/
#ifndef lint
static char sccsid[] = "@(#)ex.c 8.1 (Berkeley) 6/9/93";
#endif /* not lint */
#include <sys/types.h>
#include <sys/stat.h>
#include <ctype.h>
#include <errno.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "vi.h"
#include "excmd.h"
char *defcmdarg[2];
static char *linespec __P((SCR *, EXF *, char *, EXCMDARG *));
#define DEFCOM ".+1"
/*
* ex --
* Read an ex command and execute it.
*/
int
ex(sp, ep)
SCR *sp;
EXF *ep;
{
TEXT *tp;
int eval;
char defcom[sizeof(DEFCOM)];
if (ex_init(sp, ep))
return (1);
if (sp->s_refresh(sp, ep))
return (ex_end(sp));
for (eval = 0;;) {
if (sp->s_get(sp, ep,
&sp->bhdr, ':', TXT_CR | TXT_PROMPT))
continue;
tp = sp->bhdr.next;
if (tp->len == 0) {
(void)fputc('\r', sp->stdfp);
(void)fflush(sp->stdfp);
memmove(defcom, DEFCOM, sizeof(DEFCOM));
(void)ex_cstring(sp, ep, defcom, sizeof(DEFCOM) - 1);
} else {
(void)fputc('\n', sp->stdfp);
(void)ex_cstring(sp, ep, tp->lb, tp->len);
}
msg_rpt(sp, sp->stdfp);
if (!F_ISSET(sp, S_MODE_EX) || F_ISSET(sp, S_MAJOR_CHANGE))
break;
if (sp->s_refresh(sp, ep)) {
eval = 1;
break;
}
}
return (ex_end(sp) || eval);
}
/*
* ex_cfile --
* Execute ex commands from a file.
*/
int
ex_cfile(sp, ep, filename, noexisterr)
SCR *sp;
EXF *ep;
char *filename;
int noexisterr;
{
struct stat sb;
int fd, len, rval;
char *bp;
if ((fd = open(filename, O_RDONLY, 0)) < 0) {
if (noexisterr)
goto e1;
return (0);
}
if (fstat(fd, &sb))
goto e2;
if (sb.st_size > SIZE_T_MAX) {
errno = E2BIG;
goto e2;
}
if ((bp = malloc(sb.st_size)) == NULL)
goto e2;
len = read(fd, bp, (int)sb.st_size);
if (len == -1 || len != sb.st_size) {
if (len != sb.st_size)
errno = EIO;
goto e3;
}
bp[sb.st_size] = '\0';
rval = ex_cstring(sp, ep, bp, len);
/*
* XXX
* THE UNDERLYING EXF MAY HAVE CHANGED (but we don't care).
*/
free(bp);
(void)close(fd);
return (rval);
e3: free(bp);
e2: (void)close(fd);
e1: msgq(sp, M_ERR, "%s: %s.", filename, strerror(errno));
return (1);
}
/*
* ex_cstring --
* Execute EX commands from a string. The commands may be separated
* by newlines or by | characters, and may be quoted. Quotes are
* either the user's literal next character or a backslash. Literal
* next characters are translated into backslashes.
*/
int
ex_cstring(sp, ep, cmd, len)
SCR *sp;
EXF *ep;
char *cmd;
int len;
{
u_int saved_mode;
int ch, cnt, rval;
char *p, *t;
rval = 0;
saved_mode = F_ISSET(sp, S_MODE_EX | S_MODE_VI | S_MAJOR_CHANGE);
for (p = t = cmd, cnt = 0;; ++cnt, --len) {
if (len == 0)
goto cend;
switch (ch = *t++) {
case '|':
case '\n':
cend: if (p > cmd) {
*p = '\0'; /* XXX: 8BIT */
/*
* Errors are ignored, although error
* messages will be displayed later.
*/
if (ex_cmd(sp, ep, cmd))
rval = 1;
p = cmd;
}
if (len == 0)
return (rval);
if (saved_mode != F_ISSET(sp,
S_MODE_EX | S_MODE_VI | S_MAJOR_CHANGE)) {
msgq(sp, M_ERR,
"File or status changed, remaining input discarded.");
return (1);
}
break;
default:
if (ch == '\\' || sp->special[ch] == K_VLNEXT) {
*p++ = '\\';
if (len == 1)
break;
--len;
ch = *t++;
}
*p++ = ch;
break;
}
}
/* NOTREACHED */
}
/*
* ex_cmd --
* Parse and execute an ex command.
*/
int
ex_cmd(sp, ep, exc)
SCR *sp;
EXF *ep;
char *exc;
{
EXCMDARG cmd;
EXCMDLIST *cp;
recno_t lcount, lno, num;
long flagoff;
u_int saved_mode;
int ch, cmdlen, flags, uselastcmd;
char *p, *endp;
#if DEBUG && 0
TRACE(sp, "ex: {%s}\n", exc);
#endif
/*
* Permit a single extra colon at the start of the line, for
* historical reasons.
*/
if (*exc == ':')
++exc;
/* Ignore command lines that start with a double-quote. */
if (*exc == '"')
return (0);
/* Skip whitespace. */
for (; isspace(*exc); ++exc);
/* Initialize the argument structure. */
memset(&cmd, 0, sizeof(EXCMDARG));
cmd.buffer = OOBCB;
/*
* Parse line specifiers if the command uses addresses. New command
* line position is returned, or NULL on error.
*/
if ((exc = linespec(sp, ep, exc, &cmd)) == NULL)
return (1);
/* Skip whitespace. */
for (; isspace(*exc); ++exc);
/*
* If no command, ex does the last specified of p, l, or #, and vi
* moves to the line. Otherwise, find out how long the command name
* is. There are a few commands that aren't alphabetic, but they
* are all single character commands.
*/
if (*exc) {
if (strchr("!#&<=>@", *exc)) {
p = exc;
exc++;
cmdlen = 1;
} else {
for (p = exc; isalpha(*exc); ++exc);
cmdlen = exc - p;
}
for (cp = cmds; cp->name && memcmp(p, cp->name, cmdlen); ++cp);
if (cp->name == NULL) {
msgq(sp, M_ERR,
"The %.*s command is unknown.", cmdlen, p);
return (1);
}
uselastcmd = 0;
/* Some commands are turned off. */
if (F_ISSET(cp, E_NOPERM)) {
msgq(sp, M_ERR,
"The %.*s command is not currently supported.",
cmdlen, p);
return (1);
}
/* Some commands aren't okay in globals. */
if (F_ISSET(sp, S_GLOBAL) && F_ISSET(cp, E_NOGLOBAL)) {
msgq(sp, M_ERR,
"The %.*s command can't be used as part of a global command.", cmdlen, p);
return (1);
}
/*
* Another "special" feature.
* NOTE: cmd.string is NOT nul terminated in this case.
*/
if (cp == &cmds[C_SHIFTL] && *exc == '<' ||
cp == &cmds[C_SHIFTR] && *exc == '>') {
ch = *exc;
for (cmd.string = exc; *++exc == ch;);
}
} else {
cp = sp->lastcmd;
uselastcmd = 1;
}
LF_INIT(cp->flags);
/*
* File state must be checked throughout this code, because it is
* called when reading the .exrc file and similar things. There's
* this little chicken and egg problem -- if we read the file first,
* we won't know how to display it. If we read/set the exrc stuff
* first, we can't allow any command that requires file state.
* Historic vi generally took the easy way out and dropped core.
*/
if (LF_ISSET(E_NORC) && ep == NULL) {
msgq(sp, M_ERR,
"The %s command requires a file to already have been read in.",
cp->name);
return (1);
}
/*
* Set the default addresses. It's an error to specify an address for
* a command that doesn't take them. If two addresses are specified
* for a command that only takes one, lose the first one. Two special
* cases here, some commands take 0 or 2 addresses. For most of them
* (the E_ADDR2_ALL flag), 0 defaults to the entire file. For one
* (the `!' command, the E_ADDR2_NONE flag), 0 defaults to no lines.
*
* Also, if the file is empty, some commands want to use an address of
* 0, i.e. the entire file is 0 to 0, and the default first address is
* 0. Otherwise, an entire file is 1 to N and the default line is 1.
* Note, we also add the E_ZERO flag to the command flags, for the case
* where the 0 address is only valid if it's a default address.
*/
flagoff = 0;
switch(flags & (E_ADDR1|E_ADDR2|E_ADDR2_ALL|E_ADDR2_NONE)) {
case E_ADDR1: /* One address: */
switch(cmd.addrcnt) {
case 0: /* Default cursor/empty file. */
cmd.addrcnt = 1;
if (LF_ISSET(E_ZERODEF)) {
if (file_lline(sp, ep, &lno))
return (1);
if (lno == 0) {
cmd.addr1.lno = 0;
LF_SET(E_ZERO);
} else
cmd.addr1.lno = sp->lno;
} else
cmd.addr1.lno = sp->lno;
cmd.addr1.cno = sp->cno;
break;
case 1:
break;
case 2: /* Lose the first address. */
cmd.addrcnt = 1;
cmd.addr1 = cmd.addr2;
}
break;
case E_ADDR2_NONE: /* Zero/two addresses: */
if (cmd.addrcnt == 0) /* Default to nothing. */
break;
goto two;
case E_ADDR2_ALL: /* Zero/two addresses: */
if (cmd.addrcnt == 0) { /* Default entire/empty file. */
cmd.addrcnt = 2;
if (file_lline(sp, ep, &cmd.addr2.lno))
return (1);
if (LF_ISSET(E_ZERODEF) && cmd.addr2.lno == 0) {
cmd.addr1.lno = 0;
LF_SET(E_ZERO);
} else
cmd.addr1.lno = 1;
cmd.addr1.cno = cmd.addr2.cno = 0;
cmd.flags |= E_ADDR2_ALL;
break;
}
/* FALLTHROUGH */
case E_ADDR2: /* Two addresses: */
two: switch(cmd.addrcnt) {
case 0: /* Default cursor/empty file. */
cmd.addrcnt = 2;
if (LF_ISSET(E_ZERODEF) && sp->lno == 1) {
if (file_lline(sp, ep, &lno))
return (1);
if (lno == 0) {
cmd.addr1.lno = cmd.addr2.lno = 0;
LF_SET(E_ZERO);
} else
cmd.addr1.lno = cmd.addr2.lno = sp->lno;
} else
cmd.addr1.lno = cmd.addr2.lno = sp->lno;
cmd.addr1.cno = cmd.addr2.cno = sp->cno;
break;
case 1: /* Default to first address. */
cmd.addrcnt = 2;
cmd.addr2 = cmd.addr1;
break;
case 2:
break;
}
break;
default:
if (cmd.addrcnt) /* Error. */
goto usage;
}
/*
* If the entire string is parsed by the command itself, we don't
* even skip leading white-space, it's significant for some commands.
* However, require that there be *something*.
*/
if (cp->syntax[0] == 's') {
for (p = exc; *p && isspace(*p); ++p);
cmd.string = *p ? exc : NULL;
goto addr2;
}
for (lcount = 0, p = cp->syntax; *p; ++p) {
for (; isspace(*exc); ++exc); /* Skip whitespace. */
if (!*exc)
break;
switch (*p) {
case '!': /* ! */
if (*exc == '!') {
++exc;
cmd.flags |= E_FORCE;
}
break;
case '+': /* +cmd */
if (*exc != '+')
break;
for (cmd.plus = ++exc; !isspace(*exc); ++exc);
if (*exc)
*exc++ = '\0';
break;
case '1': /* #, l, p */
for (; *exc == '+' || *exc == '-'; ++exc)
switch (*exc) {
case '+':
++flagoff;
break;
case '-':
--flagoff;
break;
}
for (; *exc == '#' || *exc == 'l' || *exc == 'p'; ++exc)
switch (*exc) {
case '#':
cmd.flags |= E_F_HASH;
break;
case 'l':
cmd.flags |= E_F_LIST;
break;
case 'p':
cmd.flags |= E_F_PRINT;
break;
}
for (; *exc == '+' || *exc == '-'; ++exc)
switch (*exc) {
case '+':
++flagoff;
break;
case '-':
--flagoff;
break;
}
break;
case '2': /* -, ., +, ^ */
for (;; ++exc)
switch (*exc) {
case '-':
cmd.flags |= E_F_DASH;
break;
case '.':
cmd.flags |= E_F_DOT;
break;
case '+':
cmd.flags |= E_F_PLUS;
break;
case '^':
cmd.flags |= E_F_CARAT;
break;
default:
goto end2;
}
end2: break;
#ifdef XXX_THIS_NO_LONGER_USED
case '>': /* >> */
if (exc[0] == '>' && exc[1] == '>') {
cmd.flags |= E_APPEND;
exc += 2;
}
break;
#endif
case 'b': /* buffer */
cmd.buffer = *exc++;
break;
case 'c': /* count */
if (isdigit(*exc)) {
lcount = strtol(exc, &endp, 10);
if (lcount == 0) {
msgq(sp, M_ERR,
"Count may not be zero.");
return (1);
}
exc = endp;
/*
* Fix up the addresses. Count's only occur
* with commands taking two addresses. The
* historic vi practice was to use the count
* as an offset from the *second* address.
*/
cmd.addr1 = cmd.addr2;
cmd.addr2.lno = cmd.addr1.lno + lcount - 1;
}
break;
case 'l': /* line */
if (isdigit(*exc)) {
cmd.lineno = strtol(exc, &endp, 10);
exc = endp;
}
break;
case 'f': /* file */
if (buildargv(sp, ep, exc, 1, &cmd.argc, &cmd.argv))
return (1);
goto countchk;
case 's': /* string */
for (p = exc; *p && isspace(*p); ++p);
cmd.string = *p ? exc : NULL;
goto addr2;
case 'w': /* word */
if (buildargv(sp, ep, exc, 0, &cmd.argc, &cmd.argv))
return (1);
countchk: if (*++p != 'N') { /* N */
/*
* If a number is specified, must either be
* 0 or that number, if optional, and that
* number, if required.
*/
num = *p - '0';
if ((*++p != 'o' || cmd.argc != 0) &&
cmd.argc != num)
goto usage;
}
goto addr2;
default:
msgq(sp, M_ERR,
"Internal syntax table error (%s).", cp->name);
}
}
/*
* Shouldn't be anything left, and no more required fields.
* That means neither 'l' or 'r' in the syntax.
*/
for (; *exc && isspace(*exc); ++exc);
if (*exc || strpbrk(p, "lr")) {
usage: msgq(sp, M_ERR, "Usage: %s.", cp->usage);
return (1);
}
/* Verify that the addresses are legal. */
addr2: switch(cmd.addrcnt) {
case 2:
if (file_lline(sp, ep, &lcount))
return (1);
if (cmd.addr2.lno > lcount) {
if (lcount == 0)
msgq(sp, M_ERR, "The file is empty.");
else
msgq(sp, M_ERR,
"Only %lu line%s in the file",
lcount, lcount > 1 ? "s" : "");
return (1);
}
/* FALLTHROUGH */
case 1:
num = cmd.addr1.lno;
/*
* If it's a "default vi command", zero is okay. Historic
* vi allowed this, note, it's also the hack that allows
* "vi + nonexistent_file" to work.
*/
if (num == 0 && (!F_ISSET(sp, S_MODE_VI) || uselastcmd != 1) &&
!LF_ISSET(E_ZERO)) {
msgq(sp, M_ERR,
"The %s command doesn't permit an address of 0.",
cp->name);
return (1);
}
if (file_lline(sp, ep, &lcount))
return (1);
if (num > lcount) {
msgq(sp, M_ERR, "Only %lu line%s in the file",
lcount, lcount > 1 ? "s" : "");
return (1);
}
break;
}
/* If doing a default command, vi just moves to the line. */
if (F_ISSET(sp, S_MODE_VI) && uselastcmd) {
sp->lno = cmd.addr1.lno ? cmd.addr1.lno : 1;
sp->cno = cmd.addr1.cno;
return (0);
}
/* Reset "last" command. */
if (LF_ISSET(E_SETLAST))
sp->lastcmd = cp;
cmd.cmd = cp;
#if DEBUG && 0
{
int __cnt;
TRACE(sp, "ex_cmd: %s", cmd.cmd->name);
if (cmd.addrcnt > 0) {
TRACE(sp, "\taddr1 %d", cmd.addr1.lno);
if (cmd.addrcnt > 1)
TRACE(sp, " addr2: %d", cmd.addr2.lno);
TRACE(sp, "\n");
}
if (cmd.lineno)
TRACE(sp, "\tlineno %d", cmd.lineno);
if (cmd.flags)
TRACE(sp, "\tflags %0x", cmd.flags);
if (cmd.command)
TRACE(sp, "\tcommand %s", cmd.command);
if (cmd.plus)
TRACE(sp, "\tplus %s", cmd.plus);
if (cmd.buffer != OOBCB)
TRACE(sp, "\tbuffer %c", cmd.buffer);
TRACE(sp, "\n");
if (cmd.argc) {
for (__cnt = 0; __cnt < cmd.argc; ++__cnt)
TRACE(sp, "\targ %d: {%s}", __cnt, cmd.argv[__cnt]);
TRACE(sp, "\n");
}
}
#endif
/* Clear autoprint. */
F_CLR(sp, S_AUTOPRINT);
/*
* If file state and not doing a global command, log the start of
* an action.
*/
if (ep != NULL && !F_ISSET(sp, S_GLOBAL))
(void)log_cursor(sp, ep);
/* Save the current mode. */
saved_mode = F_ISSET(sp, S_MODE_EX | S_MODE_VI | S_MAJOR_CHANGE);
/* Do the command. */
if ((cp->fn)(sp, ep, &cmd))
return (1);
/* If the world changed, we're done. */
if (saved_mode != F_ISSET(sp, S_MODE_EX | S_MODE_VI | S_MAJOR_CHANGE))
return (0);
/* If just starting up, or not in ex mode, we're done. */
if (ep == NULL || !F_ISSET(sp, S_MODE_EX))
return (0);
/*
* The print commands have already handled the `print' flags.
* If so, clear them. Don't return, autoprint may still have
* stuff to print out.
*/
if (LF_ISSET(E_F_PRCLEAR))
cmd.flags &= ~(E_F_HASH | E_F_LIST | E_F_PRINT);
/*
* If the command was successful, and there was an explicit flag to
* display the new cursor line, or we're in ex mode, autoprint is set,
* and a change was made, display the line.
*/
if (flagoff) {
if (flagoff < 0) {
if ((recno_t)flagoff > sp->lno) {
msgq(sp, M_ERR,
"Flag offset before line 1.");
return (1);
}
} else {
if (file_lline(sp, ep, &lno))
return (1);
if (sp->lno + flagoff > lno) {
msgq(sp, M_ERR,
"Flag offset past end-of-file.");
return (1);
}
}
sp->lno += flagoff;
}
if (F_ISSET(sp, S_AUTOPRINT) && O_ISSET(sp, O_AUTOPRINT))
LF_INIT(E_F_PRINT);
else
LF_INIT(cmd.flags & (E_F_HASH | E_F_LIST | E_F_PRINT));
memset(&cmd, 0, sizeof(EXCMDARG));
cmd.addrcnt = 2;
cmd.addr1.lno = cmd.addr2.lno = sp->lno;
cmd.addr1.cno = cmd.addr2.cno = sp->cno;
if (flags) {
switch (flags) {
case E_F_HASH:
cmd.cmd = &cmds[C_HASH];
ex_number(sp, ep, &cmd);
break;
case E_F_LIST:
cmd.cmd = &cmds[C_LIST];
ex_list(sp, ep, &cmd);
break;
case E_F_PRINT:
cmd.cmd = &cmds[C_PRINT];
ex_pr(sp, ep, &cmd);
break;
}
}
return (0);
}
/*
* linespec --
* Parse a line specifier for ex commands.
*
* XXX
* Currently ignores any character quoting. Not sure that's right.
*/
static char *
linespec(sp, ep, cmd, cp)
SCR *sp;
EXF *ep;
char *cmd;
EXCMDARG *cp;
{
MARK cur, savecursor, sm, *mp;
long num, total;
int savecursor_set;
char *endp;
/* Percent character is all lines in the file. */
if (*cmd == '%') {
cp->addr1.lno = 1;
if (file_lline(sp, ep, &cp->addr2.lno))
return (NULL);
/* If an empty file, then the first line is 0, not 1. */
if (cp->addr2.lno == 0)
cp->addr1.lno = 0;
cp->addr1.cno = cp->addr2.cno = 0;
cp->addrcnt = 2;
return (++cmd);
}
/* Parse comma or semi-colon delimited line specs. */
savecursor_set = 0;
for (cp->addrcnt = 0;;) {
switch(*cmd) {
case ';': /* Semi-colon delimiter. */
/*
* Comma delimiters delimit; semi-colon delimiters
* change the current address for the 2nd address
* to be the first address. Trailing or multiple
* delimiters are discarded.
*/
if (cp->addrcnt == 0)
goto done;
if (!savecursor_set) {
savecursor.lno = sp->lno;
savecursor.cno = sp->cno;
sp->lno = cp->addr1.lno;
sp->cno = cp->addr1.cno;
}
savecursor_set = 1;
/* FALLTHROUGH */
case ',': /* Comma delimiter. */
++cmd;
break;
case '$': /* Last line. */
if (file_lline(sp, ep, &cur.lno))
return (NULL);
cur.cno = 0;
++cmd;
break;
/* Absolute line number. */
case '0': case '1': case '2': case '3': case '4':
case '5': case '6': case '7': case '8': case '9':
cur.lno = strtol(cmd, &endp, 10);
cur.cno = 0;
cmd = endp;
break;
case '\'': /* Set mark. */
if (cmd[1] == '\0') {
msgq(sp, M_ERR,
"No mark name; use 'a' to 'z'.");
return (NULL);
}
if ((mp = mark_get(sp, ep, cmd[1])) == NULL)
return (NULL);
cur = *mp;
cmd += 2;
break;
case '/': /* Search forward. */
sm.lno = sp->lno;
sm.cno = sp->cno;
if (f_search(sp, ep,
&sm, &sm, cmd, &endp, SEARCH_MSG | SEARCH_PARSE))
return (NULL);
cur.lno = sp->lno = sm.lno;
cur.cno = sp->cno = sm.cno;
cmd = endp;
break;
case '?': /* Search backward. */
sm.lno = sp->lno;
sm.cno = sp->cno;
if (b_search(sp, ep,
&sm, &sm, cmd, &endp, SEARCH_MSG | SEARCH_PARSE))
return (NULL);
cur.lno = sp->lno = sm.lno;
cur.cno = sp->cno = sm.cno;
cmd = endp;
break;
case '.': /* Current position. */
++cmd;
/* FALLTHROUGH */
case '+': /* Increment. */
case '-': /* Decrement. */
/* If an empty file, then '.' is 0, not 1. */
if (sp->lno == 1) {
if (file_lline(sp, ep, &cur.lno))
return (NULL);
if (cur.lno != 0)
cur.lno = 1;
} else
cur.lno = sp->lno;
cur.cno = sp->cno;
break;
default:
goto done;
}
/*
* Evaluate any offset. Offsets are +/- any number, or,
* any number of +/- signs, or any combination thereof.
*/
for (total = 0; *cmd == '-' || *cmd == '+'; total += num) {
num = *cmd == '-' ? -1 : 1;
if (isdigit(*++cmd)) {
num *= strtol(cmd, &endp, 10);
cmd = endp;
}
}
if (total < 0 && -total > cur.lno) {
msgq(sp, M_ERR,
"Reference to a line number less than 0.");
return (NULL);
}
cur.lno += total;
/* Extra addresses are discarded, starting with the first. */
switch(cp->addrcnt) {
case 0:
cp->addr1 = cur;
cp->addrcnt = 1;
break;
case 1:
cp->addr2 = cur;
cp->addrcnt = 2;
break;
case 2:
cp->addr1 = cp->addr2;
cp->addr2 = cur;
break;
}
}
/*
* XXX
* This is probably not right for treatment of savecursor -- figure
* out what the historical ex did for ";,;,;5p" or similar stupidity.
*/
done: if (savecursor_set) {
sp->lno = savecursor.lno;
sp->cno = savecursor.cno;
}
return (cmd);
}