0
\$\begingroup\$

Inspired from Literate programming in Haskell.

Below follows a Literate script filter. A literate script is a program in which comments are given the leading role, whilst program text must be explicitly flagged as such by placing it between \begin{code} and \end{code} in the first column of each line.

litc is a filter that can be used to strip away all of the comment lines out a literate script file.

The default code markers are \begin{code} and \end{code}, but they can be changed with command-line arguments, provided that the begin and end markers are nonsimilar.

The following is a valid literate script:

Some text speaking about the code:

\begin{code}
#include <stdio.h>

int main(void) {
    puts("Hello World!");
}
\end{code}

Some more text..

and so is this:

Some text speaking about the code:

```c
#include <stdio.h>

int main(void) {
    puts("Hello World!");
}
```
Some more text...

GHC itself uses a standalone C program called unlit to process .lhs files.

Code:

#undef _POSIX_C_SOURCE
#undef _XOPEN_SOURCE

#define _POSIX_C_SOURCE 200819L
#define _XOPEN_SOURCE   700

#define IO_IMPLEMENTATION
#define IO_STATIC
#include "io.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <assert.h>

#include <unistd.h>
#include <getopt.h>

/* C2X/C23 or later? */
#if defined(__STDC_VERSION__) && __STDC_VERSION__ >= 202000L
    #include <stddef.h>             /* nullptr_t */
#else
    #include <stdbool.h>            /* bool, true, false */
    #define nullptr ((void *)0)
    typedef void *nullptr_t;
#endif                          /* nullptr, nullptr_t */

#define SHORT_OP_LIST   "b:e:o:h"
#define DEFAULT_BEGIN   "\\begin{code}"
#define DEFAULT_END     "\\end{code}"

/* Do we even need getopt? */
typedef struct {
    const char *bflag;          /* Begin flag. */
    const char *eflag;          /* End flag. */
    FILE *output;               /* Output to FILE. */
} flags;

static void help(void)
{
    printf("Usage: litc [OPTIONS] SRC\n\n"
        "  litc - extract code from a LaTEX document.\n\n"
        "Options:\n"
        "  -b, --begin          Line that denotes the beginning of the code\n"
        "                       block in the markup langugage. Default: %s.\n"
        "  -e, --end            Line that denotes the end of the code block in\n" 
        "                       the markup language. Default: %s.\n"
        "  -h, --help           Displays this message and exits.\n"
        "  -o, --output=FILE    Writes result to FILE instead of standard output.\n\n"
        "Note: The begin and end markers must be different.\n\n"
        "For Markdown, they can be:\n"
        "  ```python\n"
        "  # Some code here\n"
        "  ```\n",
        DEFAULT_BEGIN, DEFAULT_END);
    exit(EXIT_SUCCESS);
}

static void err_and_fail(void)
{
    fputs("The syntax of the command is incorrect.\n"
        "Try litc -h for more information.\n", stderr);
    exit(EXIT_FAILURE);
}

static void parse_options(const struct option   long_options[static 1],
                          flags                 opt_ptr[static 1], 
                          int                   argc, 
                          char *                argv[static argc])
{
    while (true) {
        const int c =
            getopt_long(argc, argv, SHORT_OP_LIST, long_options, nullptr);

        if (c == -1) {
            break;
        }

        switch (c) {
            case 'e':
            case 'b':
                if (optarg == nullptr) {
                    err_and_fail();
                }
                *(c == 'b' ? &opt_ptr->bflag : &opt_ptr->eflag) = optarg;
                break;
            case 'h':
                help();
                break;
            case 'o':
                /* If -o was provided more than once. */
                if (opt_ptr->output != stdout) {
                    fprintf(stderr, "Error: Multiple -o flags provided.\n");
                    err_and_fail();
                }

                errno = 0;
                opt_ptr->output = fopen(optarg, "a");

                if (opt_ptr->output == nullptr) {
                    perror(optarg);
                    exit(EXIT_FAILURE);
                }
                break;

                /* case '?' */
            default:
                err_and_fail();
                break;
        }
    }
}

