This is a tiny project of mine, a minimal command line todo list program that I regularly use to manage my todos.
It saves todos using GitHub markdown task lists format in a plain text file.
The whole thing is divided into three files: utils.c
, todoargs.c
and todo.c
.
utils.c
contains few small utility functions along with the 2 "main" data structures used in the program, namelyStringBuffer
andStringList
.todoargs.c
deals with command line arguments which are parsed usinggetopt()
. In its header are defined the 13 "operations" that can be performed on the list and a datatype,TodoArgs
, which will hold the parsed operation and other parsed data.- in
todo.c
each operation defined intodoargs.h
is "translated" into a function, it also contains themain()
function and small functions to deal with a single todo.
I'm interested in hearing everything other people have to say about my code especially about implementation, readability and "design choices". If you spot a bug or something really "nasty", please let me know =)
The code is also on GitHub in which you will find usage examples and a Makefile.
utils.h
#ifndef UTILS_HEADER #define UTILS_HEADER #define _XOPEN_SOURCE 500 #include <stdlib.h> #include <stdbool.h> #include <string.h> #include <strings.h> #include <stdio.h> #include <stdarg.h> #include <limits.h> #include <sys/stat.h> /* Print question (which can be a format string) followed by " [y/N] " and waits * for user input. * Return true if user typed y or yes (case doesn't matter), false otherwise. */ bool ask_yes_no(const char *question, ...); /* Print "error: " + error + '\n' to stderr. error can be a format string. */ void print_error(const char *error, ...); /* Check if str is a replace pattern in the form /substring/replacement/ and * return the index of the middle slash. * Return 0 if str is not a replace patter. */ size_t is_replace_pattern(const char *str, size_t str_len); bool file_exists_and_is_reg(const char *filepath); bool file_exists_and_is_dir(const char *filepath); /* Return a long int >= 0 parsed from str using base 10 which can be casted to a * (large enough) unsigned type. * str may start with white spaces, '+' or '0'. It must end with a digit. * The max value the function can return is LONG_MAX - 1, if str represent a * value greater that that, it is considered a parse error. * On parse error return a number < 0. */ long int parse_unsigned(const char *str); /*** StringBuffer ***/ #define STRING_BUFFER_INITIAL_CAPACITY 32 /* Simple dynamic string buffer. * After the first append, string will always be a null-terminated cstring. */ typedef struct { size_t length; size_t capacity; char *string; } StringBuffer; /* Initializes sb fields to 0. */ void string_buffer_init(StringBuffer *sb); /* Appends c to sb->string increasing its capacity if necessary. * Return true on success, false on alloc error. */ bool string_buffer_append_char(StringBuffer *sb, char c); /* Appends str to sb->string increasing its capacity if necessary. * Return true on success, false on alloc error. */ bool string_buffer_append_string(StringBuffer *sb, const char *str); /* Appends sb_to_app.string to sb->string increasing sb capacity if necessary. * Return true on success, false on alloc error. */ bool string_buffer_append_string_buffer(StringBuffer *sb, StringBuffer *sb_to_app); /* Appends n strings from strings to sb->string. * If join_c is not '\0' appends join_c before each string. * Return true on success, false if an alloc error occurred and less than n * strings were appended. */ bool string_buffer_append_strings(StringBuffer *sb, char **strings, size_t n, char join_c); /* Finds first occurrence of needle inside sb->string starting from index start, * replaces it with str and return true. * Return false *only if* more space was needed, but the allocation failed. * If needle length is 0 or an occurrence of needle doesn't exist inside * sb->string (after index start) doesn't perform any operation and return true. */ bool string_buffer_replace_from(StringBuffer *sb, size_t start, const char *needle, const char *str); /* Set sb->length to new_len if sb->capacity greater than new_len. */ void string_buffer_set_length(StringBuffer *sb, size_t new_len); /* Clear content of sb->string making it an empty string. */ void string_buffer_clear(StringBuffer *sb); /* Frees sb->string and calls string_buffer_init() on sb. It's safe to pass * NULL. */ void string_buffer_free(StringBuffer *sb); /*** StringList ***/ #define STRING_LIST_INITIAL_CAPACITY 16 /* Simple dynamic list of (allocated) cstrings. */ typedef struct { size_t length; size_t capacity; char **list; } StringList; /* Return a new empty StringList of capacity STRING_LIST_INITIAL_CAPACITY. * Return NULL on alloc error. */ StringList *string_list_new(void); /* Append str to sl increasing sl capacity if necessary. * Return true on success, false on alloc error. */ bool string_list_append(StringList *sl, char *str); /* Like string_list_append() but appends an heap allocated copy of str. * Return true on success, false on alloc error. */ bool string_list_append_dup(StringList *sl, const char *str); /* Return string at index i. */ char *string_list_get(StringList *sl, size_t i); /* Replace string at index i with str and return the old string. */ char *string_list_get_replace(StringList *sl, size_t i, char *str); /* Calls func() once for each string in sl. */ void string_list_for_each(StringList *sl, void (*func)(char *str)); /* Like string_list_for_each() but also passes the index of the string to * func(). */ void string_list_for_each_index(StringList *sl, void (*func)(char *str, size_t i)); /* Like string_list_for_each_index() but calls func() only if test() returns * true. */ void string_list_for_each_index_if(StringList *sl, bool (*test)(const char *str), void (*func)(char *str, size_t i)); /* Calls test() once for each string in sl: if test() returns true, frees and * removes that string from the list. */ void string_list_for_each_remove_if(StringList *sl, bool (*test)(const char *str)); /* For each index in indices, frees and removes the string at that index. * indices array will be sorted. Indices contained in indices are assumed to be * unique and less then sl->length. * n is the number of elements in indices. */ void string_list_remove_multiple(StringList *sl, size_t *indices, size_t n); /* Writes strings contained in sl to file fp. */ void string_list_write(StringList *sl, FILE *fp); /* Frees and removes all strings contained in sl. */ void string_list_clear(StringList *sl); /* Frees all strings contained in sl and frees memory occupied by sl too. It's * safe to pass NULL. */ void string_list_free(StringList *sl); #endif
utils.c
#include "utils.h" bool ask_yes_no(const char *question, ...) { va_list ap; va_start(ap, question); vprintf(question, ap); va_end(ap); fputs(" [y/N] ", stdout); char ans[5] = { '\0' }; int i = 0; int c; while ((c = getchar()) != EOF && c != '\n' && i < 4) { ans[i++] = c; } return strcasecmp(ans, "y") == 0 || strcasecmp(ans, "yes") == 0; } void print_error(const char *error, ...) { fputs("error: ", stderr); va_list ap; va_start(ap, error); vfprintf(stderr, error, ap); va_end(ap); fputs("\n", stderr); } size_t is_replace_pattern(const char *str, size_t str_len) { if (str_len < 3) { return 0; } if (str[0] != '/' || str[str_len - 1] != '/') { return 0; } char *middle_slash_p = strchr(str + 1, '/'); size_t middle_slash_i = middle_slash_p - str; if (middle_slash_i == str_len -1) { return 0; } return middle_slash_i; } bool file_exists_and_is_reg(const char *filepath) { struct stat st; if (stat(filepath, &st) == -1) { return false; } return S_ISREG(st.st_mode); } bool file_exists_and_is_dir(const char *filepath) { struct stat st; if (stat(filepath, &st) == -1) { return false; } return S_ISDIR(st.st_mode); } long int parse_unsigned(const char *str) { char *end_p = NULL; long int i = strtol(str, &end_p, 10); if (i < 0 || i == LONG_MAX || *end_p != '\0' || end_p == str) { return -1; } return i; } /*** StringBuffer ***/ static bool string_buffer_resize(StringBuffer *sb, size_t new_cap) { void *new_str = realloc(sb->string, new_cap); if (new_str == NULL && new_cap > 0) { return false; } sb->string = new_str; sb->capacity = new_cap; return true; } static bool string_buffer_ensure_space(StringBuffer *sb, size_t n) { if (sb->capacity - sb->length >= n) { return true; } size_t inc = sb->capacity > 0 ? sb->capacity / 2 : STRING_BUFFER_INITIAL_CAPACITY; return string_buffer_resize(sb, sb->capacity + (inc > n ? inc : n)); } static bool string_buffer_append(StringBuffer *sb, const char *buf, size_t n) { if ( ! string_buffer_ensure_space(sb, n + 1)) { return false; } memcpy(sb->string + sb->length, buf, n); string_buffer_set_length(sb, sb->length + n); return true; } void string_buffer_init(StringBuffer *sb) { sb->length = 0; sb->capacity = 0; sb->string = NULL; } bool string_buffer_append_char(StringBuffer *sb, char c) { return string_buffer_append(sb, &c, 1); } bool string_buffer_append_string(StringBuffer *sb, const char *str) { return string_buffer_append(sb, str, strlen(str)); } bool string_buffer_append_string_buffer(StringBuffer *sb, StringBuffer *sb_to_app) { return string_buffer_append(sb, sb_to_app->string, sb_to_app->length); } bool string_buffer_append_strings(StringBuffer *sb, char **strings, size_t n, char join_c) { size_t i; for (i = 0; i < n; i++) { if (join_c != '\0' && i > 0 && ! string_buffer_append_char(sb, join_c)) { break; } if ( ! string_buffer_append_string(sb, strings[i])) { break; } } return i == n; } bool string_buffer_replace_from(StringBuffer *sb, size_t start, const char *needle, const char *str) { if (sb->length == 0) { return true; } size_t needle_len = strlen(needle); if (needle_len == 0) { return true; } char *needle_p = strstr(sb->string + start, needle); if (needle_p == NULL) { return true; } size_t needle_index = needle_p - sb->string; size_t str_len = strlen(str); if (needle_len < str_len) { if ( ! string_buffer_ensure_space(sb, str_len - needle_len + 1)) { return false; } // sb->string pointer could change after resize needle_p = sb->string + needle_index; } if (needle_len < str_len || needle_len > str_len) { memmove(needle_p + str_len, needle_p + needle_len, (sb->length + 1) - (needle_index + needle_len)); } memcpy(needle_p, str, str_len); sb->length = sb->length - needle_len + str_len; return true; } void string_buffer_set_length(StringBuffer *sb, size_t new_len) { if (sb->capacity > new_len) { sb->length = new_len; sb->string[sb->length] = '\0'; } } void string_buffer_clear(StringBuffer *sb) { string_buffer_set_length(sb, 0); } void string_buffer_free(StringBuffer *sb) { if (sb != NULL) { free(sb->string); string_buffer_init(sb); } } /*** StringList ***/ static bool string_list_resize(StringList *sl, size_t new_cap) { void *new_list = realloc(sl->list, sizeof(*(sl->list)) * new_cap); if (new_list == NULL && new_cap > 0) { return false; } sl->list = new_list; sl->capacity = new_cap; return true; } static bool string_list_grow_if_necessary(StringList *sl) { if (sl->capacity - sl->length > 0) { return true; } return string_list_resize(sl, sl->capacity + sl->capacity / 2); } StringList *string_list_new(void) { StringList *new_sl = malloc(sizeof(*new_sl)); if (new_sl == NULL) { return NULL; } new_sl->length = 0; new_sl->list = NULL; if ( ! string_list_resize(new_sl, STRING_LIST_INITIAL_CAPACITY)) { free(new_sl); return NULL; } return new_sl; } bool string_list_append(StringList *sl, char *str) { if ( ! string_list_grow_if_necessary(sl)) { return false; } sl->list[sl->length++] = str; return true; } bool string_list_append_dup(StringList *sl, const char *str) { char *str_dup = strdup(str); if (str_dup != NULL && string_list_append(sl, str_dup)) { return true; } free(str_dup); return false; } char *string_list_get(StringList *sl, size_t i) { return sl->list[i]; } char *string_list_get_replace(StringList *sl, size_t i, char *str) { char *old = sl->list[i]; sl->list[i] = str; return old; } void string_list_for_each(StringList *sl, void (*func)(char *str)) { for (size_t i = 0; i < sl->length; i++) { func(sl->list[i]); } } void string_list_for_each_index(StringList *sl, void (*func)(char *str, size_t i)) { for (size_t i = 0; i < sl->length; i++) { func(sl->list[i], i); } } void string_list_for_each_index_if(StringList *sl, bool (*test)(const char *str), void (*func)(char *str, size_t i)) { for (size_t i = 0; i < sl->length; i++) { if (test(sl->list[i])) { func(sl->list[i], i); } } } void string_list_for_each_remove_if(StringList *sl, bool (*test)(const char *str)) { size_t i, j; for (i = j = 0; i < sl->length; i++) { if (test(sl->list[i])) { free(sl->list[i]); } else { sl->list[j++] = sl->list[i]; } } sl->length = j; } static int indices_cmp(const void *a, const void *b) { size_t aa = *(size_t *)a; size_t bb = *(size_t *)b; if (aa > bb) { return 1; } else if (aa < bb) { return -1; } return 0; } void string_list_remove_multiple(StringList *sl, size_t *indices, size_t n) { qsort(indices, n, sizeof(*indices), indices_cmp); size_t i, j, q; for (i = j = q = 0; q < n; i++) { if (i == indices[q]) { free(sl->list[i]); q++; } else { sl->list[j++] = sl->list[i]; } } for (; i < sl->length; i++) { sl->list[j++] = sl->list[i]; } sl->length = j; } void string_list_write(StringList *sl, FILE *fp) { for (size_t i = 0; i < sl->length; i++) { fputs(sl->list[i], fp); } } void string_list_clear(StringList *sl) { for (size_t i = 0; i < sl->length; i++) { free(sl->list[i]); } sl->length = 0; } void string_list_free(StringList *sl) { if (sl != NULL) { string_list_clear(sl); free(sl->list); free(sl); } }
todoargs.h
#ifndef TODOARGS_HEADER #define TODOARGS_HEADER #include "utils.h" #include <stdint.h> #define TODO_LIST_FILENAME "TODO.md" #define TODO_FLAG_PRINT 0b00000001 #define TODO_FLAG_ADD 0b00000010 #define TODO_FLAG_COMPLETED 0b00000100 #define TODO_FLAG_UNCOMPLETED 0b00001000 #define TODO_FLAG_EDIT 0b00010000 #define TODO_FLAG_REMOVE 0b00100000 #define TODO_FLAG_ALL 0b01000000 #define TODO_LIST_ADD (TODO_FLAG_ADD) #define TODO_LIST_EDIT (TODO_FLAG_EDIT) #define TODO_LIST_PRINT (TODO_FLAG_PRINT) #define TODO_LIST_PRINT_COMPLETED (TODO_FLAG_PRINT | TODO_FLAG_COMPLETED) #define TODO_LIST_PRINT_UNCOMPLETED (TODO_FLAG_PRINT | TODO_FLAG_UNCOMPLETED) #define TODO_LIST_MARK_COMPLETED (TODO_FLAG_COMPLETED) #define TODO_LIST_MARK_UNCOMPLETED (TODO_FLAG_UNCOMPLETED) #define TODO_LIST_MARK_ALL_COMPLETED (TODO_FLAG_ALL | TODO_FLAG_COMPLETED) #define TODO_LIST_MARK_ALL_UNCOMPLETED (TODO_FLAG_ALL | TODO_FLAG_UNCOMPLETED) #define TODO_LIST_REMOVE (TODO_FLAG_REMOVE) #define TODO_LIST_REMOVE_ALL (TODO_FLAG_REMOVE | TODO_FLAG_ALL) #define TODO_LIST_REMOVE_COMPLETED (TODO_FLAG_REMOVE | TODO_FLAG_COMPLETED) #define TODO_LIST_REMOVE_UNCOMPLETED (TODO_FLAG_REMOVE | TODO_FLAG_UNCOMPLETED) typedef struct { uint8_t operation; // one of TODO_LIST_* macros. size_t *indices; // unique parsed indices. size_t n_indices; char *filepath; // selected todo list filepath. char **remaining_argv; size_t remaining_argc; } TodoArgs; /* Initializes todo_args fields to 0. */ void todo_args_init(TodoArgs *todo_args); /* Fills todo_args fields with arguments parsed from argv. * indices will not be parsed. todo_args->n_indices will be set to the expected * number of indices e.g in case of operation TODO_LIST_MARK_COMPLETED it will * be equal to todo_args->remaining_argc. * On parse (or alloc) error, print an error message using print_error() and * returns false. * Shows an usage message and returns false if option -h is detected. */ bool todo_args_parse_options(TodoArgs *todo_args, int argc, char **argv); /* Parse at most todo_args->n_indices from todo_args->remaining_argv and stores * them inside todo_args->indices (which will be allocated by this function) and * return true. * On parse (or alloc) error, print an error message using print_error() and * returns false. * In particular, fails if an index is < 0 or >= upper_bound. * Duplicates indices are ignored, todo_args->n_indices will hold the number of * unique indices parsed. */ bool todo_args_parse_indices(TodoArgs *todo_args, size_t upper_bound); /* Frees todo_args->filepath and todo_args->indices and calls todo_args_init() * on todo_args. It's safe to pass NULL. */ void todo_args_free(TodoArgs *todo_args); #endif
todoargs.c
#include "todoargs.h" /* Return an allocated string containing the todo list filepath. * * If filepath is not NULL and refers to a directory, return * "filepath/TODO_LIST_FILENAME", if it doesn't refer to a directory, return * "filepath". * If filepath is NULL and a regular file named TODO_LIST_FILENAME exist in the * current directory, return "TODO_LIST_FILENAME". * Otherwise return "$HOME/TODO_LIST_FILENAME". * * Return NULL on alloc error or if getenv("HOME") returned NULL. */ static char *get_todo_list_filepath(char *filepath); static void usage(const char *exec_name) { printf("Usage: %s [-f filepath] [-c] [-u] [-e] [-r] [-A] [indices]... [description]...\n" "\n" "Command line todo list.\n" "Saves todos using GitHub markdown task lists format in a plain text file.\n" "By default looks for a file named \"%s\" in the current directory, if it doesn't\n" "exists falls back to the main \"$HOME/%s\" file.\n" "If called with no arguments (besides -f), print to stdout all todos.\n" "\n" " -f filepath use filepath as todo list file. If filepath is a directory,\n" " looks for a file named \"%s\" inside that directory.\n" "\n" " description... add a new uncompleted todo to the list joining description strings.\n" "\n" " -c print completed todos.\n" " -c indices... mark specified todos as completed.\n" " -c -A mark all todos as completed.\n" "\n" " -u print uncompleted todos.\n" " -u indices... mark specified todos as uncompleted.\n" " -u -A mark all todos as uncompleted.\n" "\n" " -e index description... replace todo description at index index with description.\n" " -e index /sub/rep/ replace first occurrence of substring sub inside todo\n" " description at index index with rep.\n" "\n" " -r indices... remove specified todos.\n" " -r -c remove completed todos.\n" " -r -u remove uncompleted todos.\n" " -r -A remove all todos.\n" "\n" " -h show this message.\n" "\n" "repository: https://github.com/MarcoLucidi01/todo\n", exec_name, TODO_LIST_FILENAME, TODO_LIST_FILENAME, TODO_LIST_FILENAME); } void todo_args_init(TodoArgs *todo_args) { todo_args->operation = 0; todo_args->indices = NULL; todo_args->n_indices = 0; todo_args->remaining_argv = NULL; todo_args->remaining_argc = 0; todo_args->filepath = NULL; } bool todo_args_parse_options(TodoArgs *todo_args, int argc, char **argv) { char *filepath = NULL; int opt; while ((opt = getopt(argc, argv, ":f:cuerAh")) != -1) { switch (opt) { case 'f': filepath = optarg; break; case 'c': todo_args->operation |= TODO_FLAG_COMPLETED; break; case 'u': todo_args->operation |= TODO_FLAG_UNCOMPLETED; break; case 'e': todo_args->operation |= TODO_FLAG_EDIT; break; case 'r': todo_args->operation |= TODO_FLAG_REMOVE; break; case 'A': todo_args->operation |= TODO_FLAG_ALL; break; case 'h': usage(argv[0]); return false; case ':': print_error("option -%c requires an argument", optopt); return false; case '?': print_error("invalid option -%c", optopt); return false; default: print_error("unknown error"); return false; } } todo_args->remaining_argv = argv + optind; todo_args->remaining_argc = argc - optind; if (todo_args->operation == 0) { if (todo_args->remaining_argc == 0) { todo_args->operation |= TODO_FLAG_PRINT; } else { todo_args->operation |= TODO_FLAG_ADD; } } if (todo_args->remaining_argc == 0 && (todo_args->operation == TODO_FLAG_COMPLETED || todo_args->operation == TODO_FLAG_UNCOMPLETED)) { todo_args->operation |= TODO_FLAG_PRINT; } switch (todo_args->operation) { case TODO_LIST_PRINT: case TODO_LIST_PRINT_COMPLETED: case TODO_LIST_PRINT_UNCOMPLETED: case TODO_LIST_MARK_ALL_COMPLETED: case TODO_LIST_MARK_ALL_UNCOMPLETED: case TODO_LIST_REMOVE_ALL: case TODO_LIST_REMOVE_COMPLETED: case TODO_LIST_REMOVE_UNCOMPLETED: if (todo_args->remaining_argc > 0) { print_error("too many arguments provided"); return false; } break; case TODO_LIST_MARK_COMPLETED: case TODO_LIST_MARK_UNCOMPLETED: case TODO_LIST_REMOVE: if (todo_args->remaining_argc < 1) { print_error("missing todo index"); return false; } todo_args->n_indices = todo_args->remaining_argc; break; case TODO_LIST_EDIT: if (todo_args->remaining_argc < 2) { print_error("missing todo index and/or new description"); return false; } todo_args->n_indices = 1; break; case TODO_LIST_ADD: break; default: print_error("invalid arguments"); return false; } todo_args->filepath = get_todo_list_filepath(filepath); if (todo_args->filepath == NULL) { print_error("unable to select a valid todo list file path"); return false; } return true; } static char *get_todo_list_filepath(char *filepath) { StringBuffer filepath_buf; string_buffer_init(&filepath_buf); if (filepath != NULL) { if ( ! string_buffer_append_string(&filepath_buf, filepath)) { goto error; } if (file_exists_and_is_dir(filepath_buf.string)) { char *path_tail[2] = { "/", TODO_LIST_FILENAME }; if ( ! string_buffer_append_strings(&filepath_buf, path_tail, 2, '\0')) { goto error; } } return filepath_buf.string; } if ( ! string_buffer_append_string(&filepath_buf, TODO_LIST_FILENAME)) { goto error; } if (file_exists_and_is_reg(filepath_buf.string)) { return filepath_buf.string; } string_buffer_clear(&filepath_buf); char *home = getenv("HOME"); if (home == NULL) { goto error; } char *homepath[3] = { home, "/", TODO_LIST_FILENAME }; if ( ! string_buffer_append_strings(&filepath_buf, homepath, 3, '\0')) { goto error; } return filepath_buf.string; error: string_buffer_free(&filepath_buf); return NULL; } bool todo_args_parse_indices(TodoArgs *todo_args, size_t upper_bound) { todo_args->indices = malloc(sizeof(*(todo_args->indices)) * todo_args->n_indices); if (todo_args->indices == NULL) { return false; } bool already_seen[upper_bound]; memset(already_seen, false, upper_bound); // n is the expected number of indices size_t n = todo_args->n_indices; // todo_args->n_indices will hold the number of unique indices parsed todo_args->n_indices = 0; for (size_t i = 0; i < n; i++) { long int index = parse_unsigned(todo_args->remaining_argv[i]); if (index < 0 || (size_t)index >= upper_bound) { if (index < 0) { print_error("invalid index \"%s\"", todo_args->remaining_argv[i]); } else { print_error("index %ld is out of range", index); } free(todo_args->indices); todo_args->indices = NULL; return false; } if ( ! already_seen[index]) { already_seen[index] = true; todo_args->indices[todo_args->n_indices++] = index; } } todo_args->remaining_argv += n; todo_args->remaining_argc -= n; return true; } void todo_args_free(TodoArgs *todo_args) { if (todo_args != NULL) { free(todo_args->indices); free(todo_args->filepath); todo_args_init(todo_args); } }
todo.c
#include "utils.h" #include "todoargs.h" #include <errno.h> #define TODO_HEAD_UNCOMPLETED "- [ ] " #define TODO_HEAD_COMPLETED "- [x] " #define TODO_HEAD_LENGTH 6 /*** todo ***/ static bool todo_is_valid(const char *todo); static bool todo_is_completed(const char *todo); static bool todo_is_uncompleted(const char *todo); static void todo_print(char *todo, size_t index); static void todo_mark_completed(char *todo); static void todo_mark_uncompleted(char *todo); /* Allocates a new uncompleted todo built joining n strings from strings. * Return NULL on alloc error. */ static char *todo_build_from_strings(char **strings, size_t n); static bool todo_is_valid(const char *todo) { return todo_is_completed(todo) || todo_is_uncompleted(todo); } static bool todo_is_completed(const char *todo) { return strncmp(todo, TODO_HEAD_COMPLETED, TODO_HEAD_LENGTH) == 0; } static bool todo_is_uncompleted(const char *todo) { return strncmp(todo, TODO_HEAD_UNCOMPLETED, TODO_HEAD_LENGTH) == 0; } static void todo_print(char *todo, size_t index) { printf("%2zu%s", index, todo + 1); // skip leading - } static void todo_mark_completed(char *todo) { todo[3] = 'x'; } static void todo_mark_uncompleted(char *todo) { todo[3] = ' '; } static char *todo_build_from_strings(char **strings, size_t n) { StringBuffer todo_buf; string_buffer_init(&todo_buf); if ( ! string_buffer_append_string(&todo_buf, TODO_HEAD_UNCOMPLETED) || ! string_buffer_append_strings(&todo_buf, strings, n, ' ') || ! string_buffer_append_char(&todo_buf, '\n')) { string_buffer_free(&todo_buf); return NULL; } return todo_buf.string; } /*** todo_list ***/ /* Read todo list from file todo_list_fp and return a StringList containg todo * strings. * Return NULL if it reads an invalid todo or on alloc error. * StringList list returned should be freed. File read errors are not detected. */ static StringList *todo_list_read_from_file(FILE *todo_list_fp); /* Perform the operation todo_args->operation over todo_list and return true. * Return false if the operation performed returned false (e.g. * todo_list_add()) due to an alloc error. * Asks for user confirmation if the operation should modify more than one todo * but the user did not provide the indices. */ static bool todo_list_do_operation(StringList *todo_list, TodoArgs *todo_args); /* Builds a new todo from strings and appends it to todo_list. * Return true on success, false on alloc error. */ static bool todo_list_add(StringList *todo_list, char **strings, size_t n); /* Joins n strings from strings together and edit the todo at index i: * if the joined string represent a replace pattern in the form * /substring/replacement/ replace first occurrence of substring inside todo * description with replacement; * else replace description of todo with the joined string. * Return false on alloc error. */ static bool todo_list_edit(StringList *todo_list, size_t i, char **strings, size_t n); static void todo_list_print(StringList *todo_list); static void todo_list_print_completed(StringList *todo_list); static void todo_list_print_uncompleted(StringList *todo_list); static void todo_list_mark_completed(StringList *todo_list, size_t *indices, size_t n); static void todo_list_mark_uncompleted(StringList *todo_list, size_t *indices, size_t n); static void todo_list_mark_all_completed(StringList *todo_list); static void todo_list_mark_all_uncompleted(StringList *todo_list); static void todo_list_remove(StringList *todo_list, size_t *indices, size_t n); static void todo_list_remove_all(StringList *todo_list); static void todo_list_remove_completed(StringList *todo_list); static void todo_list_remove_uncompleted(StringList *todo_list); static StringList *todo_list_read_from_file(FILE *todo_list_fp) { StringBuffer todo_buf; string_buffer_init(&todo_buf); StringList *todo_list = string_list_new(); if (todo_list == NULL) { goto error; } int c; while ((c = fgetc(todo_list_fp)) != EOF) { if ( ! string_buffer_append_char(&todo_buf, c)) { goto error; } if (c == '\n') { if ( ! todo_is_valid(todo_buf.string) || ! string_list_append_dup(todo_list, todo_buf.string)) { goto error; } string_buffer_clear(&todo_buf); } } string_buffer_free(&todo_buf); return todo_list; error: string_buffer_free(&todo_buf); string_list_free(todo_list); return NULL; } static bool todo_list_do_operation(StringList *todo_list, TodoArgs *todo_args) { switch (todo_args->operation) { case TODO_LIST_ADD: return todo_list_add(todo_list, todo_args->remaining_argv, todo_args->remaining_argc); break; case TODO_LIST_EDIT: return todo_list_edit(todo_list, todo_args->indices[0], todo_args->remaining_argv, todo_args->remaining_argc); break; case TODO_LIST_PRINT: todo_list_print(todo_list); break; case TODO_LIST_PRINT_COMPLETED: todo_list_print_completed(todo_list); break; case TODO_LIST_PRINT_UNCOMPLETED: todo_list_print_uncompleted(todo_list); break; case TODO_LIST_MARK_COMPLETED: todo_list_mark_completed(todo_list, todo_args->indices, todo_args->n_indices); break; case TODO_LIST_MARK_UNCOMPLETED: todo_list_mark_uncompleted(todo_list, todo_args->indices, todo_args->n_indices); break; case TODO_LIST_MARK_ALL_COMPLETED: if (ask_yes_no("are you sure you want to mark all todos as completed?")) { todo_list_mark_all_completed(todo_list); } break; case TODO_LIST_MARK_ALL_UNCOMPLETED: if (ask_yes_no("are you sure you want to mark all todos as uncompleted?")) { todo_list_mark_all_uncompleted(todo_list); } break; case TODO_LIST_REMOVE: todo_list_remove(todo_list, todo_args->indices, todo_args->n_indices); break; case TODO_LIST_REMOVE_ALL: if (ask_yes_no("are you sure you want to remove all todos?")) { todo_list_remove_all(todo_list); } break; case TODO_LIST_REMOVE_COMPLETED: if (ask_yes_no("are you sure you want to remove completed todos?")) { todo_list_remove_completed(todo_list); } break; case TODO_LIST_REMOVE_UNCOMPLETED: if (ask_yes_no("are you sure you want to remove uncompleted todos?")) { todo_list_remove_uncompleted(todo_list); } break; } return true; } static bool todo_list_add(StringList *todo_list, char **strings, size_t n) { char *new_todo = todo_build_from_strings(strings, n); if (new_todo != NULL && string_list_append(todo_list, new_todo)) { return true; } free(new_todo); return false; } static bool todo_list_edit(StringList *todo_list, size_t i, char **strings, size_t n) { StringBuffer join_buf; string_buffer_init(&join_buf); if ( ! string_buffer_append_strings(&join_buf, strings, n, ' ')) { string_buffer_free(&join_buf); return false; } char *todo = string_list_get(todo_list, i); size_t todo_len = strlen(todo); StringBuffer todo_buf = { .length = todo_len, .capacity = todo_len + 1, .string = todo }; bool ret = false; size_t middle_slash_i = is_replace_pattern(join_buf.string, join_buf.length); if (middle_slash_i > 0) { join_buf.string[middle_slash_i] = '\0'; join_buf.string[join_buf.length - 1] = '\0'; const char *sub = join_buf.string + 1; const char *rep = join_buf.string + middle_slash_i + 1; ret = string_buffer_replace_from(&todo_buf, TODO_HEAD_LENGTH, sub, rep); } else { if (string_buffer_append_char(&join_buf, '\n')) { string_buffer_set_length(&todo_buf, TODO_HEAD_LENGTH); ret = string_buffer_append_string_buffer(&todo_buf, &join_buf); } } // todo_buf.string could be a new pointer after resize. No need to free // the old one, StringBuffer has take care of that. string_list_get_replace(todo_list, i, todo_buf.string); string_buffer_free(&join_buf); return ret; } static void todo_list_print(StringList *todo_list) { string_list_for_each_index(todo_list, todo_print); } static void todo_list_print_completed(StringList *todo_list) { string_list_for_each_index_if(todo_list, todo_is_completed, todo_print); } static void todo_list_print_uncompleted(StringList *todo_list) { string_list_for_each_index_if(todo_list, todo_is_uncompleted, todo_print); } static void todo_list_mark_completed(StringList *todo_list, size_t *indices, size_t n) { for (size_t i = 0; i < n; i++) { todo_mark_completed(string_list_get(todo_list, indices[i])); } } static void todo_list_mark_uncompleted(StringList *todo_list, size_t *indices, size_t n) { for (size_t i = 0; i < n; i++) { todo_mark_uncompleted(string_list_get(todo_list, indices[i])); } } static void todo_list_mark_all_completed(StringList *todo_list) { string_list_for_each(todo_list, todo_mark_completed); } static void todo_list_mark_all_uncompleted(StringList *todo_list) { string_list_for_each(todo_list, todo_mark_uncompleted); } static void todo_list_remove(StringList *todo_list, size_t *indices, size_t n) { string_list_remove_multiple(todo_list, indices, n); } static void todo_list_remove_all(StringList *todo_list) { string_list_clear(todo_list); } static void todo_list_remove_completed(StringList *todo_list) { string_list_for_each_remove_if(todo_list, todo_is_completed); } static void todo_list_remove_uncompleted(StringList *todo_list) { string_list_for_each_remove_if(todo_list, todo_is_uncompleted); } int main(int argc, char **argv) { int exit_status = EXIT_FAILURE; FILE *todo_list_fp = NULL; StringList *todo_list = NULL; TodoArgs todo_args; todo_args_init(&todo_args); if ( ! todo_args_parse_options(&todo_args, argc, argv)) { goto done; } todo_list_fp = fopen(todo_args.filepath, "r"); if (todo_list_fp == NULL && errno == ENOENT && ask_yes_no("file %s doesn't exists. Do you want to create it?", todo_args.filepath)) { todo_list_fp = fopen(todo_args.filepath, "w+"); } if (todo_list_fp == NULL) { print_error("unable to open todo list file %s", todo_args.filepath); goto done; } todo_list = todo_list_read_from_file(todo_list_fp); if (todo_list == NULL) { print_error("unable to read todo list file %s, maybe some todo is invalid", todo_args.filepath); goto done; } switch (todo_args.operation) { case TODO_LIST_EDIT: case TODO_LIST_REMOVE: case TODO_LIST_MARK_COMPLETED: case TODO_LIST_MARK_UNCOMPLETED: // operation requires at least one todo index if ( ! todo_args_parse_indices(&todo_args, todo_list->length)) { goto done; } } if ( ! todo_list_do_operation(todo_list, &todo_args)) { print_error("unable to perform the operation on todo list"); goto done; } if ( ! (todo_args.operation & TODO_FLAG_PRINT)) { // operation was not a simple print, we need to persist the todo list if (freopen(todo_args.filepath, "w", todo_list_fp) == NULL) { print_error("unable to save changes on todo list file %s", todo_args.filepath); goto done; } string_list_write(todo_list, todo_list_fp); } exit_status = EXIT_SUCCESS; done: todo_args_free(&todo_args); if (todo_list_fp != NULL) { fclose(todo_list_fp); } string_list_free(todo_list); exit(exit_status); }
string_buffer_init()
, should.string
, always point to a null character terminated C string after returning from somestring_...()
? If so, it is unclear whystring_buffer_set_length()
has testif (sb->capacity > new_len)
before doingsb->string[sb->length] = '\0';
. It looks like a hole that could result in a non-null character array.\$\endgroup\$string_append_*()
call,sb->string
should point to a null-terminated string. I added that test to "avoid problems" ifstring_buffer_set_length()
(orstring_buffer_clear()
) is called after astring_buffer_init()
but before any append operation. Maybe I should limit the use ofset_length()
tonew_len
values strictly less thansb->length
...\$\endgroup\$