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