static bool write_codelines(flags   options[static 1],
                            size_t  nlines, 
                            char *  lines[static nlines])
{
    /* Would these 7 variables be better expressed as a struct? */
    /* Perhaps:
     *
     * What would be a better name for this struct?
     *
     * struct {
     *     FILE *const f;
     *     const char *const begin_marker;
     *     const char *const end_marker;
     *     size_t ncodelines;       // No. of lines seen so far. 
     *     size_t curr_line;
     *     size_t last_begin_pos;
     *     bool code_mode;
     * } code = {options->output,
     *           options->bflag ? options->bflag : DEFAULT_BEGIN,
     *           options->eflag ? options->eflag : DEFAULT_END,
     *           1,
     *           1,
     *           0,
     *           false
     * }; 
     */
    const char *const begin_marker =
        options->bflag ? options->bflag : DEFAULT_BEGIN;
    const char *const end_marker =
        options->eflag ? options->eflag : DEFAULT_END;
    FILE *const f = options->output;

    if (f != stdout && ftruncate(fileno(f), 0)) {
        perror("seek()");
        return false;
    }

    bool code_mode = false;
    size_t ncodelines = 1;
    size_t curr_line = 1;
    size_t last_begin_pos = 0;

    for (size_t i = 0; i < nlines; ++i, ++curr_line) {
        if (code_mode) {
            if (strcmp(lines[i], begin_marker) == 0) {
                fprintf(stderr, "Error: %s missing, %s started at line: %zu.\n",
                    end_marker, begin_marker, last_begin_pos);
                goto cleanup_and_fail;
            }

            if (strcmp(lines[i], end_marker) == 0) {
                code_mode = false;
            } else {
                if (fprintf(f, "%s\n", lines[i]) < 0) {
                    perror("fprintf()");
                    goto cleanup_and_fail;
                }
                ++ncodelines;
            }
        } else {
            if (strcmp(lines[i], end_marker) == 0) {
                fprintf(stderr, "Error: spurious %s at line: %zu.\n",
                    end_marker, curr_line);
                goto cleanup_and_fail;
            }

            code_mode = strcmp(lines[i], begin_marker) == 0;

            if (code_mode) {
                /* Only print newlines after the first code block. */
                if (ncodelines > 1) {
                    fputc('\n', f);
                }

                last_begin_pos = curr_line;
            }
        }
    }

    if (code_mode) {
        fprintf(stderr, "Error: %s missing. %s started at line: %zu.\n",
            end_marker, begin_marker, last_begin_pos);
        goto cleanup_and_fail;
    }

    if (ncodelines == 0) {
        fprintf(stderr, "Error: no code blocks were found in the file.\n");
        goto cleanup_and_fail;
    }

    goto success;

  cleanup_and_fail:
    if (f != stdout) {
        fclose(f);
    }

    return false;

  success:
    return f == stdout || !fclose(f);
}

int main(int argc, char *argv[])
{

    /* Sanity check. POSIX requires the invoking process to pass a non-null
     * argv[0]. 
     */
    if (!argv) {
        fputs("A NULL argv[0] was passed through an exec system call.\n",
            stderr);
        return EXIT_FAILURE;
    }

    static const struct option long_options[] = {
        { "begin",  required_argument,  nullptr,    'b' },
        { "end",    required_argument,  nullptr,    'e' },
        { "help",   no_argument,        nullptr,    'h' },
        { "output", required_argument,  nullptr,    'o' },
        { nullptr,  0,                  nullptr,     0  },
    };

    FILE *in_file = stdin;
    flags options = { nullptr, nullptr, stdout };

    parse_options(long_options, &options, argc, argv);

    if ((optind + 1) == argc) {
        in_file = fopen(argv[optind], "r");
        if (!in_file) {
            perror(argv[optind]);

            if (options.output) {
                fclose(options.output);
            }
            return EXIT_FAILURE;
        }
    }

    else if (optind > argc) {
        err_and_fail();
    }

    size_t nbytes = 0;
    char *const content = io_read_file(in_file, &nbytes);
    size_t nlines = 0;
    char **lines = io_split_lines(content, &nlines);
    int status = EXIT_FAILURE;

    if (!lines) {
        perror("fread()");
        goto cleanup_and_fail;
    }

    if (!write_codelines(&options, nlines, lines)) {
        goto cleanup_and_fail;
    }

    status = EXIT_SUCCESS;

  cleanup_and_fail:
    /* As we're exiting, we don't need to free anything. */
    /* free(lines); */
    /* free(content); */

    if (in_file != stdin) {
        fclose(in_file);
    }

    return status;
}
#ifndef IO_H
#define IO_H

#include <stdio.h>
#include <stdbool.h>
#include <stdint.h>

/* 
 * To use, do this:
 *   #define IO_IMPLEMENTATION
 * before you include this file in *one* C to create the implementation.
 *
 * i.e. it should look like:
 * #include ...
 * #include ...
 *
 * #define IO_IMPLEMENTATION
 * #include "io.h"
 * ...
 * 
 * To make all functions have internal linkage, i.e. be private to the source
 * file, do this:
 *  #define `IO_STATIC` 
 * before including "io.h".
 *
 * i.e. it should look like:
 * #define IO_IMPLEMENTATION
 * #define IO_STATIC
 * #include "io.h"
 * ...
 *
 * You can #define IO_MALLOC, IO_REALLOC, and IO_FREE to avoid using malloc(),
 * realloc(), and free(). Note that all three must be defined at once, or none.
 */

#ifndef IO_DEF
    #ifdef IO_STATIC
        #define IO_DEF  static
#else
    #define IO_DEF  extern
    #endif              /* IO_STATIC */
#endif                  /* IO_DEF */

#if defined(__GNUC__) || defined(__clang__)
    #define ATTRIB_NONNULL(...)             __attribute__((nonnull (__VA_ARGS__)))
    #define ATTRIB_WARN_UNUSED_RESULT       __attribute__((warn_unused_result))
    #define ATTRIB_MALLOC                   __attribute__((malloc))
#else
    #define ATTRIB_NONNULL(...)             /* If only. */
    #define ATTRIB_WARN_UNUSED_RESULT       /* If only. */
    #define ATTRIB_MALLOC                   /* If only. */ 
#endif                          /* defined(__GNUC__) || define(__clang__) */

