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