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