/* 
 * Reads the file pointed to by `stream` to a buffer and returns it.
 * The returned buffer is a nul-terminated string.
 * If `nbytes` is not NULL, it shall hold the size of the file. Otherwise it
 * shall hold 0.
 * 
 * Returns NULL on memory allocation failure. The caller is responsible for
 * freeing the returned pointer.
 */
IO_DEF char *io_read_file(FILE *stream, size_t *nbytes)
    ATTRIB_NONNULL(1) ATTRIB_WARN_UNUSED_RESULT ATTRIB_MALLOC;

/* 
 * Splits a string into a sequence of tokens. The `delim` argument 
 * specifies a set of bytes that delimit the tokens in the parsed string.
 * If `ntokens` is not NULL, it shall hold the amount of total tokens. Else it
 * shall hold 0.
 *
 * Returns an array of pointers to the tokens, or NULL on memory allocation
 * failure. The caller is responsible for freeing the returned pointer.
 */
IO_DEF char **io_split_by_delim(char *restrict s, const char *restrict delim,
    size_t *ntokens) 
    ATTRIB_NONNULL(1, 2) ATTRIB_WARN_UNUSED_RESULT ATTRIB_MALLOC;

/* 
 * Splits a string into lines.
 * A wrapper around `io_split_by_delim()`. It calls the function with "\n" as
 * the delimiter.
 *
 * Returns an array of pointers to the tokens, or NULL on memory allocation
 * failure. The caller is responsible for freeing the returned pointer.
 */
IO_DEF char **io_split_lines(char *s, size_t *nlines)
    ATTRIB_NONNULL(1) ATTRIB_WARN_UNUSED_RESULT ATTRIB_MALLOC;

/* 
 * Reads the next chunk of data from the stream referenced to by `stream`.
 * `chunk` must be a pointer to an array of at least size IO_CHUNK_SIZE. 
 *
 * If `size` is a non-null pointer, it'd hold the size of the chunk, else it
 * would hold 0 on failure.
 *
 * Returns a pointer to the chunk on success, or NULL elsewise. The returned
 * chunk is null-terminated.
 *
 * `io_read_next_chunk()` does not distinguish between end-of-file and error; the
 * routines `feof()` and `ferror()` must be used to determine which occured.
 */
IO_DEF char *io_read_next_chunk(FILE *restrict stream, char *restrict chunk, size_t *size)
    ATTRIB_NONNULL(1, 2) ATTRIB_WARN_UNUSED_RESULT;

/* 
 * Reads the next line from the stream pointed to by `stream`. The returned line 
 * is terminated and does not contain a newline, if one was found.
 *
 * The memory pointed to by `size` shall contain the length of the 
 * line (including the terminating null character). Else it shall contain 0.
 *  
 * Upon successful completion a pointer is returned and the size of the line is 
 * stored in the memory pointed to by `size`, otherwise NULL is returned and
 * `size` holds 0.
 * 
 * `io_read_line()` does not distinguish between end-of-file and error; the routines
 * `feof()` and `ferror()` must be used to determine which occurred. The
 * function also returns NULL on a memory-allocation failure. 
 *
 * Although a null character is always supplied after the line, note that
 * `strlen(line)` will always be smaller than the value is `size` if the line
 * contains embedded null characters.
 */
IO_DEF char *io_read_line(FILE *stream, size_t *size)
    ATTRIB_NONNULL(1, 2) ATTRIB_WARN_UNUSED_RESULT ATTRIB_MALLOC;

/*
 * `size` should be a non-null pointer. On success, the function assigns `size`
 * with the number of bytes read and returns true, or returns false elsewise.
 * The function also returns false if the size of the file can not be
 * represented.
 *
 * Note: The file can grow between io_fsize() and a subsequent read.
 */
IO_DEF bool io_fsize(FILE *stream, uintmax_t *size) 
    ATTRIB_NONNULL(1, 2) ATTRIB_WARN_UNUSED_RESULT;

/* 
 * Writes `lines` to the file pointed to by `stream`.
 * A wrapper around 
 * On success, it returns true, or false elsewise.
 */
IO_DEF bool io_write_lines(FILE *stream, size_t nlines, char *lines[const static nlines]) 
    ATTRIB_NONNULL(1, 3);

/* 
 * Writes nbytes from the buffer pointed to by `data` to the file pointed to 
 * by `stream`. 
 *
 * On success, it returns true, or false elsewise.
 */
IO_DEF bool io_write_file(FILE *stream, size_t nbytes, const char data[static nbytes]) 
    ATTRIB_NONNULL(1, 3);

#endif                          /* IO_H */

#ifdef IO_IMPLEMENTATION

#if defined(IO_MALLOC) != defined(IO_REALLOC) || defined(IO_REALLOC) != defined(IO_FREE)
    #error  "Must define all or none of IO_MALLOC, IO_REALLOC, and IO_FREE."
#endif

#ifndef IO_MALLOC
#define IO_MALLOC(sz)       malloc(sz)
#define IO_REALLOC(p, sz)   realloc(p, sz)
#define IO_FREE(p)          free(p)
#endif

#undef _POSIX_C_SOURCE
#define _POSIX_C_SOURCE 200809L

#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include <string.h>

#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>

