1/* $NetBSD: lesskey_parse.c,v 1.2 2023/10/06 05:49:49 simonb Exp $ */ 2 3/* 4 * Copyright (C) 1984-2023 Mark Nudelman 5 * 6 * You may distribute under the terms of either the GNU General Public 7 * License or the Less License, as specified in the README file. 8 * 9 * For more information, see the README file. 10 */ 11 12#include "defines.h" 13#include <stdio.h> 14#include <string.h> 15#include <stdlib.h> 16#include "lesskey.h" 17#include "cmd.h" 18#include "xbuf.h" 19 20#define CONTROL(c) ((c)&037) 21#define ESC CONTROL('[') 22 23extern void lesskey_parse_error(char *msg); 24extern char *homefile(char *filename); 25extern void *ecalloc(int count, unsigned int size); 26extern int lstrtoi(char *str, char **end, int radix); 27extern char version[]; 28 29static int linenum; 30static int errors; 31static int less_version = 0; 32static char *lesskey_file; 33 34static struct lesskey_cmdname cmdnames[] = 35{ 36 { "back-bracket", A_B_BRACKET }, 37 { "back-line", A_B_LINE }, 38 { "back-line-force", A_BF_LINE }, 39 { "back-screen", A_B_SCREEN }, 40 { "back-scroll", A_B_SCROLL }, 41 { "back-search", A_B_SEARCH }, 42 { "back-window", A_B_WINDOW }, 43 { "clear-mark", A_CLRMARK }, 44 { "debug", A_DEBUG }, 45 { "digit", A_DIGIT }, 46 { "display-flag", A_DISP_OPTION }, 47 { "display-option", A_DISP_OPTION }, 48 { "end", A_GOEND }, 49 { "end-scroll", A_RRSHIFT }, 50 { "examine", A_EXAMINE }, 51 { "filter", A_FILTER }, 52 { "first-cmd", A_FIRSTCMD }, 53 { "firstcmd", A_FIRSTCMD }, 54 { "flush-repaint", A_FREPAINT }, 55 { "forw-bracket", A_F_BRACKET }, 56 { "forw-forever", A_F_FOREVER }, 57 { "forw-until-hilite", A_F_UNTIL_HILITE }, 58 { "forw-line", A_F_LINE }, 59 { "forw-line-force", A_FF_LINE }, 60 { "forw-screen", A_F_SCREEN }, 61 { "forw-screen-force", A_FF_SCREEN }, 62 { "forw-scroll", A_F_SCROLL }, 63 { "forw-search", A_F_SEARCH }, 64 { "forw-window", A_F_WINDOW }, 65 { "goto-end", A_GOEND }, 66 { "goto-end-buffered", A_GOEND_BUF }, 67 { "goto-line", A_GOLINE }, 68 { "goto-mark", A_GOMARK }, 69 { "help", A_HELP }, 70 { "index-file", A_INDEX_FILE }, 71 { "invalid", A_UINVALID }, 72 { "left-scroll", A_LSHIFT }, 73 { "next-file", A_NEXT_FILE }, 74 { "next-tag", A_NEXT_TAG }, 75 { "noaction", A_NOACTION }, 76 { "no-scroll", A_LLSHIFT }, 77 { "percent", A_PERCENT }, 78 { "pipe", A_PIPE }, 79 { "prev-file", A_PREV_FILE }, 80 { "prev-tag", A_PREV_TAG }, 81 { "quit", A_QUIT }, 82 { "remove-file", A_REMOVE_FILE }, 83 { "repaint", A_REPAINT }, 84 { "repaint-flush", A_FREPAINT }, 85 { "repeat-search", A_AGAIN_SEARCH }, 86 { "repeat-search-all", A_T_AGAIN_SEARCH }, 87 { "reverse-search", A_REVERSE_SEARCH }, 88 { "reverse-search-all", A_T_REVERSE_SEARCH }, 89 { "right-scroll", A_RSHIFT }, 90 { "set-mark", A_SETMARK }, 91 { "set-mark-bottom", A_SETMARKBOT }, 92 { "shell", A_SHELL }, 93 { "pshell", A_PSHELL }, 94 { "status", A_STAT }, 95 { "toggle-flag", A_OPT_TOGGLE }, 96 { "toggle-option", A_OPT_TOGGLE }, 97 { "undo-hilite", A_UNDO_SEARCH }, 98 { "clear-search", A_CLR_SEARCH }, 99 { "version", A_VERSION }, 100 { "visual", A_VISUAL }, 101 { NULL, 0 } 102}; 103 104static struct lesskey_cmdname editnames[] = 105{ 106 { "back-complete", EC_B_COMPLETE }, 107 { "backspace", EC_BACKSPACE }, 108 { "delete", EC_DELETE }, 109 { "down", EC_DOWN }, 110 { "end", EC_END }, 111 { "expand", EC_EXPAND }, 112 { "forw-complete", EC_F_COMPLETE }, 113 { "home", EC_HOME }, 114 { "insert", EC_INSERT }, 115 { "invalid", EC_UINVALID }, 116 { "kill-line", EC_LINEKILL }, 117 { "abort", EC_ABORT }, 118 { "left", EC_LEFT }, 119 { "literal", EC_LITERAL }, 120 { "right", EC_RIGHT }, 121 { "up", EC_UP }, 122 { "word-backspace", EC_W_BACKSPACE }, 123 { "word-delete", EC_W_DELETE }, 124 { "word-left", EC_W_LEFT }, 125 { "word-right", EC_W_RIGHT }, 126 { NULL, 0 } 127}; 128 129/* 130 * Print a parse error message. 131 */ 132static void parse_error(char *fmt, char *arg1) 133{ 134 char buf[1024]; 135 int n = snprintf(buf, sizeof(buf), "%s: line %d: ", lesskey_file, linenum); 136 if (n >= 0 && n < sizeof(buf)) 137 snprintf(buf+n, sizeof(buf)-n, fmt, arg1); 138 ++errors; 139 lesskey_parse_error(buf); 140} 141 142/* 143 * Initialize lesskey_tables. 144 */ 145static void init_tables(struct lesskey_tables *tables) 146{ 147 tables->currtable = &tables->cmdtable; 148 149 tables->cmdtable.names = cmdnames; 150 tables->cmdtable.is_var = 0; 151 xbuf_init(&tables->cmdtable.buf); 152 153 tables->edittable.names = editnames; 154 tables->edittable.is_var = 0; 155 xbuf_init(&tables->edittable.buf); 156 157 tables->vartable.names = NULL; 158 tables->vartable.is_var = 1; 159 xbuf_init(&tables->vartable.buf); 160} 161 162#define CHAR_STRING_LEN 8 163 164static char * char_string(char *buf, int ch, int lit) 165{ 166 if (lit || (ch >= 0x20 && ch < 0x7f)) 167 { 168 buf[0] = ch; 169 buf[1] = '\0'; 170 } else 171 { 172 snprintf(buf, CHAR_STRING_LEN, "\\x%02x", ch); 173 } 174 return buf; 175} 176 177/* 178 * Increment char pointer by one up to terminating nul byte. 179 */ 180static char * increment_pointer(char *p) 181{ 182 if (*p == '\0') 183 return p; 184 return p+1; 185} 186 187/* 188 * Parse one character of a string. 189 */ 190static char * tstr(char **pp, int xlate) 191{ 192 char *p; 193 char ch; 194 int i; 195 static char buf[CHAR_STRING_LEN]; 196 static char tstr_control_k[] = 197 { SK_SPECIAL_KEY, SK_CONTROL_K, 6, 1, 1, 1, '\0' }; 198 199 p = *pp; 200 switch (*p) 201 { 202 case '\\': 203 ++p; 204 switch (*p) 205 { 206 case '0': case '1': case '2': case '3': 207 case '4': case '5': case '6': case '7': 208 /* 209 * Parse an octal number. 210 */ 211 ch = 0; 212 i = 0; 213 do 214 ch = 8*ch + (*p - '0'); 215 while (*++p >= '0' && *p <= '7' && ++i < 3); 216 *pp = p; 217 if (xlate && ch == CONTROL('K')) 218 return tstr_control_k; 219 return char_string(buf, ch, 1); 220 case 'b': 221 *pp = p+1; 222 return ("\b"); 223 case 'e': 224 *pp = p+1; 225 return char_string(buf, ESC, 1); 226 case 'n': 227 *pp = p+1; 228 return ("\n"); 229 case 'r': 230 *pp = p+1; 231 return ("\r"); 232 case 't': 233 *pp = p+1; 234 return ("\t"); 235 case 'k': 236 if (xlate) 237 { 238 switch (*++p) 239 { 240 case 'b': ch = SK_BACKSPACE; break; 241 case 'B': ch = SK_CTL_BACKSPACE; break; 242 case 'd': ch = SK_DOWN_ARROW; break; 243 case 'D': ch = SK_PAGE_DOWN; break; 244 case 'e': ch = SK_END; break; 245 case 'h': ch = SK_HOME; break; 246 case 'i': ch = SK_INSERT; break; 247 case 'l': ch = SK_LEFT_ARROW; break; 248 case 'L': ch = SK_CTL_LEFT_ARROW; break; 249 case 'r': ch = SK_RIGHT_ARROW; break; 250 case 'R': ch = SK_CTL_RIGHT_ARROW; break; 251 case 't': ch = SK_BACKTAB; break; 252 case 'u': ch = SK_UP_ARROW; break; 253 case 'U': ch = SK_PAGE_UP; break; 254 case 'x': ch = SK_DELETE; break; 255 case 'X': ch = SK_CTL_DELETE; break; 256 case '1': ch = SK_F1; break; 257 default: 258 parse_error("invalid escape sequence \"\\k%s\"", char_string(buf, *p, 0)); 259 *pp = increment_pointer(p); 260 return (""); 261 } 262 *pp = p+1; 263 buf[0] = SK_SPECIAL_KEY; 264 buf[1] = ch; 265 buf[2] = 6; 266 buf[3] = 1; 267 buf[4] = 1; 268 buf[5] = 1; 269 buf[6] = '\0'; 270 return (buf); 271 } 272 /* FALLTHRU */ 273 default: 274 /* 275 * Backslash followed by any other char 276 * just means that char. 277 */ 278 *pp = increment_pointer(p); 279 char_string(buf, *p, 1); 280 if (xlate && buf[0] == CONTROL('K')) 281 return tstr_control_k; 282 return (buf); 283 } 284 case '^': 285 /* 286 * Caret means CONTROL. 287 */ 288 *pp = increment_pointer(p+1); 289 char_string(buf, CONTROL(p[1]), 1); 290 if (xlate && buf[0] == CONTROL('K')) 291 return tstr_control_k; 292 return (buf); 293 } 294 *pp = increment_pointer(p); 295 char_string(buf, *p, 1); 296 if (xlate && buf[0] == CONTROL('K')) 297 return tstr_control_k; 298 return (buf); 299} 300 301static int issp(char ch) 302{ 303 return (ch == ' ' || ch == '\t'); 304} 305 306/* 307 * Skip leading spaces in a string. 308 */ 309static char * skipsp(char *s) 310{ 311 while (issp(*s)) 312 s++; 313 return (s); 314} 315 316/* 317 * Skip non-space characters in a string. 318 */ 319static char * skipnsp(char *s) 320{ 321 while (*s != '\0' && !issp(*s)) 322 s++; 323 return (s); 324} 325 326/* 327 * Clean up an input line: 328 * strip off the trailing newline & any trailing # comment. 329 */ 330static char * clean_line(char *s) 331{ 332 int i; 333 334 s = skipsp(s); 335 for (i = 0; s[i] != '\0' && s[i] != '\n' && s[i] != '\r'; i++) 336 if (s[i] == '#' && (i == 0 || s[i-1] != '\\')) 337 break; 338 s[i] = '\0'; 339 return (s); 340} 341 342/* 343 * Add a byte to the output command table. 344 */ 345static void add_cmd_char(unsigned char c, struct lesskey_tables *tables) 346{ 347 xbuf_add_byte(&tables->currtable->buf, c); 348} 349 350static void erase_cmd_char(struct lesskey_tables *tables) 351{ 352 xbuf_pop(&tables->currtable->buf); 353} 354 355/* 356 * Add a string to the output command table. 357 */ 358static void add_cmd_str(char *s, struct lesskey_tables *tables) 359{ 360 for ( ; *s != '\0'; s++) 361 add_cmd_char(*s, tables); 362} 363 364/* 365 * Does a given version number match the running version? 366 * Operator compares the running version to the given version. 367 */ 368static int match_version(char op, int ver) 369{ 370 switch (op) 371 { 372 case '>': return less_version > ver; 373 case '<': return less_version < ver; 374 case '+': return less_version >= ver; 375 case '-': return less_version <= ver; 376 case '=': return less_version == ver; 377 case '!': return less_version != ver; 378 default: return 0; /* cannot happen */ 379 } 380} 381 382/* 383 * Handle a #version line. 384 * If the version matches, return the part of the line that should be executed. 385 * Otherwise, return NULL. 386 */ 387static char * version_line(char *s, struct lesskey_tables *tables) 388{ 389 char op; 390 int ver; 391 char *e; 392 char buf[CHAR_STRING_LEN]; 393 394 s += strlen("#version"); 395 s = skipsp(s); 396 op = *s++; 397 /* Simplify 2-char op to one char. */ 398 switch (op) 399 { 400 case '<': if (*s == '=') { s++; op = '-'; } break; 401 case '>': if (*s == '=') { s++; op = '+'; } break; 402 case '=': if (*s == '=') { s++; } break; 403 case '!': if (*s == '=') { s++; } break; 404 default: 405 parse_error("invalid operator '%s' in #version line", char_string(buf, op, 0)); 406 return (NULL); 407 } 408 s = skipsp(s); 409 ver = lstrtoi(s, &e, 10); 410 if (e == s) 411 { 412 parse_error("non-numeric version number in #version line", ""); 413 return (NULL); 414 } 415 if (!match_version(op, ver)) 416 return (NULL); 417 return (e); 418} 419 420/* 421 * See if we have a special "control" line. 422 */ 423static char * control_line(char *s, struct lesskey_tables *tables) 424{ 425#define PREFIX(str,pat) (strncmp(str,pat,strlen(pat)) == 0) 426 427 if (PREFIX(s, "#line-edit")) 428 { 429 tables->currtable = &tables->edittable; 430 return (NULL); 431 } 432 if (PREFIX(s, "#command")) 433 { 434 tables->currtable = &tables->cmdtable; 435 return (NULL); 436 } 437 if (PREFIX(s, "#env")) 438 { 439 tables->currtable = &tables->vartable; 440 return (NULL); 441 } 442 if (PREFIX(s, "#stop")) 443 { 444 add_cmd_char('\0', tables); 445 add_cmd_char(A_END_LIST, tables); 446 return (NULL); 447 } 448 if (PREFIX(s, "#version")) 449 { 450 return (version_line(s, tables)); 451 } 452 return (s); 453} 454 455/* 456 * Find an action, given the name of the action. 457 */ 458static int findaction(char *actname, struct lesskey_tables *tables) 459{ 460 int i; 461 462 for (i = 0; tables->currtable->names[i].cn_name != NULL; i++) 463 if (strcmp(tables->currtable->names[i].cn_name, actname) == 0) 464 return (tables->currtable->names[i].cn_action); 465 parse_error("unknown action: \"%s\"", actname); 466 return (A_INVALID); 467} 468 469/* 470 * Parse a line describing one key binding, of the form 471 * KEY ACTION [EXTRA] 472 * where KEY is the user key sequence, ACTION is the 473 * resulting less action, and EXTRA is an "extra" user 474 * key sequence injected after the action. 475 */ 476static void parse_cmdline(char *p, struct lesskey_tables *tables) 477{ 478 char *actname; 479 int action; 480 char *s; 481 char c; 482 483 /* 484 * Parse the command string and store it in the current table. 485 */ 486 do 487 { 488 s = tstr(&p, 1); 489 add_cmd_str(s, tables); 490 } while (*p != '\0' && !issp(*p)); 491 /* 492 * Terminate the command string with a null byte. 493 */ 494 add_cmd_char('\0', tables); 495 496 /* 497 * Skip white space between the command string 498 * and the action name. 499 * Terminate the action name with a null byte. 500 */ 501 p = skipsp(p); 502 if (*p == '\0') 503 { 504 parse_error("missing action", ""); 505 return; 506 } 507 actname = p; 508 p = skipnsp(p); 509 c = *p; 510 *p = '\0'; 511 512 /* 513 * Parse the action name and store it in the current table. 514 */ 515 action = findaction(actname, tables); 516 517 /* 518 * See if an extra string follows the action name. 519 */ 520 *p = c; 521 p = skipsp(p); 522 if (*p == '\0') 523 { 524 add_cmd_char((unsigned char) action, tables); 525 } else 526 { 527 /* 528 * OR the special value A_EXTRA into the action byte. 529 * Put the extra string after the action byte. 530 */ 531 add_cmd_char((unsigned char) (action | A_EXTRA), tables); 532 while (*p != '\0') 533 add_cmd_str(tstr(&p, 0), tables); 534 add_cmd_char('\0', tables); 535 } 536} 537 538/* 539 * Parse a variable definition line, of the form 540 * NAME = VALUE 541 */ 542static void parse_varline(char *line, struct lesskey_tables *tables) 543{ 544 char *s; 545 char *p = line; 546 char *eq; 547 548 eq = strchr(line, '='); 549 if (eq != NULL && eq > line && eq[-1] == '+') 550 { 551 /* 552 * Rather ugly way of handling a += line. 553 * {{ Note that we ignore the variable name and 554 * just append to the previously defined variable. }} 555 */ 556 erase_cmd_char(tables); /* backspace over the final null */ 557 p = eq+1; 558 } else 559 { 560 do 561 { 562 s = tstr(&p, 0); 563 add_cmd_str(s, tables); 564 } while (*p != '\0' && !issp(*p) && *p != '='); 565 /* 566 * Terminate the variable name with a null byte. 567 */ 568 add_cmd_char('\0', tables); 569 p = skipsp(p); 570 if (*p++ != '=') 571 { 572 parse_error("missing = in variable definition", ""); 573 return; 574 } 575 add_cmd_char(EV_OK|A_EXTRA, tables); 576 } 577 p = skipsp(p); 578 while (*p != '\0') 579 { 580 s = tstr(&p, 0); 581 add_cmd_str(s, tables); 582 } 583 add_cmd_char('\0', tables); 584} 585 586/* 587 * Parse a line from the lesskey file. 588 */ 589static void parse_line(char *line, struct lesskey_tables *tables) 590{ 591 char *p; 592 593 /* 594 * See if it is a control line. 595 */ 596 p = control_line(line, tables); 597 if (p == NULL) 598 return; 599 /* 600 * Skip leading white space. 601 * Replace the final newline with a null byte. 602 * Ignore blank lines and comments. 603 */ 604 p = clean_line(p); 605 if (*p == '\0') 606 return; 607 608 if (tables->currtable->is_var) 609 parse_varline(p, tables); 610 else 611 parse_cmdline(p, tables); 612} 613 614/* 615 * Parse a lesskey source file and store result in tables. 616 */ 617int parse_lesskey(char *infile, struct lesskey_tables *tables) 618{ 619 FILE *desc; 620 char line[1024]; 621 622 if (infile == NULL) 623 infile = homefile(DEF_LESSKEYINFILE); 624 lesskey_file = infile; 625 626 init_tables(tables); 627 errors = 0; 628 linenum = 0; 629 if (less_version == 0) 630 less_version = lstrtoi(version, NULL, 10); 631 632 /* 633 * Open the input file. 634 */ 635 if (strcmp(infile, "-") == 0) 636 desc = stdin; 637 else if ((desc = fopen(infile, "r")) == NULL) 638 { 639 /* parse_error("cannot open lesskey file %s", infile); */ 640 return (-1); 641 } 642 643 /* 644 * Read and parse the input file, one line at a time. 645 */ 646 while (fgets(line, sizeof(line), desc) != NULL) 647 { 648 ++linenum; 649 parse_line(line, tables); 650 } 651 fclose(desc); 652 return (errors); 653} 654