1 /* 2 * Copyright (C) 1984-2011 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 about less, or for information on how to 8 * contact the author, see the README file. 9 */ 10 11 12 /* 13 * lesskey [-o output] [input] 14 * 15 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 16 * 17 * Make a .less file. 18 * If no input file is specified, standard input is used. 19 * If no output file is specified, $HOME/.less is used. 20 * 21 * The .less file is used to specify (to "less") user-defined 22 * key bindings. Basically any sequence of 1 to MAX_CMDLEN 23 * keystrokes may be bound to an existing less function. 24 * 25 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 26 * 27 * The input file is an ascii file consisting of a 28 * sequence of lines of the form: 29 * string <whitespace> action [chars] <newline> 30 * 31 * "string" is a sequence of command characters which form 32 * the new user-defined command. The command 33 * characters may be: 34 * 1. The actual character itself. 35 * 2. A character preceded by ^ to specify a 36 * control character (e.g. ^X means control-X). 37 * 3. A backslash followed by one to three octal digits 38 * to specify a character by its octal value. 39 * 4. A backslash followed by b, e, n, r or t 40 * to specify \b, ESC, \n, \r or \t, respectively. 41 * 5. Any character (other than those mentioned above) preceded 42 * by a \ to specify the character itself (characters which 43 * must be preceded by \ include ^, \, and whitespace. 44 * "action" is the name of a "less" action, from the table below. 45 * "chars" is an optional sequence of characters which is treated 46 * as keyboard input after the command is executed. 47 * 48 * Blank lines and lines which start with # are ignored, 49 * except for the special control lines: 50 * #command Signals the beginning of the command 51 * keys section. 52 * #line-edit Signals the beginning of the line-editing 53 * keys section. 54 * #env Signals the beginning of the environment 55 * variable section. 56 * #stop Stops command parsing in less; 57 * causes all default keys to be disabled. 58 * 59 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 60 * 61 * The output file is a non-ascii file, consisting of a header, 62 * one or more sections, and a trailer. 63 * Each section begins with a section header, a section length word 64 * and the section data. Normally there are three sections: 65 * CMD_SECTION Definition of command keys. 66 * EDIT_SECTION Definition of editing keys. 67 * END_SECTION A special section header, with no 68 * length word or section data. 69 * 70 * Section data consists of zero or more byte sequences of the form: 71 * string <0> <action> 72 * or 73 * string <0> <action|A_EXTRA> chars <0> 74 * 75 * "string" is the command string. 76 * "<0>" is one null byte. 77 * "<action>" is one byte containing the action code (the A_xxx value). 78 * If action is ORed with A_EXTRA, the action byte is followed 79 * by the null-terminated "chars" string. 80 * 81 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 82 */ 83 84 #include "less.h" 85 #include "lesskey.h" 86 #include "cmd.h" 87 88 struct cmdname 89 { 90 char *cn_name; 91 int cn_action; 92 }; 93 94 struct cmdname cmdnames[] = 95 { 96 { "back-bracket", A_B_BRACKET }, 97 { "back-line", A_B_LINE }, 98 { "back-line-force", A_BF_LINE }, 99 { "back-screen", A_B_SCREEN }, 100 { "back-scroll", A_B_SCROLL }, 101 { "back-search", A_B_SEARCH }, 102 { "back-window", A_B_WINDOW }, 103 { "debug", A_DEBUG }, 104 { "digit", A_DIGIT }, 105 { "display-flag", A_DISP_OPTION }, 106 { "display-option", A_DISP_OPTION }, 107 { "end", A_GOEND }, 108 { "examine", A_EXAMINE }, 109 { "filter", A_FILTER }, 110 { "first-cmd", A_FIRSTCMD }, 111 { "firstcmd", A_FIRSTCMD }, 112 { "flush-repaint", A_FREPAINT }, 113 { "forw-bracket", A_F_BRACKET }, 114 { "forw-forever", A_F_FOREVER }, 115 { "forw-line", A_F_LINE }, 116 { "forw-line-force", A_FF_LINE }, 117 { "forw-screen", A_F_SCREEN }, 118 { "forw-screen-force", A_FF_SCREEN }, 119 { "forw-scroll", A_F_SCROLL }, 120 { "forw-search", A_F_SEARCH }, 121 { "forw-window", A_F_WINDOW }, 122 { "goto-end", A_GOEND }, 123 { "goto-line", A_GOLINE }, 124 { "goto-mark", A_GOMARK }, 125 { "help", A_HELP }, 126 { "index-file", A_INDEX_FILE }, 127 { "invalid", A_UINVALID }, 128 { "left-scroll", A_LSHIFT }, 129 { "next-file", A_NEXT_FILE }, 130 { "next-tag", A_NEXT_TAG }, 131 { "noaction", A_NOACTION }, 132 { "percent", A_PERCENT }, 133 { "pipe", A_PIPE }, 134 { "prev-file", A_PREV_FILE }, 135 { "prev-tag", A_PREV_TAG }, 136 { "quit", A_QUIT }, 137 { "remove-file", A_REMOVE_FILE }, 138 { "repaint", A_REPAINT }, 139 { "repaint-flush", A_FREPAINT }, 140 { "repeat-search", A_AGAIN_SEARCH }, 141 { "repeat-search-all", A_T_AGAIN_SEARCH }, 142 { "reverse-search", A_REVERSE_SEARCH }, 143 { "reverse-search-all", A_T_REVERSE_SEARCH }, 144 { "right-scroll", A_RSHIFT }, 145 { "set-mark", A_SETMARK }, 146 { "shell", A_SHELL }, 147 { "status", A_STAT }, 148 { "toggle-flag", A_OPT_TOGGLE }, 149 { "toggle-option", A_OPT_TOGGLE }, 150 { "undo-hilite", A_UNDO_SEARCH }, 151 { "version", A_VERSION }, 152 { "visual", A_VISUAL }, 153 { NULL, 0 } 154 }; 155 156 struct cmdname editnames[] = 157 { 158 { "back-complete", EC_B_COMPLETE }, 159 { "backspace", EC_BACKSPACE }, 160 { "delete", EC_DELETE }, 161 { "down", EC_DOWN }, 162 { "end", EC_END }, 163 { "expand", EC_EXPAND }, 164 { "forw-complete", EC_F_COMPLETE }, 165 { "home", EC_HOME }, 166 { "insert", EC_INSERT }, 167 { "invalid", EC_UINVALID }, 168 { "kill-line", EC_LINEKILL }, 169 { "abort", EC_ABORT }, 170 { "left", EC_LEFT }, 171 { "literal", EC_LITERAL }, 172 { "right", EC_RIGHT }, 173 { "up", EC_UP }, 174 { "word-backspace", EC_W_BACKSPACE }, 175 { "word-delete", EC_W_DELETE }, 176 { "word-left", EC_W_LEFT }, 177 { "word-right", EC_W_RIGHT }, 178 { NULL, 0 } 179 }; 180 181 struct table 182 { 183 struct cmdname *names; 184 char *pbuffer; 185 char buffer[MAX_USERCMD]; 186 }; 187 188 struct table cmdtable; 189 struct table edittable; 190 struct table vartable; 191 struct table *currtable = &cmdtable; 192 193 char fileheader[] = { 194 C0_LESSKEY_MAGIC, 195 C1_LESSKEY_MAGIC, 196 C2_LESSKEY_MAGIC, 197 C3_LESSKEY_MAGIC 198 }; 199 char filetrailer[] = { 200 C0_END_LESSKEY_MAGIC, 201 C1_END_LESSKEY_MAGIC, 202 C2_END_LESSKEY_MAGIC 203 }; 204 char cmdsection[1] = { CMD_SECTION }; 205 char editsection[1] = { EDIT_SECTION }; 206 char varsection[1] = { VAR_SECTION }; 207 char endsection[1] = { END_SECTION }; 208 209 char *infile = NULL; 210 char *outfile = NULL ; 211 212 int linenum; 213 int errors; 214 215 extern char version[]; 216 217 void 218 usage() 219 { 220 fprintf(stderr, "usage: lesskey [-o output] [input]\n"); 221 exit(1); 222 } 223 224 char * 225 mkpathname(dirname, filename) 226 char *dirname; 227 char *filename; 228 { 229 char *pathname; 230 size_t len; 231 232 len = strlen(dirname) + strlen(filename) + 2; 233 pathname = calloc(len, sizeof(char)); 234 strlcpy(pathname, dirname, len); 235 strlcat(pathname, PATHNAME_SEP, len); 236 strlcat(pathname, filename, len); 237 return (pathname); 238 } 239 240 /* 241 * Figure out the name of a default file (in the user's HOME directory). 242 */ 243 char * 244 homefile(filename) 245 char *filename; 246 { 247 char *p; 248 char *pathname; 249 250 if ((p = getenv("HOME")) != NULL && *p != '\0') 251 pathname = mkpathname(p, filename); 252 #if OS2 253 else if ((p = getenv("INIT")) != NULL && *p != '\0') 254 pathname = mkpathname(p, filename); 255 #endif 256 else 257 { 258 fprintf(stderr, "cannot find $HOME - using current directory\n"); 259 pathname = mkpathname(".", filename); 260 } 261 return (pathname); 262 } 263 264 /* 265 * Parse command line arguments. 266 */ 267 void 268 parse_args(argc, argv) 269 int argc; 270 char **argv; 271 { 272 char *arg; 273 274 outfile = NULL; 275 while (--argc > 0) 276 { 277 arg = *++argv; 278 if (arg[0] != '-') 279 /* Arg does not start with "-"; it's not an option. */ 280 break; 281 if (arg[1] == '\0') 282 /* "-" means standard input. */ 283 break; 284 if (arg[1] == '-' && arg[2] == '\0') 285 { 286 /* "--" means end of options. */ 287 argc--; 288 argv++; 289 break; 290 } 291 switch (arg[1]) 292 { 293 case '-': 294 if (strncmp(arg, "--output", 8) == 0) 295 { 296 if (arg[8] == '\0') 297 outfile = &arg[8]; 298 else if (arg[8] == '=') 299 outfile = &arg[9]; 300 else 301 usage(); 302 goto opt_o; 303 } 304 if (strcmp(arg, "--version") == 0) 305 { 306 goto opt_V; 307 } 308 usage(); 309 break; 310 case 'o': 311 outfile = &argv[0][2]; 312 opt_o: 313 if (*outfile == '\0') 314 { 315 if (--argc <= 0) 316 usage(); 317 outfile = *(++argv); 318 } 319 break; 320 case 'V': 321 opt_V: 322 printf("lesskey version %s\n", version); 323 exit(0); 324 default: 325 usage(); 326 } 327 } 328 if (argc > 1) 329 usage(); 330 /* 331 * Open the input file, or use DEF_LESSKEYINFILE if none specified. 332 */ 333 if (argc > 0) 334 infile = *argv; 335 else 336 infile = homefile(DEF_LESSKEYINFILE); 337 } 338 339 /* 340 * Initialize data structures. 341 */ 342 void 343 init_tables() 344 { 345 cmdtable.names = cmdnames; 346 cmdtable.pbuffer = cmdtable.buffer; 347 348 edittable.names = editnames; 349 edittable.pbuffer = edittable.buffer; 350 351 vartable.names = NULL; 352 vartable.pbuffer = vartable.buffer; 353 } 354 355 /* 356 * Parse one character of a string. 357 */ 358 char * 359 tstr(pp, xlate) 360 char **pp; 361 int xlate; 362 { 363 register char *p; 364 register char ch; 365 register int i; 366 static char buf[10]; 367 static char tstr_control_k[] = 368 { SK_SPECIAL_KEY, SK_CONTROL_K, 6, 1, 1, 1, '\0' }; 369 370 p = *pp; 371 switch (*p) 372 { 373 case '\\': 374 ++p; 375 switch (*p) 376 { 377 case '0': case '1': case '2': case '3': 378 case '4': case '5': case '6': case '7': 379 /* 380 * Parse an octal number. 381 */ 382 ch = 0; 383 i = 0; 384 do 385 ch = 8*ch + (*p - '0'); 386 while (*++p >= '0' && *p <= '7' && ++i < 3); 387 *pp = p; 388 if (xlate && ch == CONTROL('K')) 389 return tstr_control_k; 390 buf[0] = ch; 391 buf[1] = '\0'; 392 return (buf); 393 case 'b': 394 *pp = p+1; 395 return ("\b"); 396 case 'e': 397 *pp = p+1; 398 buf[0] = ESC; 399 buf[1] = '\0'; 400 return (buf); 401 case 'n': 402 *pp = p+1; 403 return ("\n"); 404 case 'r': 405 *pp = p+1; 406 return ("\r"); 407 case 't': 408 *pp = p+1; 409 return ("\t"); 410 case 'k': 411 if (xlate) 412 { 413 switch (*++p) 414 { 415 case 'u': ch = SK_UP_ARROW; break; 416 case 'd': ch = SK_DOWN_ARROW; break; 417 case 'r': ch = SK_RIGHT_ARROW; break; 418 case 'l': ch = SK_LEFT_ARROW; break; 419 case 'U': ch = SK_PAGE_UP; break; 420 case 'D': ch = SK_PAGE_DOWN; break; 421 case 'h': ch = SK_HOME; break; 422 case 'e': ch = SK_END; break; 423 case 'x': ch = SK_DELETE; break; 424 default: 425 error("illegal char after \\k"); 426 *pp = p+1; 427 return (""); 428 } 429 *pp = p+1; 430 buf[0] = SK_SPECIAL_KEY; 431 buf[1] = ch; 432 buf[2] = 6; 433 buf[3] = 1; 434 buf[4] = 1; 435 buf[5] = 1; 436 buf[6] = '\0'; 437 return (buf); 438 } 439 /* FALLTHRU */ 440 default: 441 /* 442 * Backslash followed by any other char 443 * just means that char. 444 */ 445 *pp = p+1; 446 buf[0] = *p; 447 buf[1] = '\0'; 448 if (xlate && buf[0] == CONTROL('K')) 449 return tstr_control_k; 450 return (buf); 451 } 452 case '^': 453 /* 454 * Carat means CONTROL. 455 */ 456 *pp = p+2; 457 buf[0] = CONTROL(p[1]); 458 buf[1] = '\0'; 459 if (buf[0] == CONTROL('K')) 460 return tstr_control_k; 461 return (buf); 462 } 463 *pp = p+1; 464 buf[0] = *p; 465 buf[1] = '\0'; 466 if (xlate && buf[0] == CONTROL('K')) 467 return tstr_control_k; 468 return (buf); 469 } 470 471 /* 472 * Skip leading spaces in a string. 473 */ 474 public char * 475 skipsp(s) 476 register char *s; 477 { 478 while (*s == ' ' || *s == '\t') 479 s++; 480 return (s); 481 } 482 483 /* 484 * Skip non-space characters in a string. 485 */ 486 public char * 487 skipnsp(s) 488 register char *s; 489 { 490 while (*s != '\0' && *s != ' ' && *s != '\t') 491 s++; 492 return (s); 493 } 494 495 /* 496 * Clean up an input line: 497 * strip off the trailing newline & any trailing # comment. 498 */ 499 char * 500 clean_line(s) 501 char *s; 502 { 503 register int i; 504 505 s = skipsp(s); 506 for (i = 0; s[i] != '\n' && s[i] != '\r' && s[i] != '\0'; i++) 507 if (s[i] == '#' && (i == 0 || s[i-1] != '\\')) 508 break; 509 s[i] = '\0'; 510 return (s); 511 } 512 513 /* 514 * Add a byte to the output command table. 515 */ 516 void 517 add_cmd_char(c) 518 int c; 519 { 520 if (currtable->pbuffer >= currtable->buffer + MAX_USERCMD) 521 { 522 error("too many commands"); 523 exit(1); 524 } 525 *(currtable->pbuffer)++ = c; 526 } 527 528 /* 529 * Add a string to the output command table. 530 */ 531 void 532 add_cmd_str(s) 533 char *s; 534 { 535 for ( ; *s != '\0'; s++) 536 add_cmd_char(*s); 537 } 538 539 /* 540 * See if we have a special "control" line. 541 */ 542 int 543 control_line(s) 544 char *s; 545 { 546 #define PREFIX(str,pat) (strncmp(str,pat,strlen(pat)) == 0) 547 548 if (PREFIX(s, "#line-edit")) 549 { 550 currtable = &edittable; 551 return (1); 552 } 553 if (PREFIX(s, "#command")) 554 { 555 currtable = &cmdtable; 556 return (1); 557 } 558 if (PREFIX(s, "#env")) 559 { 560 currtable = &vartable; 561 return (1); 562 } 563 if (PREFIX(s, "#stop")) 564 { 565 add_cmd_char('\0'); 566 add_cmd_char(A_END_LIST); 567 return (1); 568 } 569 return (0); 570 } 571 572 /* 573 * Output some bytes. 574 */ 575 void 576 fputbytes(fd, buf, len) 577 FILE *fd; 578 char *buf; 579 int len; 580 { 581 while (len-- > 0) 582 { 583 fwrite(buf, sizeof(char), 1, fd); 584 buf++; 585 } 586 } 587 588 /* 589 * Output an integer, in special KRADIX form. 590 */ 591 void 592 fputint(fd, val) 593 FILE *fd; 594 unsigned int val; 595 { 596 char c; 597 598 if (val >= KRADIX*KRADIX) 599 { 600 fprintf(stderr, "error: integer too big (%d > %d)\n", 601 val, KRADIX*KRADIX); 602 exit(1); 603 } 604 c = val % KRADIX; 605 fwrite(&c, sizeof(char), 1, fd); 606 c = val / KRADIX; 607 fwrite(&c, sizeof(char), 1, fd); 608 } 609 610 /* 611 * Find an action, given the name of the action. 612 */ 613 int 614 findaction(actname) 615 char *actname; 616 { 617 int i; 618 619 for (i = 0; currtable->names[i].cn_name != NULL; i++) 620 if (strcmp(currtable->names[i].cn_name, actname) == 0) 621 return (currtable->names[i].cn_action); 622 error("unknown action"); 623 return (A_INVALID); 624 } 625 626 void 627 error(s) 628 char *s; 629 { 630 fprintf(stderr, "line %d: %s\n", linenum, s); 631 errors++; 632 } 633 634 635 void 636 parse_cmdline(p) 637 char *p; 638 { 639 int cmdlen; 640 char *actname; 641 int action; 642 char *s; 643 char c; 644 645 /* 646 * Parse the command string and store it in the current table. 647 */ 648 cmdlen = 0; 649 do 650 { 651 s = tstr(&p, 1); 652 cmdlen += strlen(s); 653 if (cmdlen > MAX_CMDLEN) 654 error("command too long"); 655 else 656 add_cmd_str(s); 657 } while (*p != ' ' && *p != '\t' && *p != '\0'); 658 /* 659 * Terminate the command string with a null byte. 660 */ 661 add_cmd_char('\0'); 662 663 /* 664 * Skip white space between the command string 665 * and the action name. 666 * Terminate the action name with a null byte. 667 */ 668 p = skipsp(p); 669 if (*p == '\0') 670 { 671 error("missing action"); 672 return; 673 } 674 actname = p; 675 p = skipnsp(p); 676 c = *p; 677 *p = '\0'; 678 679 /* 680 * Parse the action name and store it in the current table. 681 */ 682 action = findaction(actname); 683 684 /* 685 * See if an extra string follows the action name. 686 */ 687 *p = c; 688 p = skipsp(p); 689 if (*p == '\0') 690 { 691 add_cmd_char(action); 692 } else 693 { 694 /* 695 * OR the special value A_EXTRA into the action byte. 696 * Put the extra string after the action byte. 697 */ 698 add_cmd_char(action | A_EXTRA); 699 while (*p != '\0') 700 add_cmd_str(tstr(&p, 0)); 701 add_cmd_char('\0'); 702 } 703 } 704 705 void 706 parse_varline(p) 707 char *p; 708 { 709 char *s; 710 711 do 712 { 713 s = tstr(&p, 0); 714 add_cmd_str(s); 715 } while (*p != ' ' && *p != '\t' && *p != '=' && *p != '\0'); 716 /* 717 * Terminate the variable name with a null byte. 718 */ 719 add_cmd_char('\0'); 720 721 p = skipsp(p); 722 if (*p++ != '=') 723 { 724 error("missing ="); 725 return; 726 } 727 728 add_cmd_char(EV_OK|A_EXTRA); 729 730 p = skipsp(p); 731 while (*p != '\0') 732 { 733 s = tstr(&p, 0); 734 add_cmd_str(s); 735 } 736 add_cmd_char('\0'); 737 } 738 739 /* 740 * Parse a line from the lesskey file. 741 */ 742 void 743 parse_line(line) 744 char *line; 745 { 746 char *p; 747 748 /* 749 * See if it is a control line. 750 */ 751 if (control_line(line)) 752 return; 753 /* 754 * Skip leading white space. 755 * Replace the final newline with a null byte. 756 * Ignore blank lines and comments. 757 */ 758 p = clean_line(line); 759 if (*p == '\0') 760 return; 761 762 if (currtable == &vartable) 763 parse_varline(p); 764 else 765 parse_cmdline(p); 766 } 767 768 int 769 main(argc, argv) 770 int argc; 771 char *argv[]; 772 { 773 FILE *desc; 774 FILE *out; 775 char line[1024]; 776 777 #ifdef WIN32 778 if (getenv("HOME") == NULL) 779 { 780 /* 781 * If there is no HOME environment variable, 782 * try the concatenation of HOMEDRIVE + HOMEPATH. 783 */ 784 char *drive = getenv("HOMEDRIVE"); 785 char *path = getenv("HOMEPATH"); 786 if (drive != NULL && path != NULL) 787 { 788 size_t len = strlen(drive) + strlen(path) + 6; 789 char *env = (char *) calloc(len, sizeof(char)); 790 strlcpy(env, "HOME=", len); 791 strlcat(env, drive, len); 792 strlcat(env, path, len); 793 putenv(env); 794 } 795 } 796 #endif /* WIN32 */ 797 798 /* 799 * Process command line arguments. 800 */ 801 parse_args(argc, argv); 802 init_tables(); 803 804 /* 805 * Open the input file. 806 */ 807 if (strcmp(infile, "-") == 0) 808 desc = stdin; 809 else if ((desc = fopen(infile, "r")) == NULL) 810 { 811 #if HAVE_PERROR 812 perror(infile); 813 #else 814 fprintf(stderr, "Cannot open %s\n", infile); 815 #endif 816 usage(); 817 } 818 819 /* 820 * Read and parse the input file, one line at a time. 821 */ 822 errors = 0; 823 linenum = 0; 824 while (fgets(line, sizeof(line), desc) != NULL) 825 { 826 ++linenum; 827 parse_line(line); 828 } 829 830 /* 831 * Write the output file. 832 * If no output file was specified, use "$HOME/.less" 833 */ 834 if (errors > 0) 835 { 836 fprintf(stderr, "%d errors; no output produced\n", errors); 837 exit(1); 838 } 839 840 if (outfile == NULL) 841 outfile = getenv("LESSKEY"); 842 if (outfile == NULL) 843 outfile = homefile(LESSKEYFILE); 844 if ((out = fopen(outfile, "wb")) == NULL) 845 { 846 #if HAVE_PERROR 847 perror(outfile); 848 #else 849 fprintf(stderr, "Cannot open %s\n", outfile); 850 #endif 851 exit(1); 852 } 853 854 /* File header */ 855 fputbytes(out, fileheader, sizeof(fileheader)); 856 857 /* Command key section */ 858 fputbytes(out, cmdsection, sizeof(cmdsection)); 859 fputint(out, cmdtable.pbuffer - cmdtable.buffer); 860 fputbytes(out, (char *)cmdtable.buffer, cmdtable.pbuffer-cmdtable.buffer); 861 /* Edit key section */ 862 fputbytes(out, editsection, sizeof(editsection)); 863 fputint(out, edittable.pbuffer - edittable.buffer); 864 fputbytes(out, (char *)edittable.buffer, edittable.pbuffer-edittable.buffer); 865 866 /* Environment variable section */ 867 fputbytes(out, varsection, sizeof(varsection)); 868 fputint(out, vartable.pbuffer - vartable.buffer); 869 fputbytes(out, (char *)vartable.buffer, vartable.pbuffer-vartable.buffer); 870 871 /* File trailer */ 872 fputbytes(out, endsection, sizeof(endsection)); 873 fputbytes(out, filetrailer, sizeof(filetrailer)); 874 return (0); 875 } 876