#define IO_CHUNK_SIZE          ((size_t)1024 * 8)
#define IO_TOKEN_CHUNK_SIZE    ((size_t)1024 * 2)

#define GROW_CAPACITY(capacity, initial) \
        ((capacity) < initial ? initial : (capacity) * 2)

IO_DEF char *io_read_file(FILE *stream, size_t *nbytes)
{
    char *content = NULL;
    size_t len = 0;
    size_t capacity = 0;

    if (nbytes) {
        *nbytes = 0;
    }

    for (size_t rcount = 1; rcount > 0; len += rcount) {
        capacity = GROW_CAPACITY(capacity, IO_CHUNK_SIZE);

        void *const tmp = IO_REALLOC(content, capacity + 1);

        if (tmp == NULL) {
            IO_FREE(content);
            return NULL;
        }
        content = tmp;
        rcount = fread(content + len, 1, capacity - len, stream);

        if (rcount < capacity - len) {
            if (!feof(stream)) {
                IO_FREE(content);
                return content = NULL;
            }
            /* If we break on the first iteration. */
            len += rcount;
            break;
        }
    }

    if (nbytes) {
        *nbytes = len;
    }
    content[len] = '\0';
    return content;
}

IO_DEF char **io_split_by_delim(char *restrict s, const char *restrict delim,
    size_t *ntokens)
{
    char **tokens = NULL;
    size_t capacity = 0;
    size_t token_count = 0;

    if (ntokens) {
        *ntokens = 0;
    }

    while (s != NULL && *s != '\0') {
        if (token_count >= capacity) {
            capacity = GROW_CAPACITY(capacity, IO_TOKEN_CHUNK_SIZE);
            char **const tmp = IO_REALLOC(tokens, sizeof *tokens * capacity);

            if (tmp == NULL) {
                IO_FREE(tokens);
                return NULL;
            }
            tokens = tmp;
        }
        tokens[token_count++] = s;
        s = strpbrk(s, delim);

        if (s) {
            *s++ = '\0';
        }
    }

    if (ntokens) {
        *ntokens = token_count;
    }
    return tokens;
}

IO_DEF char **io_split_lines(char *s, size_t *nlines)
{
    return io_split_by_delim(s, "\n", nlines);
}

IO_DEF char *io_read_next_chunk(FILE *stream, char *chunk, size_t *size)
{
    if (size) {
        *size = 0;
    }
    
    const size_t rcount = fread(chunk, 1, IO_CHUNK_SIZE, stream);

    if (rcount < IO_CHUNK_SIZE) {
        if (!feof(stream)) {
            /* A read error occured. */
            return NULL;
        }

        if (rcount == 0) {
            return NULL;
        }
    }
    
    chunk[rcount] = '\0';

    if (size) {
        *size = rcount;
    }

    return chunk;
}

IO_DEF char *io_read_line(FILE *stream, size_t *size)
{
    size_t count = 0;
    size_t capacity = 0;
    char *line = NULL;

    for (;;) {
        if (count >= capacity) {
            capacity = GROW_CAPACITY(capacity, BUFSIZ);
            char *const tmp = realloc(line, capacity + 1);

            if (tmp == NULL) {
                free(line);
                return NULL;
            }

            line = tmp;
        }

        const int c = getc(stream);

        if (c == EOF || c == '\n') {
            if (c == EOF) {
                if (feof(stream)) {
                    if (!count) {
                        free(line);
                        return NULL;
                    }
                    /* Return what was read. */
                    break;
                }
                /* Read error. */
                free(line);
                return NULL;
            } else {
                break;
            }
        } else {
            line[count] = (char) c;
        }
        ++count;
    }

    /* Shrink line to size if possible. */
    void *const tmp = realloc(line, count + 1);

    if (tmp) {
        line = tmp;
    }

    line[count] = '\0';
    *size = ++count;
    return line;
}

/* 
 * Reasons to not use `fseek()` and `ftell()` to compute the size of the file:
 * 
 * Subclause 7.12.9.2 of the C Standard [ISO/IEC 9899:2011] specifies the
 * following behavior when opening a binary file in binary mode:
 * 
 * >> A binary stream need not meaningfully support fseek calls with a whence 
 * >> value of SEEK_END.
 *
 * In addition, footnote 268 of subclause 7.21.3 says:
 *
 * >> Setting the file position indicator to end-of-file, as with 
 * >> fseek(file, 0, SEEK_END) has undefined behavior for a binary stream.
 *
 * For regular files, the file position indicator returned by ftell() is useful
 * only in calls to fseek. As such, the value returned may not be reflect the 
 * physical byte offset. 
 *
 */
