6
\$\begingroup\$

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, namely StringBuffer and StringList.
  • todoargs.c deals with command line arguments which are parsed using getopt(). 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 in todoargs.h is "translated" into a function, it also contains the main() 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);
}
\$\endgroup\$
  • \$\begingroup\$ After string_buffer_init(), should .string, always point to a null character terminated C string after returning from some string_...()? If so, it is unclear why string_buffer_set_length() has test if (sb->capacity > new_len) before doing sb->string[sb->length] = '\0';. It looks like a hole that could result in a non-null character array. \$\endgroup\$ – chux - Reinstate Monica Aug 16 '18 at 17:54
  • \$\begingroup\$ @chux yes, after a (successful) string_append_*() call, sb->string should point to a null-terminated string. I added that test to "avoid problems" if string_buffer_set_length() (or string_buffer_clear()) is called after a string_buffer_init() but before any append operation. Maybe I should limit the use of set_length() to new_len values strictly less than sb->length... \$\endgroup\$ – MarcoLucidi Aug 16 '18 at 19:08
4
\$\begingroup\$

Your program expects the user to refer to completed or incomplete tasks by a numeric index. However, the Markdown file contains an unnumbered bulleted list, which forces the user to count the items manually (starting from 0). That design is inhumane. I suggest that you change the Markdown to use numbered lists.

By the way, "uncompleted" is not common in English usage. I suggest rewording it as "incomplete".

You are missing some required #includes. On my system, I see that…

  • todoargs.c needs to #include <unistd.h>:

    $ make
    gcc -std=c11 -Wall -Wextra -Werror -c -o todo.o todo.c
    gcc -std=c11 -Wall -Wextra -Werror -c -o todoargs.o todoargs.c
    todoargs.c:68:23: error: implicit declaration of function 'getopt' is invalid in
          C99 [-Werror,-Wimplicit-function-declaration]
            while ((opt = getopt(argc, argv, ":f:cuerAh")) != -1) {
    …
    

    This omission is your fault.

  • utils.c has a warning-treated-as-an-error when I compile it with clang 10.0.0 on macOS 10.13:

    $ make
    gcc -std=c11 -Wall -Wextra -Werror -c -o todoargs.o todoargs.c
    gcc -std=c11 -Wall -Wextra -Werror -c -o utils.o utils.c
    utils.c:245:25: error: implicit declaration of function 'strdup' is invalid in
          C99 [-Werror,-Wimplicit-function-declaration]
            char *str_dup = strdup(str);
                            ^
    …
    

    Actually, utils.c includes utils.h, which in turn includes string.h already. I believe that this is a bug in macOS/XCode, and have filed Apple Radar 45714179 for it. My reasoning:

    1. According to the X/OPEN group documentation, strdup() first appeared in Issue 4, Version 2, then moved from X/OPEN UNIX extension to BASE in Issue 5.

    2. According to Wikipedia, SUSv2 includes "the Base Definitions, Issue 5", which should therefore include strdup().

    3. According to GNU libc, #define XOPEN_SOURCE 500 will include definitions from the Single Unix Specification, version 2. Your code compiles just fine for me on Linux. Also, IBM's z/OS documentation confirms that #define XOPEN_SOURCE 500 makes available certain key functions that are associated with Single UNIX Specification, Version 2.

    4. The Apple man page on strdup(3) merely says that "The strdup() function first appeared in 4.4BSD", and does not specify what versioning macros are necessary to obtain its declaration. Your code does compile cleanly on macOS with #define _XOPEN_SOURCE 600, but I don't believe that that is a reasonable requirement.

\$\endgroup\$
  • \$\begingroup\$ "Inhumane" - harsh, but true. I'm a native speaker and don't find uncompleted uncommon - but I do agree with Wiktionary's note that "The word often carries the connotation of "the result of a failure to complete", rather than just incomplete. IOW, "incomplete" suggests the possibility of future completion. \$\endgroup\$ – Toby Speight Nov 1 '18 at 10:44
  • \$\begingroup\$ inhumane, jeez! Ok, I will add numbers to the raw file, you convinced me! English it's not my native language, I thought uncompleted meant not completed, thank you for pointing that out! And for missing unistd.h, it was an oversight, however the code compiles just fine on my system gcc (Debian 6.3.0-18+deb9u1) 6.3.0 20170516 without it, that's why I didn't notice. Do you have any clues about how it is possible? \$\endgroup\$ – MarcoLucidi Nov 1 '18 at 14:21
  • \$\begingroup\$ It happens all the time that code compiles even with missing includes. On of your existing includes likely happens to use unistd.h. It's not portable code, though. \$\endgroup\$ – 200_success Nov 1 '18 at 14:24
  • \$\begingroup\$ Is there any compiler flag that I can add to avoid such situations? \$\endgroup\$ – MarcoLucidi Nov 1 '18 at 14:33
  • 1
    \$\begingroup\$ I don't think that it's the compiler's job to verify your code. You can use a static analyzer, such as Include-What-You-Use (which I haven't tried myself). \$\endgroup\$ – 200_success Nov 1 '18 at 16:05

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.