2 kjv: Read the Word of God from your terminal
14 #include <readline/readline.h>
15 #include <readline/history.h>
17 #include <sys/types.h>
20 #include <sys/ioctl.h>
27 int maximum_line_length
;
34 #define KJV_REF_SEARCH 1
35 #define KJV_REF_EXACT 2
36 #define KJV_REF_EXACT_SET 3
37 #define KJV_REF_RANGE 4
38 #define KJV_REF_RANGE_EXT 5
40 typedef struct kjv_ref
{
44 unsigned int chapter_end
;
46 unsigned int verse_end
;
55 return calloc(1, sizeof(kjv_ref
));
59 kjv_freeref(kjv_ref
*ref
)
62 free(ref
->search_str
);
63 regfree(&ref
->search
);
69 kjv_parseref(kjv_ref
*ref
, const char *ref_str
)
72 // 2. <book>:?<chapter>
73 // 3. <book>:?<chapter>:<verse>
74 // 3a. <book>:?<chapter>:<verse>[,<verse>]...
75 // 4. <book>:?<chapter>-<chapter>
76 // 5. <book>:?<chapter>:<verse>-<verse>
77 // 6. <book>:?<chapter>:<verse>-<chapter>:<verse>
80 // 9. <book>:?<chapter>/search
88 intset_free(ref
->verse_set
);
89 ref
->verse_set
= NULL
;
90 free(ref
->search_str
);
91 ref
->search_str
= NULL
;
92 regfree(&ref
->search
);
96 sscanf(ref_str
, "%*1[1-3]%n", &n
);
97 bool has_booknum
= n
> 0;
98 if (has_booknum
&& sscanf(ref_str
, "%1[1-3]%62[a-zA-Z ]%n", &ref
->book
[0], &ref
->book
[1], &n
) == 2) {
99 ref_str
= &ref_str
[n
];
100 } else if (!has_booknum
&& sscanf(ref_str
, "%63[a-zA-Z ]%n", &ref
->book
[0], &n
) == 1) {
101 // 1, 2, 3, 3a, 4, 5, 6, 8, 9
102 ref_str
= &ref_str
[n
];
103 } else if (ref_str
[0] == '/') {
110 if (sscanf(ref_str
, ":%u%n", &ref
->chapter
, &n
) == 1 || sscanf(ref_str
, "%u%n", &ref
->chapter
, &n
) == 1) {
111 // 2, 3, 3a, 4, 5, 6, 9
112 ref_str
= &ref_str
[n
];
113 } else if (ref_str
[0] == '/') {
116 } else if (ref_str
[0] == '\0') {
118 ref
->type
= KJV_REF_EXACT
;
124 if (sscanf(ref_str
, ":%u%n", &ref
->verse
, &n
) == 1) {
126 ref_str
= &ref_str
[n
];
127 } else if (sscanf(ref_str
, "-%u%n", &ref
->chapter_end
, &n
) == 1) {
129 if (ref_str
[n
] != '\0') {
132 ref
->type
= KJV_REF_RANGE
;
134 } else if (ref_str
[0] == '/') {
137 } else if (ref_str
[0] == '\0') {
139 ref
->type
= KJV_REF_EXACT
;
146 int ret
= sscanf(ref_str
, "-%u%n", &value
, &n
);
147 if (ret
== 1 && ref_str
[n
] == '\0') {
149 ref
->verse_end
= value
;
150 ref
->type
= KJV_REF_RANGE
;
152 } else if (ret
== 1) {
154 ref
->chapter_end
= value
;
155 ref_str
= &ref_str
[n
];
156 } else if (ref_str
[0] == '\0') {
158 ref
->type
= KJV_REF_EXACT
;
160 } else if (sscanf(ref_str
, ",%u%n", &value
, &n
) == 1) {
162 ref
->verse_set
= intset_new();
163 intset_add(ref
->verse_set
, ref
->verse
);
164 intset_add(ref
->verse_set
, value
);
165 ref_str
= &ref_str
[n
];
167 if (sscanf(ref_str
, ",%u%n", &value
, &n
) != 1) {
170 intset_add(ref
->verse_set
, value
);
171 ref_str
= &ref_str
[n
];
173 if (ref_str
[0] != '\0') {
176 ref
->type
= KJV_REF_EXACT_SET
;
182 if (sscanf(ref_str
, ":%u%n", &ref
->verse_end
, &n
) == 1 && ref_str
[n
] == '\0') {
184 ref
->type
= KJV_REF_RANGE_EXT
;
191 ref
->type
= KJV_REF_SEARCH
;
192 if (regcomp(&ref
->search
, &ref_str
[1], REG_EXTENDED
|REG_ICASE
|REG_NOSUB
) != 0) {
195 ref
->search_str
= strdup(&ref_str
[1]);
200 str_join(size_t n
, char *strs
[])
203 for (size_t i
= 0; i
< n
; i
++) {
207 length
+= strlen(strs
[i
]);
209 char *str
= malloc(length
+ 1);
211 for (size_t i
= 0; i
< n
; i
++) {
215 strcat(str
, strs
[i
]);
221 kjv_bookequal(const char *a
, const char *b
, bool short_match
)
223 for (size_t i
= 0, j
= 0; ; ) {
224 if ((!a
[i
] && !b
[j
]) || (short_match
&& !b
[j
])) {
226 } else if (a
[i
] == ' ') {
228 } else if (b
[j
] == ' ') {
230 } else if (tolower(a
[i
]) != tolower(b
[j
])) {
240 kjv_book_matches(const char *book
, const kjv_verse
*verse
)
242 return kjv_bookequal(verse
->book_name
, book
, false) ||
243 kjv_bookequal(verse
->book_abbr
, book
, false) ||
244 kjv_bookequal(verse
->book_name
, book
, true);
248 kjv_verse_matches(const kjv_ref
*ref
, const kjv_verse
*verse
)
252 return (ref
->book
[0] == '\0' || kjv_book_matches(ref
->book
, verse
)) &&
253 (ref
->chapter
== 0 || verse
->chapter
== ref
->chapter
) &&
254 regexec(&ref
->search
, verse
->text
, 0, NULL
, 0) == 0;
257 return kjv_book_matches(ref
->book
, verse
) &&
258 (ref
->chapter
== 0 || ref
->chapter
== verse
->chapter
) &&
259 (ref
->verse
== 0 || ref
->verse
== verse
->verse
);
261 case KJV_REF_EXACT_SET
:
262 return kjv_book_matches(ref
->book
, verse
) &&
263 (ref
->chapter
== 0 || verse
->chapter
== ref
->chapter
) &&
264 intset_contains(ref
->verse_set
, verse
->verse
);
267 return kjv_book_matches(ref
->book
, verse
) &&
268 ((ref
->chapter_end
== 0 && ref
->chapter
== verse
->chapter
) ||
269 (verse
->chapter
>= ref
->chapter
&& verse
->chapter
<= ref
->chapter_end
)) &&
270 (ref
->verse
== 0 || verse
->verse
>= ref
->verse
) &&
271 (ref
->verse_end
== 0 || verse
->verse
<= ref
->verse_end
);
273 case KJV_REF_RANGE_EXT
:
274 return kjv_book_matches(ref
->book
, verse
) &&
276 (verse
->chapter
== ref
->chapter
&& verse
->verse
>= ref
->verse
&& ref
->chapter
!= ref
->chapter_end
) ||
277 (verse
->chapter
> ref
->chapter
&& verse
->chapter
< ref
->chapter_end
) ||
278 (verse
->chapter
== ref
->chapter_end
&& verse
->verse
<= ref
->verse_end
&& ref
->chapter
!= ref
->chapter_end
) ||
279 (ref
->chapter
== ref
->chapter_end
&& verse
->chapter
== ref
->chapter
&& verse
->verse
>= ref
->verse
&& verse
->verse
<= ref
->verse_end
)
287 #define KJV_DIRECTION_BEFORE -1
288 #define KJV_DIRECTION_AFTER 1
291 kjv_chapter_bounds(int i
, int direction
, int maximum_steps
)
293 assert(direction
== -1 || direction
== 1);
296 for ( ; 0 <= i
&& i
< kjv_verses_length
; i
+= direction
) {
297 if (maximum_steps
!= -1 && steps
>= maximum_steps
) {
300 if ((direction
== -1 && i
== 0) || (direction
== 1 && i
+ 1 == kjv_verses_length
)) {
303 const kjv_verse
*current
= &kjv_verses
[i
], *next
= &kjv_verses
[i
+ direction
];
304 if (current
->book
!= next
->book
|| current
->chapter
!= next
->chapter
) {
313 kjv_next_match(const kjv_ref
*ref
, int i
)
315 for ( ; i
< kjv_verses_length
; i
++) {
316 const kjv_verse
*verse
= &kjv_verses
[i
];
317 if (kjv_verse_matches(ref
, verse
)) {
332 kjv_range matches
[2];
336 kjv_next_addrange(kjv_next_data
*next
, kjv_range range
) {
337 if (next
->matches
[0].start
== -1 && next
->matches
[0].end
== -1) {
338 next
->matches
[0] = range
;
339 } else if (range
.start
< next
->matches
[0].end
) {
340 next
->matches
[0] = range
;
342 next
->matches
[1] = range
;
347 kjv_next_verse(const kjv_ref
*ref
, const kjv_config
*config
, kjv_next_data
*next
)
349 if (next
->current
>= kjv_verses_length
) {
353 if (next
->matches
[0].start
!= -1 && next
->matches
[0].end
!= -1 && next
->current
>= next
->matches
[0].end
) {
354 next
->matches
[0] = next
->matches
[1];
355 next
->matches
[1] = (kjv_range
){-1, -1};
358 if ((next
->next_match
== -1 || next
->next_match
< next
->current
) && next
->next_match
< kjv_verses_length
) {
359 int next_match
= kjv_next_match(ref
, next
->current
);
360 if (next_match
>= 0) {
361 next
->next_match
= next_match
;
363 .start
= kjv_chapter_bounds(next_match
, KJV_DIRECTION_BEFORE
, config
->context_chapter
? -1 : config
->context_before
),
364 .end
= kjv_chapter_bounds(next_match
, KJV_DIRECTION_AFTER
, config
->context_chapter
? -1 : config
->context_after
) + 1,
366 kjv_next_addrange(next
, bounds
);
368 next_match
= kjv_verses_length
;
372 if (next
->matches
[0].start
== -1 && next
->matches
[0].end
== -1) {
376 if (next
->current
< next
->matches
[0].start
) {
377 next
->current
= next
->matches
[0].start
;
380 return next
->current
++;
383 #define ESC_BOLD "\033[1m"
384 #define ESC_UNDERLINE "\033[4m"
385 #define ESC_RESET "\033[m"
388 kjv_output_verse(const kjv_verse
*verse
, FILE *f
, const kjv_config
*config
, const kjv_verse
*last_printed
)
390 if (last_printed
== NULL
|| verse
->book
!= last_printed
->book
) {
391 fprintf(f
, ESC_UNDERLINE
"%s" ESC_RESET
"\n", verse
->book_name
);
393 fprintf(f
, ESC_BOLD
"%d:%d" ESC_RESET
"\t", verse
->chapter
, verse
->verse
);
394 char verse_text
[1024];
395 strcpy(verse_text
, verse
->text
);
396 size_t characters_printed
= 0;
397 char *word
= strtok(verse_text
, " ");
398 while (word
!= NULL
) {
399 size_t word_length
= strlen(word
);
400 if (config
->linewrap
&& characters_printed
+ word_length
+ (characters_printed
> 0 ? 1 : 0) > config
->maximum_line_length
- 8 - 2) {
402 characters_printed
= 0;
404 if (characters_printed
> 0) {
406 characters_printed
++;
408 fprintf(f
, "%s", word
);
409 characters_printed
+= word_length
;
410 word
= strtok(NULL
, " ");
416 kjv_output(const kjv_ref
*ref
, FILE *f
, const kjv_config
*config
)
418 kjv_next_data next
= {
427 kjv_verse
*last_printed
= NULL
;
428 for (int verse_id
; (verse_id
= kjv_next_verse(ref
, config
, &next
)) != -1; ) {
429 kjv_verse
*verse
= &kjv_verses
[verse_id
];
430 kjv_output_verse(verse
, f
, config
, last_printed
);
431 last_printed
= verse
;
433 return last_printed
!= NULL
;
437 kjv_render(const kjv_ref
*ref
, const kjv_config
*config
)
440 if (pipe(fds
) == -1) {
447 dup2(fds
[0], STDIN_FILENO
);
448 char *args
[9] = {NULL
};
450 args
[arg
++] = "less";
453 if (ref
->search_str
!= NULL
) {
455 args
[arg
++] = ref
->search_str
;
461 execvp("less", args
);
462 printf("unable not exec less\n");
464 } else if (pid
== -1) {
465 printf("unable to fork\n");
469 FILE *output
= fdopen(fds
[1], "w");
470 bool printed
= kjv_output(ref
, output
, config
);
475 waitpid(pid
, NULL
, 0);
477 printf("unknown reference\n");
483 usage
= "usage: kjv [flags] [reference...]\n"
486 " -A num number of verses of context after matching verses\n"
487 " -B num number of verses of context before matching verses\n"
488 " -C show matching verses in context of the chapter\n"
496 " <Book>:<Chapter>\n"
497 " Individual chapter of a book\n"
498 " <Book>:<Chapter>:<Verse>[,<Verse>]...\n"
499 " Individual verse(s) of a specific chapter of a book\n"
500 " <Book>:<Chapter>-<Chapter>\n"
501 " Range of chapters in a book\n"
502 " <Book>:<Chapter>:<Verse>-<Verse>\n"
503 " Range of verses in a book chapter\n"
504 " <Book>:<Chapter>:<Verse>-<Chapter>:<Verse>\n"
505 " Range of chapters and verses in a book\n"
508 " All verses that match a pattern\n"
510 " All verses in a book that match a pattern\n"
511 " <Book>:<Chapter>/<Search>\n"
512 " All verses in a chapter of a book that match a pattern\n";
515 main(int argc
, char *argv
[])
517 kjv_config config
= {
519 .maximum_line_length
= 80,
523 .context_chapter
= false,
526 bool list_books
= false;
529 for (int opt
; (opt
= getopt(argc
, argv
, "A:B:ClWh")) != -1; ) {
533 config
.context_after
= strtol(optarg
, &endptr
, 10);
534 if (endptr
[0] != '\0') {
535 fprintf(stderr
, "kjv: invalid flag value for -A\n\n%s", usage
);
540 config
.context_before
= strtol(optarg
, &endptr
, 10);
541 if (endptr
[0] != '\0') {
542 fprintf(stderr
, "kjv: invalid flag value for -B\n\n%s", usage
);
547 config
.context_chapter
= true;
553 config
.linewrap
= false;
559 fprintf(stderr
, "kjv: invalid flag -%c\n\n%s", optopt
, usage
);
565 char *last_book_printed
= NULL
;
566 for (int i
= 0; i
< kjv_verses_length
; i
++) {
567 if (last_book_printed
== NULL
|| strcmp(kjv_verses
[i
].book_name
, last_book_printed
) != 0) {
568 printf("%s (%s)\n", kjv_verses
[i
].book_name
, kjv_verses
[i
].book_abbr
);
569 last_book_printed
= kjv_verses
[i
].book_name
;
575 struct winsize ttysize
;
576 if (ioctl(STDOUT_FILENO
, TIOCGWINSZ
, &ttysize
) == 0 && ttysize
.ws_col
> 0) {
577 config
.maximum_line_length
= ttysize
.ws_col
;
580 signal(SIGPIPE
, SIG_IGN
);
582 if (argc
== optind
) {
585 char *input
= readline("kjv> ");
590 kjv_ref
*ref
= kjv_newref();
591 int success
= kjv_parseref(ref
, input
);
594 kjv_render(ref
, &config
);
599 char *ref_str
= str_join(argc
-optind
, &argv
[optind
]);
600 kjv_ref
*ref
= kjv_newref();
601 int success
= kjv_parseref(ref
, ref_str
);
604 kjv_render(ref
, &config
);