#include #include #include #include #include #include #include "xdictlib.h" /* The text file where the dictionary is stored */ #define XDICT_SAVE_TXT "xdict.save.txt" /* Define this macro if the operating system will free our memory on exit; otherwise it may take a long time for the program to free individually all the little chunks we've allocated. */ #define FAST_EXIT 1 #define NELEM(a) ((int)(sizeof a / sizeof *a)) #define PLUR(x) ("s"+((x)==1)) int printme(const char *s, void *info_dummy); void engraveme(void); int display_set(const char *s, void *info); void do_error(const char *fmt, ...); void do_help(void); void do_man(int page_height); void page(const char *s); int main(void) { struct xdict dict; int modified = 0; char cmd[100]; int rc; xdict_init(&dict); puts("Inited successfully"); switch (rc = xdict_load(&dict, XDICT_SAVE_TXT)) { case -1: do_error("Dictionary not found"); case -2: do_error("Dictionary corrupted"); case -3: do_error("Out of memory"); case -4: do_error("Out of memory"); } puts("Loaded successfully. Type HELP for details."); while (fgets(cmd, sizeof cmd, stdin) != NULL) { if (strchr(cmd, '\n') == NULL) { puts("Input line too long. Ignoring and continuing"); while (getchar() != '\n') ; continue; } if (strncmp(cmd, "ADD ", 4) == 0) { int start, end; int rc2, two_words = 0; for (start=4; isspace(cmd[start]); ++start); for (end=start; isalpha(cmd[end]); ++end) cmd[end] = tolower(cmd[end]); /* Allow "ADD foo/s" to add both words. */ rc2 = 0; if (cmd[end] == '/') { if (cmd[end+1] == 's') { two_words = 1; strcpy(cmd+end, "s"); rc2 = xdict_addword(&dict, cmd+start, end+1-start); } } cmd[end] = '\0'; rc = xdict_addword(&dict, cmd+start, end-start); if (!rc || !rc2) modified++; if (!rc && !rc2) puts("Added successfully."); else if (rc == -3 || rc == -4 || rc2 == -3 || rc2 == -4) do_error("Out of memory"); else if (rc == -1 || rc2 == -1) { int both = (rc == -1) && (rc2 == -1); printf("Failed to add %sword%s; continuing\n", (!two_words || both)? "": rc? "the shorter ": "the longer ", both? "s": ""); } } else if (strncmp(cmd, "REM ", 4) == 0) { int start, end; for (start=4; isspace(cmd[start]); ++start); for (end=start; !isspace(cmd[end]); ++end) cmd[end] = tolower(cmd[end]); cmd[end] = '\0'; rc = xdict_remmatch(&dict, cmd+start, 0); if (rc < 0) puts("Failed to remove word; continuing"); else if (rc == 0) puts("Word not found; continuing"); else { puts("Removed successfully."); modified++; } } else if (strncmp(cmd, "SET ", 4) == 0) { int start, end, index = -1; for (start=4; isspace(cmd[start]); ++start); for (end=start; !isspace(cmd[end]); ++end) { if (cmd[end] == '_') cmd[end] = '?', index = end-start; else cmd[end] = tolower(cmd[end]); } cmd[end] = '\0'; if (index < 0) { puts("Set action requires a '_' marker!"); } else { rc = xdict_find(&dict, cmd+start, display_set, &index); if (rc < 0) puts("Set action failed; continuing"); else if (rc == 0) puts("No matching words found; continuing"); else display_set(NULL, NULL); } } else if (strcmp(cmd, "SORT\n") == 0) { xdict_sort(&dict); puts("Done."); } else if (strcmp(cmd, "STAT\n") == 0) { int i, total = 0; for (i=0; i < XDICT_MAXLENGTH; ++i) total += dict.len[i]; printf("Max. word length is %d\n", XDICT_MAXLENGTH-1); printf("Total word count is %d\n", total); printf("%d modification%s; %ssorted\n", modified, PLUR(modified), dict.sorted? "": "not "); } else if (strcmp(cmd, "SAVE\n") == 0) { if (!dict.sorted) { puts("Sorting dictionary..."); xdict_sort(&dict); } if (xdict_save_small(&dict, XDICT_SAVE_TXT)) puts("Dictionary not saved"); puts("Saved successfully"); modified = 0; } else if (strcmp(cmd, "SAVEA\n") == 0) { if (xdict_save(&dict, XDICT_SAVE_TXT)) puts("Dictionary not saved"); puts("Saved successfully"); modified = 0; } else if (!strcmp(cmd, "QUIT\n") || !strcmp(cmd, "EXIT\n")) { break; } else if (strcmp(cmd, "HELP\n") == 0) { do_help(); } else if (strcmp(cmd, "HELP VERBOSE\n") == 0) { do_man(1000); } else if (strncmp(cmd, "HELP VERBOSE ", 13) == 0) { int start, end; int oops = 0; for (start=13; isspace(cmd[start]); ++start); for (end=start; !isspace(cmd[end]); ++end) { if (!isdigit(cmd[end])) oops = 1; } cmd[end] = '\0'; if (oops || (end > start+3)) { do_man(1000); } else { do_man(atoi(&cmd[start])); } } else { int start, end; for (start=0; isspace(cmd[start]); ++start) ; if (cmd[start] == '\0') { printf("(Ctrl-D to quit)\n"); goto next_loop; } for (end=start; !isspace(cmd[end]); ++end) cmd[end] = tolower(cmd[end]); cmd[end] = '\0'; rc = xdict_find(&dict, cmd+start, printme, NULL); engraveme(); printf("%d\n", rc); next_loop: ; } } puts("Wait..."); if (modified > 0) { printf("%d modification%s\n", modified, PLUR(modified)); if (!dict.sorted) { puts("Sorting dictionary..."); xdict_sort(&dict); } if (xdict_save_small(&dict, XDICT_SAVE_TXT)) do_error("Dictionary not saved"); puts("Saved successfully"); } #if FAST_EXIT #else puts("Freeing memory..."); xdict_free(&dict); #endif puts("Done."); return 0; } /* The two routines |printme| and |engraveme| work in tandem to give the user a nice view of the matching words. The |printme| routine adds its argument word to a static list, and the |engraveme| routine prints that list in columnar format and clears it in preparation for the next |printme| cycle. */ static char (*glob_print_buffer)[XDICT_MAXLENGTH] = NULL; static size_t glob_print_len = 0, glob_print_cap = 0; int printme(const char *s, void *info_dummy) { (void)info_dummy; /* This parameter is not used. */ if (glob_print_len >= glob_print_cap) { size_t newcap = 2 * glob_print_cap + 16; void *t = realloc(glob_print_buffer, newcap * sizeof *glob_print_buffer); if (t == NULL) { if (glob_print_len > 0) { engraveme(); printme(s, NULL); return 0; } else do_error("Out of memory for search results"); } glob_print_buffer = t; glob_print_cap = newcap; } strncpy(glob_print_buffer[glob_print_len++], s, XDICT_MAXLENGTH); glob_print_buffer[glob_print_len-1][XDICT_MAXLENGTH-1] = '\0'; return 0; } void engraveme(void) { /* We print our results in columns with a total page width of 60 chars, leaving at least 4 spaces between columns and keeping at least three vertical rows whenever possible. */ int n = glob_print_len; int colwidth=3; int i, r, cols, rows, x; for (i=0; i < n; ++i) { int k = strlen(glob_print_buffer[i]); if (k > colwidth) colwidth = k; } cols = 65/(colwidth+4); if (cols > n/3) cols = n/3; if (cols < 1) cols = 1; rows = (n+cols-1)/cols; x = cols*rows - n; /* Print using |cols| columns. */ for (r=0; r < rows; ++r) { for (i=r; i < n; i += rows) { printf("%s%-*s", (i==r)? "": " ", colwidth, glob_print_buffer[i]); if (r==rows-1 && i >= r+rows*(cols-x-1)) break; if (i+1 > (cols-x)*rows) --i; } printf("\n"); } /* Free the static buffer. */ free(glob_print_buffer); glob_print_buffer = NULL; glob_print_len = glob_print_cap = 0; return; } int display_set(const char *s, void *info) { static char buf[CHAR_MAX-CHAR_MIN] = {0}; if (s == NULL) { /* Display the accumulated letters and reset the buffer. */ int i, found_some = 0; for (i=0; i < NELEM(buf); ++i) { if (buf[i]) printf("%c", i+CHAR_MIN), found_some = 1; buf[i] = 0; } if (found_some) printf("\n"); } else { /* Add a new word to the buffer. */ const int *p = info; buf[s[*p]-CHAR_MIN] = 1; } return 0; } void do_error(const char *fmat, ...) { va_list ap; printf("xdict: "); va_start(ap, fmat); vprintf(fmat, ap); printf("\n"); va_end(ap); exit(EXIT_FAILURE); } void do_help(void) { puts("All meta-commands must be entered in upper case."); puts("HELP This message"); puts("HELP VERBOSE Complete man pages for xdict"); puts("QUIT, EXIT (Save and) exit, the same as Ctrl-D"); puts("SAVE Save compressed word list into " XDICT_SAVE_TXT); puts("SAVEA Save uncompressed word list"); puts("SORT Sort the dictionary"); puts("STAT Display some statistical details"); puts("ch0rtl* Display matching word(s)"); puts("SET ch_rtl* Display set of crossing letters"); puts("ADD chortle Add a word to the dictionary"); puts("REM ch0rtl* Remove word(s) from the dictionary"); puts(""); puts("set Matches the word \"set\" only"); puts("b0g Vowel: matches bag, beg, big,... but not bfg"); puts("do1 Consonant: matches doc, dog, don,... but not doe"); puts("do? Single letter: matches doc, doe, dog,..."); puts("do*t Any string: matches dot, doubt, donut,..."); } static int glob_pageheight; static int glob_paralines = 0; void do_man(int page_height) { glob_pageheight = page_height-1; page(NULL); glob_paralines = 4; page("xdict: Crossword dictionary utility.\n"); page(" The 'xdict' utility is a crossword dictionary. It supports"); page("various kinds of wildcard searches, including restricting"); page("the wildcards to vowels or consonants."); glob_paralines = 5; page(" The word list for the dictionary is stored in the text file"); page("'" XDICT_SAVE_TXT "'. That file in its simplest form is just"); page("a list of words: one word per line. Words must be completely"); page("alphabetic, and can't have any embedded spaces; capitalization"); page("is irrelevant."); glob_paralines = 11; page(" By default, the word list is saved in a slightly more complex"); page("format, to save disk space. In the compressed format, the pair"); page("of words \"bed\" and \"beds\" would be stored as \"bed/s\" (on a"); page("single line). The regular verb \"add, adds, added, adding\" is"); page("stored as \"add/v\". There are also two other regular verb"); page("constructions, exemplified by \"tap/w\" (for the verb \"tape\")"); page("and \"tap/x\" (for the verb \"tap\"). This is a purely lossless"); page("and unambiguous form of compression, and very human-friendly,"); page("but it does make the file format rather idiosyncratic. Therefore,"); page("'xdict' provides the user meta-command SAVEA, which stores the"); page("dictionary word list in the \"simplest form\" detailed above."); glob_paralines = 10; page(" The user meta-command SAVE saves the word list in compressed"); page("form. It will sort the dictionary first, if needed."); page(" When the program exits normally --- upon encountering the"); page("end-of-file marker or one of the user meta-commands QUIT and EXIT"); page("--- it will check to see whether the dictionary has been modified"); page("by any ADD or REM commands since the last time it was saved. If"); page("the word list has been modified, then it will sort the list and"); page("save it in the compressed format. If the word list is unmodified,"); page("the program will free its resources and exit without performing"); page("the redundant save operation."); glob_paralines = 8; page(" The user meta-command STAT can be used to see whether the"); page("dictionary has been modified, and whether it is currently sorted."); page(" The meta-command SET is used to find out quickly which letters"); page("can be used in a given position. For example, searching on the"); page("pattern 'be???f' yields the results \"behalf, behoof, belief\";"); page("therefore the meta-command 'SET be??_f' yields the three letters"); page("\"elo\", and 'SET be_??f' yields \"hl\". All the normal wildcards"); page("can be used in SET commands."); glob_paralines = 5; page(" All the normal wildcards can be used in REM commands, also;"); page("the command 'REM foo*' will remove \"food\" and \"footstool\"."); page("Wildcards cannot be used with ADD, for obvious reasons; you must"); page("enter 'ADD draft' and 'ADD drafted' individually, for example."); page("However, 'ADD draft/s' will add both \"draft\" and \"drafts\"."); glob_paralines = 4; page(" Type 'HELP' for a brief summary of commands and wildcards, or"); page("'HELP VERBOSE' for this message again. Type 'HELP VERBOSE k' to"); page("make this message pause after every k lines; for example, 'HELP"); page("VERBOSE 20'."); } /* This is a very icky attempt at a "nice" pager, that will avoid creating orphaned or widowed lines, as best it can, on the fly. It's overkill for this program, but since we have in-program help text instead of a verbose help option at the command line, it seems like we ought to be helping the user as much as possible. */ void page(const char *s) { static int lines_in_this_para; static int lines_dumped_this_page = 0; static int para_line; static int dump_count; if (s == NULL) { lines_dumped_this_page = 0; return; } if (glob_paralines > 0) { lines_in_this_para = glob_paralines; para_line = 0; glob_paralines = 0; if (lines_dumped_this_page + lines_in_this_para <= glob_pageheight) dump_count = lines_in_this_para; else { if (lines_dumped_this_page + 2 > glob_pageheight) { /* We may need an orphaned line. */ if (lines_dumped_this_page > 2*glob_pageheight/3) goto pause; /* Yep, make this an orphan. */ dump_count = lines_in_this_para; } else if ((lines_dumped_this_page + lines_in_this_para) % glob_pageheight == 1) { /* We may need a widowed line. */ if (lines_dumped_this_page > 2*glob_pageheight/3) goto pause; /* Yep, make this a widow. */ dump_count = lines_in_this_para; } else dump_count = lines_in_this_para; } } if (dump_count > 0) { --dump_count; goto print; } else goto pause; print: puts(s); ++lines_dumped_this_page; --glob_paralines; if (lines_dumped_this_page >= glob_pageheight) { puts("--more--"); while (getchar() != '\n') continue; lines_dumped_this_page = 0; dump_count = glob_pageheight; } return; pause: puts("--more--"); while (getchar() != '\n') continue; puts(s); lines_dumped_this_page = 1; --glob_paralines; dump_count = glob_pageheight-1; return; }