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