#include #include #include #include #include #include #include #include #define EDIT_FILE "edit.toml" #define FIXED_FILE "fixed.toml" #define MAX_SEARCH_OPTS 5 // STRUCTS struct es { int len; char* ptr; }; struct es default_es = { 0, NULL }; struct Book { int id; struct es title; struct es author; int pages; struct es isbn; struct es language; struct es translator; struct es on[5]; int published; // maybe? //struct es authors[MAX_AUTHORS]; //struct es translators[MAX_TRANSLATORS]; }; void init_book(struct Book* book) { book->id = 0; book->title = default_es; book->author = default_es; book->pages = 0; book->isbn = default_es; struct es language = { 7, "English" }; // default language book->language = language; book->translator = default_es; for (int i = 0; i < 5; i++) book->on[i] = default_es; book->published = 0; } int PARSE_LINE = 1; const char* get_last_word(const char* str) { const char* last_space = strrchr(str, ' '); if ((last_space - str) > 0) return last_space + 1; else return 0; } long parse_int(char* current_pos, char** new_pos) { char c; char* value; // strtol can handle leading spaces // will put the first non-digit into endptr char* endptr; long ret = strtol(current_pos, &endptr, 10); bool valid; switch(*endptr) { case ' ': case '\n': case ']': case ',': valid = true; break; default: valid = false; break; } *new_pos = endptr; if (valid) return ret; else return 0; } #define SEEK_UNTIL(ptr, ch) while (*ptr != ch) ptr++; #define SEEK_WHILE(ptr, ch) while (*ptr == ch) ptr++; struct es parse_string(char* current_pos, char** new_pos) { // TODO handle failure char c; char* value; // leading spaces SEEK_UNTIL(current_pos, '"'); // go past the quote and set the position of the start of value current_pos++; value = current_pos; // until the next quote SEEK_UNTIL(current_pos, '"'); struct es output; output.len = current_pos - value; output.ptr = value; // go past the quote current_pos++; // update position *new_pos = current_pos; return output; } #define ATTR_MATCH(cand, attr) (strncmp(cand, attr, strlen(attr)) == 0) void parse_book(char* current_pos, struct Book* book) { char* attr; char c = *current_pos; char* new_pos; // loop until we hit the extra newline while (c != '\n') { // we start at the beginning of a line attr = current_pos; SEEK_UNTIL(current_pos, '='); // go past the equals sign current_pos++; // attr should be the name of the attribute, with (possibly) trailing spaces if (ATTR_MATCH(attr, "title")) { struct es title = parse_string(current_pos, &new_pos); book->title = title; current_pos = new_pos; } else if (ATTR_MATCH(attr, "author")) { struct es author = parse_string(current_pos, &new_pos); book->author = author; current_pos = new_pos; } else if (ATTR_MATCH(attr, "language")) { struct es language = parse_string(current_pos, &new_pos); book->language = language; current_pos = new_pos; } else if (ATTR_MATCH(attr, "isbn")) { struct es isbn = parse_string(current_pos, &new_pos); book->isbn = isbn; current_pos = new_pos; } else if (ATTR_MATCH(attr, "translator")) { struct es translator = parse_string(current_pos, &new_pos); book->translator = translator; current_pos = new_pos; } else if (ATTR_MATCH(attr, "pages")) { int pages = parse_int(current_pos, &new_pos); book->pages = pages; current_pos = new_pos; } else if (ATTR_MATCH(attr, "published")) { int published = parse_int(current_pos, &new_pos); book->published = published; current_pos = new_pos; } else if (ATTR_MATCH(attr, "id")) { int id = parse_int(current_pos, &new_pos); book->id = id; current_pos = new_pos; } else if (ATTR_MATCH(attr, "on")) { // this is always a list // look until we hit the opening brace SEEK_UNTIL(current_pos, '['); current_pos++; // go past opening brace // loop past opening spaces SEEK_WHILE(current_pos, ' '); // so long as we keep seeing quotes, keep reading them int on_count = 0; struct es value; while ((c = *current_pos) == '"') { // read the string value = parse_string(current_pos, &new_pos); book->on[on_count] = value; on_count++; current_pos = new_pos; // go past any commas or opening spaces SEEK_WHILE(current_pos, ' '); if (*current_pos != ',') break; SEEK_UNTIL(current_pos, '"'); } } // go to (and then past) the newline SEEK_UNTIL(current_pos, '\n'); PARSE_LINE += 1; c = *(++current_pos); } } char* next_book(char* current_pos) { while (!(ATTR_MATCH(current_pos, "[[books]]"))) { current_pos++; switch(*current_pos) { case '\0': return NULL; case '\n': PARSE_LINE += 1; break; default: break; } } // current_pos is at the beginning of [[books]] // pass it, and any spaces/newlines, then return current_pos += 9; // [[books]] SEEK_UNTIL(current_pos, '\n'); // current_pos is '\n', go past then return PARSE_LINE += 1; current_pos++; return current_pos; } bool regex_match(const char* pattern, const struct es text) { // empty pattern matches everything if (!*pattern) return true; // get lengths int pattern_length = strlen(pattern); // TODO it won't always be +2, but that accounts for both specials if (pattern_length > (text.len + 2)) return false; bool head_match = *pattern == '^'; bool tail_match = *(pattern + pattern_length - 1) == '$'; // if we have either head or tail (or both), we only need to compare once if (head_match && tail_match) // text must be identical to pattern (minus ^ and $) return text.len == (pattern_length - 2) && strncasecmp(pattern + 1, text.ptr, pattern_length - 2) == 0; else if (head_match) // text must match the pattern starting from pattern + 1 return strncasecmp(pattern + 1, text.ptr, pattern_length - 1) == 0; else if (tail_match) { // text starting from (pattern + 1) from the end must match pattern (without $) return strncasecmp(pattern, text.ptr + (text.len - pattern_length + 1), pattern_length - 1) == 0; } // we only need to compare while remaining text is // as long or longer than pattern for (int i = 0; i <= (text.len - pattern_length); i++) { if (strncasecmp(pattern, text.ptr + i, pattern_length) == 0) return true; } return false; } char* load_file(char* filename) { // open the file FILE* fp = fopen(filename, "r"); if (!fp) { printf("bad file\n"); return NULL; } // seek to the end fseek(fp, 0, SEEK_END); int size = ftell(fp); rewind(fp); char* data = malloc(size + 1); if (!data) { printf("couldn't malloc\n"); return NULL; } int read_size = fread(data, 1, size, fp); if (read_size != size) { printf("didn't read everything -- %d read of %d\n", read_size, size); return NULL; } data[size] = '\0'; fclose(fp); return data; } static struct option search_options[] = { {"show", no_argument, 0, 's'}, {"edit", no_argument, 0, 'e'}, {0, 0, 0, 0} // marks the end of the array }; struct search_opt { int show; int edit; int count; char* opts[MAX_SEARCH_OPTS]; char* args[MAX_SEARCH_OPTS]; }; struct search_opt parse_search_options(int argc, char* argv[]) { // return struct struct search_opt opt_out; int count = 0; int show = false; int edit = false; // opt options int opt; int opt_idx = 0; opterr = 0; // turn off getopt error messages // look at each option while ((opt = getopt_long_only(argc, argv, "", search_options, &opt_idx)) != -1) { switch(opt) { case 's': show = true; break; case 'e': edit = true; break; case '?': // optind points at the argument (one past the option) opt_out.opts[count] = argv[optind-1] + 2; // '--example' -> 'example' opt_out.args[count] = argv[optind]; count++; break; default: printf("something went wrong with parsing!\n"); break; } } // set the count/show values opt_out.count = count; opt_out.show = show; opt_out.edit = edit; return opt_out; } void print_book(struct Book book, bool all_fields) { printf("%.*s by %.*s\n", book.title.len, book.title.ptr, book.author.len, book.author.ptr); if (all_fields) { char* esfmt = " - %s: %.*s\n"; char* intfmt = " - %s: %d\n"; if (book.isbn.ptr) printf(esfmt, "isbn", book.isbn.len, book.isbn.ptr); if (book.language.ptr) printf(esfmt, "language", book.language.len, book.language.ptr); if (book.translator.ptr) printf(esfmt, "translator", book.translator.len, book.translator.ptr); if (book.pages) printf(intfmt, "pages", book.pages); if (book.published) printf(intfmt, "published", book.published); int on_count = 0; struct es tmp = book.on[on_count]; while (tmp.len != 0) { if (on_count == 0) printf(" - %s: [ %.*s", "on", tmp.len, tmp.ptr); else printf(", %.*s", tmp.len, tmp.ptr); on_count++; tmp = book.on[on_count]; } if (on_count != 0) printf(" ]\n"); } } void write_book(struct Book book, FILE *output) { fwrite("[[books]]\n", 1, 10, output); char str[100]; int size; if (book.id) { size = sprintf(str, "id = %d\n", book.id); fwrite(str, 1, size, output); } if (book.isbn.ptr) { size = sprintf(str, "isbn = \"%.*s\"\n", book.isbn.len, book.isbn.ptr); fwrite(str, 1, size, output); } if (book.title.ptr) { size = sprintf(str, "title = \"%.*s\"\n", book.title.len, book.title.ptr); fwrite(str, 1, size, output); } if (book.author.ptr) { size = sprintf(str, "author = \"%.*s\"\n", book.author.len, book.author.ptr); fwrite(str, 1, size, output); } if (book.pages) { size = sprintf(str, "pages = %d\n", book.pages); fwrite(str, 1, size, output); } if (book.published) { size = sprintf(str, "published = %d\n", book.published); fwrite(str, 1, size, output); } if (book.language.ptr) { size = sprintf(str, "language = \"%.*s\"\n", book.language.len, book.language.ptr); fwrite(str, 1, size, output); } if (book.translator.ptr) { size = sprintf(str, "translator = \"%.*s\"\n", book.translator.len, book.translator.ptr); fwrite(str, 1, size, output); } int on_count = 0; int str_offset = 0; struct es tmp = book.on[on_count]; if (tmp.len != 0) { // got at least one book str_offset += sprintf(str, "on = [ "); while (tmp.len != 0) { if (on_count != 0) str_offset += sprintf(str + str_offset, ", "); str_offset += sprintf(str + str_offset, "\"%.*s\"", tmp.len, tmp.ptr); on_count++; tmp = book.on[on_count]; } str_offset += sprintf(str + str_offset, " ]\n"); fwrite(str, 1, str_offset, output); } fwrite("\n", 1, 1, output); // trailing newline between books } void open(char* filepath) { char* editor = getenv("EDITOR"); if (!editor) editor = "nano"; pid_t pid; int status; switch ((pid = fork())) { case -1: printf("fork has failed!\n"); break; case 0: execlp(editor, editor, filepath, NULL); printf("child failed :(\n"); break; default: // wait for the child to finish pid = wait(&status); break; } } char** search(int argc, char* argv[], char* booki_file) { struct search_opt search_opts = parse_search_options(argc, argv); // get the books array char* data = load_file(booki_file); if (!data) { printf("couldn't load data from %s\n", booki_file); return NULL; } FILE *edit_file, *fixed_file = NULL; if (search_opts.edit) { edit_file = fopen(EDIT_FILE, "w"); fixed_file = fopen(FIXED_FILE, "w"); } // book loop int book_count = 0; struct Book book; char* cur_data = data; while ((cur_data = next_book(cur_data)) != NULL) { init_book(&book); parse_book(cur_data, &book); char* field; int i; bool match = true; for (i = 0; i < search_opts.count; i++) { field = search_opts.opts[i]; // compare fields if (ATTR_MATCH(field, "title")) { if (!regex_match(search_opts.args[i], book.title)) break; } else if (ATTR_MATCH(field, "author")) { if (!regex_match(search_opts.args[i], book.author)) break; } else if (ATTR_MATCH(field, "language")) { if (!regex_match(search_opts.args[i], book.language)) break; } else if (ATTR_MATCH(field, "on")) { int cnt = 0; struct es tmp = book.on[cnt]; bool match = false; while (tmp.len != 0) { if (regex_match(search_opts.args[i], tmp)) match = true; cnt++; tmp = book.on[cnt]; } if (!match) break; } else { printf("unsupported field: %s\n", field); break; } } match = i == search_opts.count; if (match) { if (search_opts.edit) write_book(book, edit_file); else print_book(book, search_opts.show); book_count++; } else if (search_opts.edit) { write_book(book, fixed_file); } } free(data); // if we're editing, both files are open at this point if (search_opts.edit) { fclose(edit_file); open(EDIT_FILE); data = load_file(EDIT_FILE); if (!data) { printf("can't open edit file\n"); return NULL; } cur_data = data; while ((cur_data = next_book(cur_data)) != NULL) { init_book(&book); parse_book(cur_data, &book); print_book(book, true); write_book(book, fixed_file); } fclose(fixed_file); } return argv + optind; } void help(bool err) { printf("booki! it's a thing\n"); if (err) { printf("you did something wrong\n"); } } int main(int argc, char* argv[]) { char* booki_file = getenv("BOOKI_FILE"); if (!booki_file) { printf("expecting BOOKI_FILE variable\n"); return 1; } char** remaining = NULL; if (argc == 1) { help(false); return 0; } else if (strcmp(argv[1], "open") == 0) { open(booki_file); } else if (strcmp(argv[1], "search") == 0) { remaining = search(argc - 1, argv + 1, booki_file); } else { printf("unknown subcommand: '%s'\n", argv[1]); return 1; } return 0; }