bool io_fsize(FILE *stream, uintmax_t *size)
{
/*
 *   Windows supports fileno(), struct stat, and fstat() as _fileno(),
 *   _fstat(), and struct _stat.
 *
 *   See: https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/fstat-fstat32-fstat64-fstati64-fstat32i64-fstat64i32?view=msvc-170
 */

#ifdef _WIN32
    #define fileno _fileno
    #ifdef _WIN64
        #define fstat  _fstat64
        #define stat   __stat64
    #else
        /* Does this suffice for a 32-bit system? */
        #define fstat  _fstat
        #define stat   _stat
    #endif                          /* WIN64 */
#endif                              /* _WIN32 */

/* According to https://web.archive.org/web/20191012035921/http://nadeausoftware.com/articles/2012/01/c_c_tip_how_use_compiler_predefined_macros_detect_operating_system
 * __unix__ should suffice for IBM AIX, all distributions of BSD, and all
 * distributions of Linux, and Hewlett-Packard HP-UX. __unix suffices for Oracle
 * Solaris. Mac OSX and iOS compilers do not define the conventional __unix__,
 * __unix, or unix macros, so they're checked for separately. WIN32 is defined
 * on 64-bit systems too.
 */
#if defined(_WIN32) || defined(__unix__) || defined(__unix) || (defined(__APPLE__) && defined(__MACH__))
    struct stat st;

    /* rewind() returns no value. */
    rewind(stream);

    if (fstat(fileno(stream), &st) == 0) {
        *size = (uintmax_t) st.st_size;
        return true;
    }
    return false;
#else
    /* Fall back to the default and read it in chunks. */
    uintmax_t rcount = 0;
    char chunk[IO_CHUNK_SIZE];

    /* rewind() returns no value. */
    rewind(stream);

    do {
        rcount = fread(chunk, 1, IO_CHUNK_SIZE, stream);

        if ((*size + rcount) < *size) {
            /* Overflow. */
            return false;
        }
        *size += rcount;
    } while (rcount == IO_CHUNK_SIZE);
    return !ferror(stream);
#endif                          /* defined(_WIN32) || defined(__unix__) || defined(__unix) || (defined(__APPLE__) && defined(__MACH__)) */
#undef fstat
#undef stat
#undef fileno
}

IO_DEF bool io_write_lines(FILE *stream, size_t nlines, 
        char *lines[const static nlines])
{
    for (size_t i = 0; i < nlines; ++i) {
        if (fprintf(stream, "%s\n", lines[i]) < 0) {
            return false;
        }
    }

    return true;
}

IO_DEF bool io_write_file(FILE *stream, size_t nbytes,
    const char data[static nbytes])
{
    return fwrite(data, 1, nbytes, stream) == nbytes;
}

#undef ATTRIB_NONNULL
#undef ATTRIB_WARN_UNUSED_RESULT
#undef ATTRIB_MALLOC
#undef TOKEN_IO_CHUNK_SIZE
#undef GROW_CAPACITY
#endif                          /* IO_IMPLEMENTATION */

#ifdef TEST_MAIN

#include <assert.h>

int main(int argc, char **argv)
{
    if (argc != 2) {
        fputs("Error: file argument missing.\n", stderr);
        return EXIT_FAILURE;
    }

    FILE *fp = fopen(argv[1], "r");
    if (!fp) {
        perror(argv[1]);
        return EXIT_FAILURE;
    }
    
    size_t nbytes = 0;
    char *const fbuf = io_read_file(fp, &nbytes);
    assert(fbuf && nbytes);
    assert(io_write_file(stdout, nbytes, fbuf));
    rewind(fp);

    size_t size = 0;
    bool rv = io_fsize(fp, &size);
    assert(rv);
    printf("Filesize: %zu.\n", size);

    size_t nlines = 0;
    char **lines = io_split_lines(fbuf, &nlines);
    assert(lines && nlines);
    assert(io_write_lines(stdout, nlines, lines));

    printf("Lines read: %zu.\n", nlines);

    for (size_t i = 0; i < nlines; ++i) {
        if (lines[i][0]) {
            size_t ntokens = 0;
            char **tokens = io_split_by_delim(lines[i], " \t", &ntokens);
            assert(tokens && ntokens);
            assert(io_write_lines(stdout, ntokens, tokens));
            free(tokens);
        }
    }
    
    rewind(fp);
 
    /* This can be allocated dynamically on the heap too. */
    char chunk[IO_CHUNK_SIZE];
    char *p = chunk;
    size_t chunk_size = 0;

    while ((p = io_read_next_chunk(fp, chunk, &chunk_size))) {
        printf("Read a chunk of size: %zu.\n", chunk_size); 
        puts(chunk);
    }
    
    rewind(fp);

    char *line = NULL;
    size_t line_size = 0;

    while ((line = io_read_line(fp, &line_size))) {
        line[strcspn(line, "\n")] = '\0';
        printf("Read a line of size: %zu.\n", line_size); 
        puts(line);
        putchar('\n');
        free(line);
    }

    free(fbuf);
    free(lines);    
    fclose(fp);
    
    return EXIT_SUCCESS;
}

#endif                          /* TEST_MAIN */

The programs need to be in the manner and order imposed by the relevant compiler, unlike Wikipedia's definition of Literate Programming.

Review Request:

General coding comments, style, et cetera.

Do you see any bugs or undefined/implementation-defined behavior? As io.h has previously been reviewed (this is not to say that it must be bug-free now), I am mainly interested in getting the main program reviewed.

