1 /*- 2 * SPDX-License-Identifier: BSD-2-Clause-FreeBSD 3 * 4 * units.c Copyright (c) 1993 by Adrian Mariano (adrian@cam.cornell.edu) 5 * 6 * Redistribution and use in source and binary forms, with or without 7 * modification, are permitted provided that the following conditions 8 * are met: 9 * 1. Redistributions of source code must retain the above copyright 10 * notice, this list of conditions and the following disclaimer. 11 * 2. The name of the author may not be used to endorse or promote products 12 * derived from this software without specific prior written permission. 13 * Disclaimer: This software is provided by the author "as is". The author 14 * shall not be liable for any damages caused in any way by this software. 15 * 16 * I would appreciate (though I do not require) receiving a copy of any 17 * improvements you might make to this program. 18 */ 19 20 #ifndef lint 21 static const char rcsid[] = 22 "$FreeBSD$"; 23 #endif /* not lint */ 24 25 #include <ctype.h> 26 #include <err.h> 27 #include <errno.h> 28 #include <histedit.h> 29 #include <getopt.h> 30 #include <stdbool.h> 31 #include <stdio.h> 32 #include <stdlib.h> 33 #include <string.h> 34 #include <unistd.h> 35 36 #include <capsicum_helpers.h> 37 38 #ifndef UNITSFILE 39 #define UNITSFILE "/usr/share/misc/definitions.units" 40 #endif 41 42 #define MAXUNITS 1000 43 #define MAXPREFIXES 100 44 45 #define MAXSUBUNITS 500 46 47 #define PRIMITIVECHAR '!' 48 49 static const char *powerstring = "^"; 50 static const char *numfmt = "%.8g"; 51 52 static struct { 53 char *uname; 54 char *uval; 55 } unittable[MAXUNITS]; 56 57 struct unittype { 58 char *numerator[MAXSUBUNITS]; 59 char *denominator[MAXSUBUNITS]; 60 double factor; 61 double offset; 62 int quantity; 63 }; 64 65 static struct { 66 char *prefixname; 67 char *prefixval; 68 } prefixtable[MAXPREFIXES]; 69 70 71 static char NULLUNIT[] = ""; 72 73 #define SEPARATOR ":" 74 75 static int unitcount; 76 static int prefixcount; 77 static bool verbose = false; 78 static bool terse = false; 79 static const char * outputformat; 80 static const char * havestr; 81 static const char * wantstr; 82 83 static int addsubunit(char *product[], char *toadd); 84 static int addunit(struct unittype *theunit, const char *toadd, int flip, int quantity); 85 static void cancelunit(struct unittype * theunit); 86 static int compare(const void *item1, const void *item2); 87 static int compareproducts(char **one, char **two); 88 static int compareunits(struct unittype * first, struct unittype * second); 89 static int completereduce(struct unittype * unit); 90 static char *dupstr(const char *str); 91 static void initializeunit(struct unittype * theunit); 92 static char *lookupunit(const char *unit); 93 static void readunits(const char *userfile); 94 static int reduceproduct(struct unittype * theunit, int flip); 95 static int reduceunit(struct unittype * theunit); 96 static void showanswer(struct unittype * have, struct unittype * want); 97 static void showunit(struct unittype * theunit); 98 static void sortunit(struct unittype * theunit); 99 static void usage(void); 100 static void zeroerror(void); 101 102 static const char* promptstr = ""; 103 104 static const char * prompt(EditLine *e __unused) { 105 return promptstr; 106 } 107 108 static char * 109 dupstr(const char *str) 110 { 111 char *ret; 112 113 ret = strdup(str); 114 if (!ret) 115 err(3, "dupstr"); 116 return (ret); 117 } 118 119 120 static void 121 readunits(const char *userfile) 122 { 123 FILE *unitfile; 124 char line[512], *lineptr; 125 int len, linenum, i; 126 cap_rights_t unitfilerights; 127 128 unitcount = 0; 129 linenum = 0; 130 131 if (userfile) { 132 unitfile = fopen(userfile, "r"); 133 if (!unitfile) 134 errx(1, "unable to open units file '%s'", userfile); 135 } 136 else { 137 unitfile = fopen(UNITSFILE, "r"); 138 if (!unitfile) { 139 char *direc, *env; 140 char filename[1000]; 141 142 env = getenv("PATH"); 143 if (env) { 144 direc = strtok(env, SEPARATOR); 145 while (direc) { 146 snprintf(filename, sizeof(filename), 147 "%s/%s", direc, UNITSFILE); 148 unitfile = fopen(filename, "rt"); 149 if (unitfile) 150 break; 151 direc = strtok(NULL, SEPARATOR); 152 } 153 } 154 if (!unitfile) 155 errx(1, "can't find units file '%s'", UNITSFILE); 156 } 157 } 158 cap_rights_init(&unitfilerights, CAP_READ, CAP_FSTAT); 159 if (caph_rights_limit(fileno(unitfile), &unitfilerights) < 0) 160 err(1, "cap_rights_limit() failed"); 161 while (!feof(unitfile)) { 162 if (!fgets(line, sizeof(line), unitfile)) 163 break; 164 linenum++; 165 lineptr = line; 166 if (*lineptr == '/' || *lineptr == '#') 167 continue; 168 lineptr += strspn(lineptr, " \n\t"); 169 len = strcspn(lineptr, " \n\t"); 170 lineptr[len] = 0; 171 if (!strlen(lineptr)) 172 continue; 173 if (lineptr[strlen(lineptr) - 1] == '-') { /* it's a prefix */ 174 if (prefixcount == MAXPREFIXES) { 175 warnx("memory for prefixes exceeded in line %d", linenum); 176 continue; 177 } 178 lineptr[strlen(lineptr) - 1] = 0; 179 prefixtable[prefixcount].prefixname = dupstr(lineptr); 180 for (i = 0; i < prefixcount; i++) 181 if (!strcmp(prefixtable[i].prefixname, lineptr)) { 182 warnx("redefinition of prefix '%s' on line %d ignored", 183 lineptr, linenum); 184 continue; 185 } 186 lineptr += len + 1; 187 lineptr += strspn(lineptr, " \n\t"); 188 len = strcspn(lineptr, "\n\t"); 189 if (len == 0) { 190 warnx("unexpected end of prefix on line %d", 191 linenum); 192 continue; 193 } 194 lineptr[len] = 0; 195 prefixtable[prefixcount++].prefixval = dupstr(lineptr); 196 } 197 else { /* it's not a prefix */ 198 if (unitcount == MAXUNITS) { 199 warnx("memory for units exceeded in line %d", linenum); 200 continue; 201 } 202 unittable[unitcount].uname = dupstr(lineptr); 203 for (i = 0; i < unitcount; i++) 204 if (!strcmp(unittable[i].uname, lineptr)) { 205 warnx("redefinition of unit '%s' on line %d ignored", 206 lineptr, linenum); 207 continue; 208 } 209 lineptr += len + 1; 210 lineptr += strspn(lineptr, " \n\t"); 211 if (!strlen(lineptr)) { 212 warnx("unexpected end of unit on line %d", 213 linenum); 214 continue; 215 } 216 len = strcspn(lineptr, "\n\t"); 217 lineptr[len] = 0; 218 unittable[unitcount++].uval = dupstr(lineptr); 219 } 220 } 221 fclose(unitfile); 222 } 223 224 static void 225 initializeunit(struct unittype * theunit) 226 { 227 theunit->numerator[0] = theunit->denominator[0] = NULL; 228 theunit->factor = 1.0; 229 theunit->offset = 0.0; 230 theunit->quantity = 0; 231 } 232 233 234 static int 235 addsubunit(char *product[], char *toadd) 236 { 237 char **ptr; 238 239 for (ptr = product; *ptr && *ptr != NULLUNIT; ptr++); 240 if (ptr >= product + MAXSUBUNITS) { 241 warnx("memory overflow in unit reduction"); 242 return 1; 243 } 244 if (!*ptr) 245 *(ptr + 1) = NULL; 246 *ptr = dupstr(toadd); 247 return 0; 248 } 249 250 251 static void 252 showunit(struct unittype * theunit) 253 { 254 char **ptr; 255 int printedslash; 256 int counter = 1; 257 258 printf(numfmt, theunit->factor); 259 if (theunit->offset) 260 printf("&%.8g", theunit->offset); 261 for (ptr = theunit->numerator; *ptr; ptr++) { 262 if (ptr > theunit->numerator && **ptr && 263 !strcmp(*ptr, *(ptr - 1))) 264 counter++; 265 else { 266 if (counter > 1) 267 printf("%s%d", powerstring, counter); 268 if (**ptr) 269 printf(" %s", *ptr); 270 counter = 1; 271 } 272 } 273 if (counter > 1) 274 printf("%s%d", powerstring, counter); 275 counter = 1; 276 printedslash = 0; 277 for (ptr = theunit->denominator; *ptr; ptr++) { 278 if (ptr > theunit->denominator && **ptr && 279 !strcmp(*ptr, *(ptr - 1))) 280 counter++; 281 else { 282 if (counter > 1) 283 printf("%s%d", powerstring, counter); 284 if (**ptr) { 285 if (!printedslash) 286 printf(" /"); 287 printedslash = 1; 288 printf(" %s", *ptr); 289 } 290 counter = 1; 291 } 292 } 293 if (counter > 1) 294 printf("%s%d", powerstring, counter); 295 printf("\n"); 296 } 297 298 299 void 300 zeroerror(void) 301 { 302 warnx("unit reduces to zero"); 303 } 304 305 /* 306 Adds the specified string to the unit. 307 Flip is 0 for adding normally, 1 for adding reciprocal. 308 Quantity is 1 if this is a quantity to be converted rather than a pure unit. 309 310 Returns 0 for successful addition, nonzero on error. 311 */ 312 313 static int 314 addunit(struct unittype * theunit, const char *toadd, int flip, int quantity) 315 { 316 char *scratch, *savescr; 317 char *item; 318 char *divider, *slash, *offset; 319 int doingtop; 320 321 if (!strlen(toadd)) 322 return 1; 323 324 savescr = scratch = dupstr(toadd); 325 for (slash = scratch + 1; *slash; slash++) 326 if (*slash == '-' && 327 (tolower(*(slash - 1)) != 'e' || 328 !strchr(".0123456789", *(slash + 1)))) 329 *slash = ' '; 330 slash = strchr(scratch, '/'); 331 if (slash) 332 *slash = 0; 333 doingtop = 1; 334 do { 335 item = strtok(scratch, " *\t\n/"); 336 while (item) { 337 if (strchr("0123456789.", *item)) { /* item is a number */ 338 double num, offsetnum; 339 340 if (quantity) 341 theunit->quantity = 1; 342 343 offset = strchr(item, '&'); 344 if (offset) { 345 *offset = 0; 346 offsetnum = atof(offset+1); 347 } else 348 offsetnum = 0.0; 349 350 divider = strchr(item, '|'); 351 if (divider) { 352 *divider = 0; 353 num = atof(item); 354 if (!num) { 355 zeroerror(); 356 free(savescr); 357 return 1; 358 } 359 if (doingtop ^ flip) { 360 theunit->factor *= num; 361 theunit->offset *= num; 362 } else { 363 theunit->factor /= num; 364 theunit->offset /= num; 365 } 366 num = atof(divider + 1); 367 if (!num) { 368 zeroerror(); 369 free(savescr); 370 return 1; 371 } 372 if (doingtop ^ flip) { 373 theunit->factor /= num; 374 theunit->offset /= num; 375 } else { 376 theunit->factor *= num; 377 theunit->offset *= num; 378 } 379 } 380 else { 381 num = atof(item); 382 if (!num) { 383 zeroerror(); 384 free(savescr); 385 return 1; 386 } 387 if (doingtop ^ flip) { 388 theunit->factor *= num; 389 theunit->offset *= num; 390 } else { 391 theunit->factor /= num; 392 theunit->offset /= num; 393 } 394 } 395 if (doingtop ^ flip) 396 theunit->offset += offsetnum; 397 } 398 else { /* item is not a number */ 399 int repeat = 1; 400 401 if (strchr("23456789", 402 item[strlen(item) - 1])) { 403 repeat = item[strlen(item) - 1] - '0'; 404 item[strlen(item) - 1] = 0; 405 } 406 for (; repeat; repeat--) { 407 if (addsubunit(doingtop ^ flip ? theunit->numerator : theunit->denominator, item)) { 408 free(savescr); 409 return 1; 410 } 411 } 412 } 413 item = strtok(NULL, " *\t/\n"); 414 } 415 doingtop--; 416 if (slash) { 417 scratch = slash + 1; 418 } 419 else 420 doingtop--; 421 } while (doingtop >= 0); 422 free(savescr); 423 return 0; 424 } 425 426 427 static int 428 compare(const void *item1, const void *item2) 429 { 430 return strcmp(*(const char * const *)item1, *(const char * const *)item2); 431 } 432 433 434 static void 435 sortunit(struct unittype * theunit) 436 { 437 char **ptr; 438 unsigned int count; 439 440 for (count = 0, ptr = theunit->numerator; *ptr; ptr++, count++); 441 qsort(theunit->numerator, count, sizeof(char *), compare); 442 for (count = 0, ptr = theunit->denominator; *ptr; ptr++, count++); 443 qsort(theunit->denominator, count, sizeof(char *), compare); 444 } 445 446 447 void 448 cancelunit(struct unittype * theunit) 449 { 450 char **den, **num; 451 int comp; 452 453 den = theunit->denominator; 454 num = theunit->numerator; 455 456 while (*num && *den) { 457 comp = strcmp(*den, *num); 458 if (!comp) { 459 /* if (*den!=NULLUNIT) free(*den); 460 if (*num!=NULLUNIT) free(*num);*/ 461 *den++ = NULLUNIT; 462 *num++ = NULLUNIT; 463 } 464 else if (comp < 0) 465 den++; 466 else 467 num++; 468 } 469 } 470 471 472 473 474 /* 475 Looks up the definition for the specified unit. 476 Returns a pointer to the definition or a null pointer 477 if the specified unit does not appear in the units table. 478 */ 479 480 static char buffer[100]; /* buffer for lookupunit answers with 481 prefixes */ 482 483 char * 484 lookupunit(const char *unit) 485 { 486 int i; 487 char *copy; 488 489 for (i = 0; i < unitcount; i++) { 490 if (!strcmp(unittable[i].uname, unit)) 491 return unittable[i].uval; 492 } 493 494 if (unit[strlen(unit) - 1] == '^') { 495 copy = dupstr(unit); 496 copy[strlen(copy) - 1] = 0; 497 for (i = 0; i < unitcount; i++) { 498 if (!strcmp(unittable[i].uname, copy)) { 499 strlcpy(buffer, copy, sizeof(buffer)); 500 free(copy); 501 return buffer; 502 } 503 } 504 free(copy); 505 } 506 if (unit[strlen(unit) - 1] == 's') { 507 copy = dupstr(unit); 508 copy[strlen(copy) - 1] = 0; 509 for (i = 0; i < unitcount; i++) { 510 if (!strcmp(unittable[i].uname, copy)) { 511 strlcpy(buffer, copy, sizeof(buffer)); 512 free(copy); 513 return buffer; 514 } 515 } 516 if (copy[strlen(copy) - 1] == 'e') { 517 copy[strlen(copy) - 1] = 0; 518 for (i = 0; i < unitcount; i++) { 519 if (!strcmp(unittable[i].uname, copy)) { 520 strlcpy(buffer, copy, sizeof(buffer)); 521 free(copy); 522 return buffer; 523 } 524 } 525 } 526 free(copy); 527 } 528 for (i = 0; i < prefixcount; i++) { 529 size_t len = strlen(prefixtable[i].prefixname); 530 if (!strncmp(prefixtable[i].prefixname, unit, len)) { 531 if (!strlen(unit + len) || lookupunit(unit + len)) { 532 snprintf(buffer, sizeof(buffer), "%s %s", 533 prefixtable[i].prefixval, unit + len); 534 return buffer; 535 } 536 } 537 } 538 return 0; 539 } 540 541 542 543 /* 544 reduces a product of symbolic units to primitive units. 545 The three low bits are used to return flags: 546 547 bit 0 (1) set on if reductions were performed without error. 548 bit 1 (2) set on if no reductions are performed. 549 bit 2 (4) set on if an unknown unit is discovered. 550 */ 551 552 553 #define ERROR 4 554 555 static int 556 reduceproduct(struct unittype * theunit, int flip) 557 { 558 559 char *toadd; 560 char **product; 561 int didsomething = 2; 562 563 if (flip) 564 product = theunit->denominator; 565 else 566 product = theunit->numerator; 567 568 for (; *product; product++) { 569 570 for (;;) { 571 if (!strlen(*product)) 572 break; 573 toadd = lookupunit(*product); 574 if (!toadd) { 575 printf("unknown unit '%s'\n", *product); 576 return ERROR; 577 } 578 if (strchr(toadd, PRIMITIVECHAR)) 579 break; 580 didsomething = 1; 581 if (*product != NULLUNIT) { 582 free(*product); 583 *product = NULLUNIT; 584 } 585 if (addunit(theunit, toadd, flip, 0)) 586 return ERROR; 587 } 588 } 589 return didsomething; 590 } 591 592 593 /* 594 Reduces numerator and denominator of the specified unit. 595 Returns 0 on success, or 1 on unknown unit error. 596 */ 597 598 static int 599 reduceunit(struct unittype * theunit) 600 { 601 int ret; 602 603 ret = 1; 604 while (ret & 1) { 605 ret = reduceproduct(theunit, 0) | reduceproduct(theunit, 1); 606 if (ret & 4) 607 return 1; 608 } 609 return 0; 610 } 611 612 613 static int 614 compareproducts(char **one, char **two) 615 { 616 while (*one || *two) { 617 if (!*one && *two != NULLUNIT) 618 return 1; 619 if (!*two && *one != NULLUNIT) 620 return 1; 621 if (*one == NULLUNIT) 622 one++; 623 else if (*two == NULLUNIT) 624 two++; 625 else if (strcmp(*one, *two)) 626 return 1; 627 else { 628 one++; 629 two++; 630 } 631 } 632 return 0; 633 } 634 635 636 /* Return zero if units are compatible, nonzero otherwise */ 637 638 static int 639 compareunits(struct unittype * first, struct unittype * second) 640 { 641 return 642 compareproducts(first->numerator, second->numerator) || 643 compareproducts(first->denominator, second->denominator); 644 } 645 646 647 static int 648 completereduce(struct unittype * unit) 649 { 650 if (reduceunit(unit)) 651 return 1; 652 sortunit(unit); 653 cancelunit(unit); 654 return 0; 655 } 656 657 static void 658 showanswer(struct unittype * have, struct unittype * want) 659 { 660 double ans; 661 char* oformat; 662 663 if (compareunits(have, want)) { 664 printf("conformability error\n"); 665 if (verbose) 666 printf("\t%s = ", havestr); 667 else if (!terse) 668 printf("\t"); 669 showunit(have); 670 if (!terse) { 671 if (verbose) 672 printf("\t%s = ", wantstr); 673 else 674 printf("\t"); 675 showunit(want); 676 } 677 } 678 else if (have->offset != want->offset) { 679 if (want->quantity) 680 printf("WARNING: conversion of non-proportional quantities.\n"); 681 if (have->quantity) { 682 asprintf(&oformat, "\t%s\n", outputformat); 683 printf(oformat, 684 (have->factor + have->offset-want->offset)/want->factor); 685 free(oformat); 686 } 687 else { 688 asprintf(&oformat, "\t (-> x*%sg %sg)\n\t (<- y*%sg %sg)\n", 689 outputformat, outputformat, outputformat, outputformat); 690 printf(oformat, 691 have->factor / want->factor, 692 (have->offset-want->offset)/want->factor, 693 want->factor / have->factor, 694 (want->offset - have->offset)/have->factor); 695 } 696 } 697 else { 698 ans = have->factor / want->factor; 699 700 if (verbose) { 701 printf("\t%s = ", havestr); 702 printf(outputformat, ans); 703 printf(" * %s", wantstr); 704 printf("\n"); 705 } 706 else if (terse) { 707 printf(outputformat, ans); 708 printf("\n"); 709 } 710 else { 711 printf("\t* "); 712 printf(outputformat, ans); 713 printf("\n"); 714 } 715 716 if (verbose) { 717 printf("\t%s = (1 / ", havestr); 718 printf(outputformat, 1/ans); 719 printf(") * %s\n", wantstr); 720 } 721 else if (!terse) { 722 printf("\t/ "); 723 printf(outputformat, 1/ans); 724 printf("\n"); 725 } 726 } 727 } 728 729 730 static void __dead2 731 usage(void) 732 { 733 fprintf(stderr, 734 "usage: units [-f unitsfile] [-H historyfile] [-UVq] [from-unit to-unit]\n"); 735 exit(3); 736 } 737 738 static struct option longopts[] = { 739 {"help", no_argument, NULL, 'h'}, 740 {"exponential", no_argument, NULL, 'e'}, 741 {"file", required_argument, NULL, 'f'}, 742 {"history", required_argument, NULL, 'H'}, 743 {"output-format", required_argument, NULL, 'o'}, 744 {"quiet", no_argument, NULL, 'q'}, 745 {"terse", no_argument, NULL, 't'}, 746 {"unitsfile", no_argument, NULL, 'U'}, 747 {"verbose", no_argument, NULL, 'v'}, 748 {"version", no_argument, NULL, 'V'}, 749 { 0, 0, 0, 0 } 750 }; 751 752 753 int 754 main(int argc, char **argv) 755 { 756 757 struct unittype have, want; 758 int optchar; 759 bool quiet; 760 bool readfile; 761 bool quit; 762 History *inhistory; 763 EditLine *el; 764 HistEvent ev; 765 int inputsz; 766 char const * history_file; 767 768 quiet = false; 769 readfile = false; 770 history_file = NULL; 771 outputformat = numfmt; 772 quit = false; 773 while ((optchar = getopt_long(argc, argv, "+ehf:o:qtvH:UV", longopts, NULL)) != -1) { 774 switch (optchar) { 775 case 'e': 776 outputformat = "%6e"; 777 break; 778 case 'f': 779 readfile = true; 780 if (strlen(optarg) == 0) 781 readunits(NULL); 782 else 783 readunits(optarg); 784 break; 785 case 'H': 786 history_file = optarg; 787 break; 788 case 'q': 789 quiet = true; 790 break; 791 case 't': 792 terse = true; 793 break; 794 case 'o': 795 outputformat = optarg; 796 break; 797 case 'v': 798 verbose = true; 799 break; 800 case 'V': 801 fprintf(stderr, "FreeBSD units\n"); 802 /* FALLTHROUGH */ 803 case 'U': 804 if (access(UNITSFILE, F_OK) == 0) 805 printf("%s\n", UNITSFILE); 806 else 807 printf("Units data file not found"); 808 exit(0); 809 case 'h': 810 /* FALLTHROUGH */ 811 812 default: 813 usage(); 814 } 815 } 816 817 if (!readfile) 818 readunits(NULL); 819 820 if (optind == argc - 2) { 821 if (caph_enter() < 0) 822 err(1, "unable to enter capability mode"); 823 824 havestr = argv[optind]; 825 wantstr = argv[optind + 1]; 826 initializeunit(&have); 827 addunit(&have, havestr, 0, 1); 828 completereduce(&have); 829 initializeunit(&want); 830 addunit(&want, wantstr, 0, 1); 831 completereduce(&want); 832 showanswer(&have, &want); 833 } else { 834 inhistory = history_init(); 835 el = el_init(argv[0], stdin, stdout, stderr); 836 el_set(el, EL_PROMPT, &prompt); 837 el_set(el, EL_EDITOR, "emacs"); 838 el_set(el, EL_SIGNAL, 1); 839 el_set(el, EL_HIST, history, inhistory); 840 el_source(el, NULL); 841 history(inhistory, &ev, H_SETSIZE, 800); 842 if (inhistory == 0) 843 err(1, "Could not initialize history"); 844 845 if (caph_enter() < 0) 846 err(1, "unable to enter capability mode"); 847 848 if (!quiet) 849 printf("%d units, %d prefixes\n", unitcount, 850 prefixcount); 851 while (!quit) { 852 do { 853 initializeunit(&have); 854 if (!quiet) 855 promptstr = "You have: "; 856 havestr = el_gets(el, &inputsz); 857 if (havestr == NULL) { 858 quit = true; 859 break; 860 } 861 if (inputsz > 0) 862 history(inhistory, &ev, H_ENTER, 863 havestr); 864 } while (addunit(&have, havestr, 0, 1) || 865 completereduce(&have)); 866 if (quit) { 867 break; 868 } 869 do { 870 initializeunit(&want); 871 if (!quiet) 872 promptstr = "You want: "; 873 wantstr = el_gets(el, &inputsz); 874 if (wantstr == NULL) { 875 quit = true; 876 break; 877 } 878 if (inputsz > 0) 879 history(inhistory, &ev, H_ENTER, 880 wantstr); 881 } while (addunit(&want, wantstr, 0, 1) || 882 completereduce(&want)); 883 if (quit) { 884 break; 885 } 886 showanswer(&have, &want); 887 } 888 889 history_end(inhistory); 890 el_end(el); 891 } 892 893 return (0); 894 } 895