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