\$\endgroup\$
18
  • \$\begingroup\$ Why 202000L in __STDC_VERSION__ >= 202000L - where does that value come from? (The only stdc versions I know of are: 199409, 199901, 201112, 201710). Why the L given there is no long in macro compares? \$\endgroup\$ Commented Mar 12 at 12:23
  • \$\begingroup\$ @chux-ReinstateMonica __STDC_VERSION__, _POSIX_C_SOURCE, etc. are always explicitly defined as longs. (And you forgot 202311L; I don't know where OP got the value they're using either) \$\endgroup\$
    – Shawn
    Commented Mar 12 at 15:14
  • \$\begingroup\$ Filtering is not merely the stripping away of comments. A literate program processor should assemble a program A from fragments B, C, and D when I write: comments on fragment B <<B>>= code fragment B @ comments on fragment C <<C>>= code fragment C @ comments on program as a whole <<A>>= <<B>> <<C>> <<D>> @ comments on fragment D <<D>>= code fragment D @ final notes \$\endgroup\$
    – MetaEd
    Commented Mar 12 at 16:23
  • 1
    \$\begingroup\$ @Shawn And you forgot 202311L; --> It appears the C2X may be released in April 2024. Although the April 2023 draft has "Major changes in this fifth edition (STDC_VERSION 202311L) include:", that remains a working draft. I recommend we wait until C is updated before we assume 202311. \$\endgroup\$ Commented Mar 12 at 17:41
  • 2
    \$\begingroup\$ You do know that Literate Programming far predates Haskell, and was introduced by Knuth? The main C implementation remains the cweb system, consting of ctangle and cweave; see also github.com/ascherer/cweb \$\endgroup\$
    – tripleee
    Commented Mar 13 at 9:16

1 Answer 1

4
\$\begingroup\$

I'm going to ignore the larger questions about the history of Literate Programming and just address the issues behind "a program to delete commentary but preserve code blocks set off between specifiable marker lines". I made some of these observations in the comments to the question.

