rewrite in C
[ssp.git] / src / kjv.c
blob2a3092aa793720032436a4786f8ca1c8dba0da38
1 /*
2 kjv: Read the Word of God from your terminal
4 License: Public domain
5 */
7 #include <stdio.h>
8 #include <stdlib.h>
9 #include <unistd.h>
10 #include <stdbool.h>
11 #include <string.h>
12 #include <signal.h>
13 #include <readline/readline.h>
14 #include <readline/history.h>
15 #include <regex.h>
16 #include <sys/types.h>
17 #include <sys/wait.h>
18 #include <ctype.h>
19 #include <sys/ioctl.h>
21 #include "data.h"
22 #include "intset.h"
24 #define KJV_REF_SEARCH 1
25 #define KJV_REF_EXACT 2
26 #define KJV_REF_EXACT_SET 3
27 #define KJV_REF_RANGE 4
28 #define KJV_REF_RANGE_EXT 5
30 typedef struct kjv_ref {
31 int type;
32 char book[64];
33 unsigned int chapter;
34 unsigned int chapter_end;
35 unsigned int verse;
36 unsigned int verse_end;
37 intset *verse_set;
38 regex_t search;
39 } kjv_ref;
41 static kjv_ref *
42 kjv_newref()
44 return calloc(1, sizeof(kjv_ref));
47 static void
48 kjv_freeref(kjv_ref *ref)
50 if (ref) {
51 regfree(&ref->search);
52 free(ref);
56 static int
57 kjv_parseref(kjv_ref *ref, const char *ref_str)
59 // 1. <book>
60 // 2. <book>:?<chapter>
61 // 3. <book>:?<chapter>:<verse>
62 // 3a. <book>:?<chapter>:<verse>[,<verse>]...
63 // 4. <book>:?<chapter>-<chapter>
64 // 5. <book>:?<chapter>:<verse>-<verse>
65 // 6. <book>:?<chapter>:<verse>-<chapter>:<verse>
66 // 7. /<search>
67 // 8. <book>/search
68 // 9. <book>:?<chapter>/search
70 ref->type = 0;
71 ref->book[0] = '\0';
72 ref->chapter = 0;
73 ref->chapter_end = 0;
74 ref->verse = 0;
75 ref->verse_end = 0;
76 intset_free(ref->verse_set);
77 ref->verse_set = NULL;
78 regfree(&ref->search);
80 int n = 0;
82 sscanf(ref_str, "%*1[1-3]%n", &n);
83 bool has_booknum = n > 0;
84 if (has_booknum && sscanf(ref_str, "%1[1-3]%62[a-zA-Z ]%n", &ref->book[0], &ref->book[1], &n) == 2) {
85 ref_str = &ref_str[n];
86 } else if (!has_booknum && sscanf(ref_str, "%63[a-zA-Z ]%n", &ref->book[0], &n) == 1) {
87 // 1, 2, 3, 3a, 4, 5, 6, 8, 9
88 ref_str = &ref_str[n];
89 } else if (ref_str[0] == '/') {
90 // 7
91 goto search;
92 } else {
93 return 1;
96 if (sscanf(ref_str, ":%u%n", &ref->chapter, &n) == 1 || sscanf(ref_str, "%u%n", &ref->chapter, &n) == 1) {
97 // 2, 3, 3a, 4, 5, 6, 9
98 ref_str = &ref_str[n];
99 } else if (ref_str[0] == '/') {
100 // 8
101 goto search;
102 } else if (ref_str[0] == '\0') {
103 // 1
104 ref->type = KJV_REF_EXACT;
105 return 0;
106 } else {
107 return 1;
110 if (sscanf(ref_str, ":%u%n", &ref->verse, &n) == 1) {
111 // 3, 3a, 5, 6
112 ref_str = &ref_str[n];
113 } else if (sscanf(ref_str, "-%u%n", &ref->chapter_end, &n) == 1) {
114 // 4
115 if (ref_str[n] != '\0') {
116 return 1;
118 ref->type = KJV_REF_RANGE;
119 return 0;
120 } else if (ref_str[0] == '/') {
121 // 9
122 goto search;
123 } else if (ref_str[0] == '\0') {
124 // 2
125 ref->type = KJV_REF_EXACT;
126 return 0;
127 } else {
128 return 1;
131 unsigned int value;
132 int ret = sscanf(ref_str, "-%u%n", &value, &n);
133 if (ret == 1 && ref_str[n] == '\0') {
134 // 5
135 ref->verse_end = value;
136 ref->type = KJV_REF_RANGE;
137 return 0;
138 } else if (ret == 1) {
139 // 6
140 ref->chapter_end = value;
141 ref_str = &ref_str[n];
142 } else if (ref_str[0] == '\0') {
143 // 3
144 ref->type = KJV_REF_EXACT;
145 return 0;
146 } else if (sscanf(ref_str, ",%u%n", &value, &n) == 1) {
147 // 3a
148 ref->verse_set = intset_new();
149 intset_add(ref->verse_set, ref->verse);
150 intset_add(ref->verse_set, value);
151 ref_str = &ref_str[n];
152 while (true) {
153 if (sscanf(ref_str, ",%u%n", &value, &n) != 1) {
154 break;
156 intset_add(ref->verse_set, value);
157 ref_str = &ref_str[n];
159 if (ref_str[0] != '\0') {
160 return 1;
162 ref->type = KJV_REF_EXACT_SET;
163 return 0;
164 } else {
165 return 1;
168 if (sscanf(ref_str, ":%u%n", &ref->verse_end, &n) == 1 && ref_str[n] == '\0') {
169 // 6
170 ref->type = KJV_REF_RANGE_EXT;
171 return 0;
172 } else {
173 return 1;
176 search:
177 ref->type = KJV_REF_SEARCH;
178 if (regcomp(&ref->search, &ref_str[1], REG_EXTENDED|REG_ICASE|REG_NOSUB) != 0) {
179 return 2;
181 return 0;
184 static char *
185 str_join(size_t n, char *strs[])
187 size_t length = 0;
188 for (size_t i = 0; i < n; i++) {
189 if (i > 0) {
190 length++;
192 length += strlen(strs[i]);
194 char *str = malloc(length + 1);
195 str[0] = '\0';
196 for (size_t i = 0; i < n; i++) {
197 if (i > 0) {
198 strcat(str, " ");
200 strcat(str, strs[i]);
202 return str;
205 static bool
206 kjv_bookequal(const char *a, const char *b, bool short_match)
208 for (size_t i = 0, j = 0; ; ) {
209 if ((!a[i] && !b[j]) || (short_match && !b[j])) {
210 return true;
211 } else if (a[i] == ' ') {
212 i++;
213 } else if (b[j] == ' ') {
214 j++;
215 } else if (tolower(a[i]) != tolower(b[j])) {
216 return false;
217 } else {
218 i++;
219 j++;
224 static bool
225 kjv_book_matches(const char *book, const kjv_verse *verse)
227 return kjv_bookequal(verse->book_name, book, false) ||
228 kjv_bookequal(verse->book_abbr, book, false) ||
229 kjv_bookequal(verse->book_name, book, true);
232 static bool
233 kjv_should_output(const kjv_ref *ref, const kjv_verse *verse)
235 switch (ref->type) {
236 case KJV_REF_SEARCH:
237 return (ref->book[0] == '\0' || kjv_book_matches(ref->book, verse)) &&
238 (ref->chapter == 0 || verse->chapter == ref->chapter) &&
239 regexec(&ref->search, verse->text, 0, NULL, 0) == 0;
241 case KJV_REF_EXACT:
242 return kjv_book_matches(ref->book, verse) &&
243 (ref->chapter == 0 || ref->chapter == verse->chapter) &&
244 (ref->verse == 0 || ref->verse == verse->verse);
246 case KJV_REF_EXACT_SET:
247 return kjv_book_matches(ref->book, verse) &&
248 (ref->chapter == 0 || verse->chapter == ref->chapter) &&
249 intset_contains(ref->verse_set, verse->verse);
251 case KJV_REF_RANGE:
252 return kjv_book_matches(ref->book, verse) &&
253 ((ref->chapter_end == 0 && ref->chapter == verse->chapter) ||
254 (verse->chapter >= ref->chapter && verse->chapter <= ref->chapter_end)) &&
255 (ref->verse == 0 || verse->verse >= ref->verse) &&
256 (ref->verse_end == 0 || verse->verse <= ref->verse_end);
258 case KJV_REF_RANGE_EXT:
259 return kjv_book_matches(ref->book, verse) &&
261 (verse->chapter == ref->chapter && verse->verse >= ref->verse && ref->chapter != ref->chapter_end) ||
262 (verse->chapter > ref->chapter && verse->chapter < ref->chapter_end) ||
263 (verse->chapter == ref->chapter_end && verse->verse <= ref->verse_end && ref->chapter != ref->chapter_end) ||
264 (ref->chapter == ref->chapter_end && verse->chapter == ref->chapter && verse->verse >= ref->verse && verse->verse <= ref->verse_end)
267 default:
268 return false;
272 static bool
273 kjv_output(const kjv_ref *ref, FILE *f, bool no_linewrap, int maximum_width)
275 bool printed = false;
276 int last_book_printed = 0;
277 for (size_t i = 0; i < kjv_verses_length; i++) {
278 kjv_verse *verse = &kjv_verses[i];
279 if (kjv_should_output(ref, verse)) {
280 if (verse->book != last_book_printed) {
281 fprintf(f, "%s\n", verse->book_name);
282 last_book_printed = verse->book;
284 fprintf(f, "%d:%d\t", verse->chapter, verse->verse);
285 if (no_linewrap) {
286 fprintf(f, "%s\n", verse->text);
287 } else {
288 char verse_text[1024];
289 strcpy(verse_text, verse->text);
290 size_t characters_printed = 0;
291 char *word = strtok(verse_text, " ");
292 while (word != NULL) {
293 size_t word_length = strlen(word);
294 if (characters_printed + word_length + (characters_printed > 0 ? 1 : 0) > maximum_width - 8) {
295 fprintf(f, "\n\t");
296 characters_printed = 0;
298 if (characters_printed > 0) {
299 fprintf(f, " ");
300 characters_printed++;
302 fprintf(f, "%s", word);
303 characters_printed += word_length;
304 word = strtok(NULL, " ");
306 fprintf(f, "\n");
308 printed = true;
311 return printed;
314 static int
315 kjv_render(const kjv_ref *ref, bool no_linewrap, int maximum_width)
317 int fds[2];
318 if (pipe(fds) == -1) {
319 return 1;
322 pid_t pid = fork();
323 if (pid == 0) {
324 close(fds[1]);
325 dup2(fds[0], STDIN_FILENO);
326 char *args[] = { "less", "-f", "-", NULL };
327 execvp("less", args);
328 printf("unable not exec less\n");
329 _exit(0);
330 } else if (pid == -1) {
331 printf("unable to fork\n");
332 return 2;
334 close(fds[0]);
335 FILE *output = fdopen(fds[1], "w");
336 bool printed = kjv_output(ref, output, no_linewrap, maximum_width);
337 if (!printed) {
338 kill(pid, SIGTERM);
340 fclose(output);
341 waitpid(pid, NULL, 0);
342 if (!printed) {
343 printf("unknown reference\n");
345 return 0;
348 const char *
349 usage = "usage: kjv [flags] [reference...]\n"
350 "\n"
351 " -l list books\n"
352 " -W no line wrap\n"
353 " -h show help\n"
354 "\n"
355 " Reference:\n"
356 " <Book>\n"
357 " Individual book\n"
358 " <Book>:<Chapter>\n"
359 " Individual chapter of a book\n"
360 " <Book>:<Chapter>:<Verse>[,<Verse>]...\n"
361 " Individual verse(s) of a specific chapter of a book\n"
362 " <Book>:<Chapter>-<Chapter>\n"
363 " Range of chapters in a book\n"
364 " <Book>:<Chapter>:<Verse>-<Verse>\n"
365 " Range of verses in a book chapter\n"
366 " <Book>:<Chapter>:<Verse>-<Chapter>:<Verse>\n"
367 " Range of chapters and verses in a book\n"
368 "\n"
369 " /<Search>\n"
370 " All verses that match a pattern\n"
371 " <Book>/<Search>\n"
372 " All verses in a book that match a pattern\n"
373 " <Book>:<Chapter>/<Search>\n"
374 " All verses in a chapter of a book that match a pattern\n";
377 main(int argc, char *argv[])
379 bool list_books = false;
380 bool no_linewrap = false;
382 int opt;
383 while ((opt = getopt(argc, argv, "lWh")) != -1) {
384 switch (opt) {
385 case 'l':
386 list_books = true;
387 break;
388 case 'W':
389 no_linewrap = true;
390 break;
391 case 'h':
392 printf("%s", usage);
393 return 0;
394 case '?':
395 printf("%s", usage);
396 return 1;
400 if (list_books) {
401 int i;
402 char *last_book_printed = NULL;
403 for (i = 0; i < kjv_verses_length; i++) {
404 if (last_book_printed == NULL || strcmp(kjv_verses[i].book_name, last_book_printed) != 0) {
405 printf("%s (%s)\n", kjv_verses[i].book_name, kjv_verses[i].book_abbr);
406 last_book_printed = kjv_verses[i].book_name;
409 return 0;
412 int maximum_width = 80;
413 struct winsize ttysize;
414 memset(&ttysize, 0, sizeof(struct winsize));
415 if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ttysize) == 0 && ttysize.ws_col > 0) {
416 maximum_width = ttysize.ws_col;
419 signal(SIGPIPE, SIG_IGN);
421 if (argc == optind) {
422 while (true) {
423 char *input = readline("kjv> ");
424 if (input == NULL) {
425 break;
427 kjv_ref *ref = kjv_newref();
428 int success = kjv_parseref(ref, input);
429 free(input);
430 if (success == 0) {
431 kjv_render(ref, no_linewrap, maximum_width);
433 kjv_freeref(ref);
435 } else {
436 char *ref_str = str_join(argc-optind, &argv[optind]);
437 kjv_ref *ref = kjv_newref();
438 int success = kjv_parseref(ref, ref_str);
439 free(ref_str);
440 if (success == 0) {
441 kjv_render(ref, no_linewrap, maximum_width);
443 kjv_freeref(ref);
446 return 0;