Using while (true) { earns several demerits. And using the const in const int c = … is pretty pointless too. In my opinion, it would be better and more idiomatic C to use:

int c;
while ((c = getopt_long(…)) != -1)
{
    …
}

In the help message, you use -o, --output=FILE to describe the output file option. Why isn't a similar notation used for -b, --begin and -e, --end (e.g. -b, --begin=start-mark and -e, --end=end-mark)?

The indentation in the #ifndef IO_DEF block in io.h is erratic — the #else and the next line should be indented more. That assumes that the indentation of preprocessing directives is desirable; I am not convinced, but it is a valid choice when done consistently.

I'm also unconvinced about including both the function declarations and the implementation in a single file. I don't think this is a good design decision. It becomes awkward to create a library containing the I/O code; you need to create an io.c file with an appropriate set of #define lines before including io.h.

I won't critique (or analyze) the code in io.h further (in part because the question states,"io.h has previously been reviewed"). I don't think you need anywhere near as much I/O code. You simply need to read lines — POSIX getline() is appropriate since you already assume POSIX support — and look at one line at a time. You keep track of the state — either processing commentary or processing code.

  • If the input line is a begin marker line, then switch to code mode.
  • If the input line is an end marker line, switch to comment mode.
  • If you're in comment mode, skip the line.
  • If you're in code mode, print the line.

You have to worry about invalid transitions — reading a start marker while in code mode, or reading an end marker in comment mode. You also need to warn if you reach EOF in code mode.

This design avoids the need to store the whole file in memory.

I don't think the check on argv is sensible. You wrote:

if (!argv) {
    fputs("A NULL argv[0] was passed through an exec system call.\n",
        stderr);
    return EXIT_FAILURE;
}

This checks whether the argv pointer is null (not whether the argv[0] pointer is null as the error message suggests). The C standard guarantees that argv will not be null; even if the system cannot identify the program name, argv[0] must point to an empty string.

C11 §5.1.2.2.1 Program startup ¶2

If they are declared, the parameters to the main function shall obey the following constraints:

  • The value of argc shall be nonnegative.
  • argv[argc] shall be a null pointer.
  • If the value of argc is greater than zero, the array members argv[0] through argv[argc-1] inclusive shall contain pointers to strings, which are given implementation-defined values by the host environment prior to program startup. The intent is to supply to the program information determined prior to program startup from elsewhere in the hosted environment. If the host environment is not capable of supplying strings with letters in both uppercase and lowercase, the implementation shall ensure that the strings are received in lowercase.
  • If the value of argc is greater than zero, the string pointed to by argv[0] represents the program name; argv[0][0] shall be the null character if the program name is not available from the host environment. If the value of argc is greater than one, the strings pointed to by argv[1] through argv[argc-1] represent the program parameters.
  • The parameters argc and argv and the strings pointed to by the argv array shall be modifiable by the program, and retain their last-stored values between program startup and program termination.

Your check is pointless — even if argc is 0, argv will not be a null pointer. argv[0] might be a null pointer, but the only code that has to worry about that is code that stashes the program name away for error reporting. If you want to check that argv[0] is not null, you need to write the correct test: if (argv[0] != NULL).

I'm not yet ready to use C23 features in code that I need to be able to run on machines that only have support for older versions of the standard. YMMV on that score. I'll leave your conditional definition and uses of nullptr alone, but your code doesn't use nullptr_t, so that can be omitted.

I ended up removing all the Boolean variables. This evades problems with <stdbool.h>. In C23, bool is a built-in type, true and false are keywords, and the header is not needed. For C99 through C18, the header should be used. Prior to C99, there was no <stdbool.h> header, but that shouldn't be a concern — you shouldn't be using compilers that do not support at least C99 and preferably C11 or C18. As of March 2024, the C23 standard has not been released by ISO. It will probably be ISO 9899:2024, but will probably continue to be known as C23.

You asked about using a structure for the control variables. In a one-file program like this, it is legitimate to use static file scope ('global') variables to convey global state information to functions. If I kept the structure, I might use a name such as struct Control. Note that you cannot initialize file stream variables (such as output) at file scope with stdin in modern C libraries, though you'd find such code written for older systems. Note that the main() function is the only symbol defined by the code that is visible outside the object file. I wish that static was the default visibility and that you had to use an explicit notation to make symbols visible outside the source file. However, a language with those rules would not be C.

The long_options argument to parse_options() isn't needed if you either move the definition of the array into the function or make it a file scope 'variable' placed near the definition of the short options string. You don't pass the short options as a parameter to parse_options() — consistency suggests one or the other, not a mixture.

You've almost certainly used programs that match the 'filter' paradigm (e.g. cat or grep). If one or more file names are present on the command line, those files are processed; otherwise, standard input is processed. This program should probably follow that pattern too. Granted, it is unlikely that more than one file will be processed because concatenated C files are not usually a good idea.

You have the code:

    if (f != stdout && ftruncate(fileno(f), 0)) {
        perror("seek()");

It is misleading to claim a seek error when ftruncate() fails. However, if you open the output file in "w" mode, the ftruncate() operation is superfluous.

If this were my code, I'd use the error reporting routines available in my SOQ (Stack Overflow Questions) repository on GitHub as files stderr.c and stderr.h in the src/libsoq sub-directory. On Linux, you can find loosely similar functionality documented as err(3). Either package simplifies error reporting.

This version of the code continues processing files even if one cannot be opened. It terminates the processing of the current file on an error, but continues to the next file. It is perfectly reasonable to decide to terminate on any error — in fact, it is often better to do that ("fail fast"), in my opinion.

Here's what I think will do the job for you:

/* Code Review: CR 209-991 */

#undef _POSIX_C_SOURCE
#undef _XOPEN_SOURCE

#define _POSIX_C_SOURCE 200819L
#define _XOPEN_SOURCE   700

#include <assert.h>
#include <errno.h>
#include <getopt.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

/* In C2X/C23 or later, nullptr is a keyword. */
/* Patch up C18 (__STDC_VERSION__ == 201710L) and earlier versions. */
#if !defined(__STDC_VERSION__) || __STDC_VERSION__ <= 201710L
#define nullptr ((void *)0)
#endif

static const char short_options[] = "b:e:o:h";
static const struct option long_options[] =
{
    { "begin",  required_argument,  nullptr,    'b' },
    { "end",    required_argument,  nullptr,    'e' },
    { "help",   no_argument,        nullptr,    'h' },
    { "output", required_argument,  nullptr,    'o' },
    { nullptr,  0,                  nullptr,     0  },
};
static const char *argv0 = "<unknown>";

#define DEFAULT_BEGIN   "\\begin{code}"
#define DEFAULT_END     "\\end{code}"

static const char *bflag = DEFAULT_BEGIN;       /* Begin flag */
static const char *eflag = DEFAULT_END;         /* End flag */
static FILE *output = NULL;                     /* Output file stream */

static void help(void)
{
    printf("Usage: %s [OPTIONS] SRC\n\n"
        "  %s - extract code from a LaTEX document.\n\n"
        "Options:\n"
        "  -b, --begin=MARKER   Line that denotes the beginning of the code\n"
        "                       block in the markup language. Default: %s.\n"
        "  -e, --end=MARKER     Line that denotes the end of the code block in\n"
        "                       the markup language. Default: %s.\n"
        "  -h, --help           Displays this message and exits.\n"
        "  -o, --output=FILE    Writes result to FILE instead of standard output.\n\n"
        "Note: The begin and end markers must be different.\n\n"
        "For Markdown, they can be:\n"
        "  ```python\n"
        "  # Some code here\n"
        "  ```\n",
        argv0, argv0, DEFAULT_BEGIN, DEFAULT_END);
    exit(EXIT_SUCCESS);
}

static void err_and_fail(void)
{
    fprintf(stderr, "The syntax of the command is incorrect.\n"
            "Use: %s -h for more information.\n", argv0);
    exit(EXIT_FAILURE);
}

static void parse_options(int argc, char **argv)
{
    int c;

    output = stdout;

    while ((c = getopt_long(argc, argv, short_options, long_options, nullptr)) != -1)
    {
        switch (c)
        {
        case 'e':
            eflag = optarg;
            break;
        case 'b':
            bflag = optarg;
            break;
        case 'h':
            help();
            break;
        case 'o':
            /* If -o was provided more than once. */
            if (output != stdout)
            {
                fprintf(stderr, "Error: Multiple -o flags provided.\n");
                err_and_fail();
            }
            output = fopen(optarg, "w");
            if (output == nullptr)
            {
                fprintf(stderr, "%s: failed to open file '%s' for writing: %d %s\n",
                        argv0, optarg, errno, strerror(errno));
                exit(EXIT_FAILURE);
            }
            break;
        default:
            err_and_fail();
            break;
        }
    }
    if (strcmp(eflag, bflag) == 0)
    {
        fprintf(stderr, "%s: the start and end markers must be different (both are '%s')\n", eflag, bflag);
        exit(EXIT_FAILURE);
    }
}

static int process(FILE *fp, const char *fn)
{
    size_t  buflen = 0;
    char   *buffer = 0;
    enum { COMMENT, CODE } mode = COMMENT;
    size_t  lineno = 0;
    size_t  num_code_blocks = 0;

    ssize_t length;
    while ((length = getline(&buffer, &buflen, fp)) > 0)
    {
        lineno++;
        assert(buffer[length] == '\0' && buffer[length - 1] == '\n');
        buffer[length - 1] = '\0';
        if (mode == CODE)
        {
            if (strcmp(buffer, bflag) == 0)
            {
                fprintf(stderr, "%s: error: found begin marker '%s' in file '%s' line %zu while in code mode\n",
                        argv0, bflag, fn, lineno);
                goto cleanup_and_fail;
            }
            else if (strcmp(buffer, eflag) == 0)
                mode = COMMENT;
            else if (fprintf(output, "%s\n", buffer) < 0)
            {
                fprintf(stderr, "%s: error: failed to write to output file while reading file '%s' line %zu\n",
                        argv0, fn, lineno);
                goto cleanup_and_fail;
            }
        }
        else
        {
            assert(mode == COMMENT);
            if (strcmp(buffer, eflag) == 0)
            {
                fprintf(stderr, "%s: error: found end marker '%s' in file '%s' line %zu while in comment mode\n",
                        argv0, eflag, fn, lineno);
                goto cleanup_and_fail;
            }
            else if (strcmp(buffer, bflag) == 0)
            {
                num_code_blocks++;
                mode = CODE;
            }
        }
    }

    free(buffer);

    if (mode != COMMENT)
    {
        fprintf(stderr, "%s: file '%s' is missing a code end marker '%s'\n", argv0, fn, eflag);
        return EXIT_FAILURE;
    }

    if (num_code_blocks == 0)
    {
        fprintf(stderr, "%s: file '%s' contained zero code blocks\n", argv0, fn);
        return EXIT_FAILURE;
    }

    return EXIT_SUCCESS;

cleanup_and_fail:
    free(buffer);
    return EXIT_FAILURE;
}

int main(int argc, char *argv[])
{
    int status = EXIT_SUCCESS;

    /* The test is superfluous */
    if (argv[0])
        argv0 = argv[0];

    parse_options(argc, argv);

    if (optind == argc)
        status = process(stdin, "standard input");
    else
    {
        for (int i = optind; i < argc; i++)
        {
            FILE *fp = fopen(argv[i], "r");
            if (fp == NULL)
            {
                fprintf(stderr, "%s: failed to open file '%s' for reading: %d (%s)\n",
                        argv0, argv[i], errno, strerror(errno));
                status = EXIT_FAILURE;
            }
            else
            {
                int rc = process(fp, argv[i]);
                if (status == EXIT_SUCCESS)
                    status = rc;
                fclose(fp);
            }
        }
    }

    if (fclose(output) != 0)
    {
        fprintf(stderr, "%s: failed to close output file\n", argv0);
        status = EXIT_FAILURE;
    }

    return status;
}

I copied your sample inputs into files litc-1.script and litc-2.script. My program was called cr-290991. The outputs I got were:

$ cr-290991 litc-1.script              
#include <stdio.h>

int main(void) {
    puts("Hello World!");
}
$ cr-290991 -o litc-2.c -b '```c' -e '```' litc-2.script
$ cat litc-2.c
#include <stdio.h>

int main(void) {
    puts("Hello World!");
}
$
\$\endgroup\$
4
  • \$\begingroup\$ "Re: It is misleading to claim a seek error when ftruncate() fails. However, if you open the output file in "w" mode, the ftruncate() operation is superfluous." ==> That's true. Yet there's often the case that one accidentally provides the same file for both input and output. For those select cases, I tend to open the file in append mode, read it, and then truncate it. I find that better than just destroying the output file and being left with nothing to read. \$\endgroup\$
    – Harith
    Commented Mar 28 at 23:31
  • \$\begingroup\$ assert(mode == COMMENT); ==> Why is this necessary? \$\endgroup\$
    – Harith
    Commented Mar 29 at 1:20
  • \$\begingroup\$ @Harith: at one level, it's not necessary. If it was necessary, it wouldn't be an assertion. OTOH, it ensures that mode has only one of the two values, COMMENT and CODE. There's an explicit test for CODE; the else clause assumes that the mode must be COMMENT, but the assertion ensures that the mode is COMMENT, at least when compiled without setting -DNDEBUG. It gave me reassurance that the code was behaving as expected. \$\endgroup\$ Commented Mar 29 at 2:54
  • \$\begingroup\$ Regarding the 'open in append mode; read; then truncate' — that's an interesting viewpoint. It makes sense. There are simpler ways to check whether the output file is the same as the existing file using POSIX functions such as stat(). pwd \$\endgroup\$ Commented Mar 29 at 3:06

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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