3
\$\begingroup\$

A little program I wrote because I had nothing better to do: a README reader.
Run readme to print the first lines of the README file (or README.md or README.org...) either in the current directory or on a directory up in the directory hierarchy.
Run readme -e to edit that file.

Manual:

README(1)                   General Commands Manual                  README(1)

NAME
       readme - README reader

SYNOPSIS
       readme [-cep] [dir]

DESCRIPTION
       readme displays to the standard output the header of the first found
       README* file in the current directory or in a directory up in the
       directory hierarchy.  If a directory is given as argument, use it in
       place of the current directory.

       The options are as follows:

       -c     Cat mode.  Rather than display only the head of the README file,
              displays it entirely.

       -e     Editor mode.  Edit the README file rather than displaying it.

       -p     Pager mode.  Display the README file in pager.

       All options are mutually exclusive.

ENVIRONMENT
       The following environment variables affect the execution of readme

       EDITOR, VISUAL
              Specifies an editor to use.  If both EDITOR and VISUAL are set,
              VISUAL takes precedence.  If neither EDITOR nor VISUAL are set,
              the default is vi(1).

       PAGER  Specifies the pager to use.  If it is not set, use more(1).

                                                                     README(1)

code:

#include <ctype.h>
#include <err.h>
#include <glob.h>
#include <limits.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>

#define MAXLINES  22
#define MAXCHARS  1024
#define DEFEDITOR "vi"
#define DEFPAGER  "more"
#define VISUAL    "VISUAL"
#define EDITOR    "EDITOR"
#define PAGER     "PAGER"

enum {
    MODE_HEADER,
    MODE_CAT,
    MODE_EDITOR,
    MODE_PAGER
};

/* show usage and exit */
static void
usage(void)
{
    (void)fprintf(stderr, "usage: readme [-cep] [dir]\n");
    exit(1);
}

/* search for a README* file in dir or up in the directory hierarchy */
static char *
readme(char *file, char *dir)
{
    glob_t g;
    char *s;

    for (;;) {
        (void)snprintf(file, PATH_MAX, "%s/README*", dir);
        if (glob(file, 0, NULL, &g) != 0)
            return NULL;
        if (g.gl_pathc > 0) {
            (void)snprintf(file, PATH_MAX, "%s", g.gl_pathv[0]);
            file[PATH_MAX - 1] = '\0';
            globfree(&g);
            return file;
        } else {
            globfree(&g);
        }
        if ((s = strrchr(dir, '/')) != NULL)
            *s = '\0';
        else
            break;
    }
    return NULL;
}

/* print the header of file */
static void
cat(const char *file, int head)
{
    FILE *fp;
    int nlines, nchars;
    int ch, prev;
    int blank;

    if ((fp = fopen(file, "r")) == NULL)
        err(1, "%s", file);
    prev = '\0';
    blank = 0;
    nchars = nlines = 0;
    while ((ch = getc(fp)) != EOF) {
        if (iscntrl((unsigned char)ch) && !isspace((unsigned char)ch))
            continue;
        if (ch == '\n' && head) {
            nlines++;
            if (prev == '\n') {
                blank++;
            }
        }
        if (head) {
            nchars++;
            if (blank > 1 || nchars >= MAXCHARS || nlines >= MAXLINES)
                return;
            prev = ch;
        }
        putchar(ch);
    }
}

/* call editor on file */
static void
editor(const char *file)
{
    char *prog;

    if ((prog = getenv(VISUAL)) == NULL)
        if ((prog = getenv(EDITOR)) == NULL)
            prog = DEFEDITOR;
    execlp(prog, prog, file, NULL);
}

/* call pager on file */
static void
pager(const char *file)
{
    char *prog;

    if ((prog = getenv(PAGER)) == NULL)
        prog = DEFPAGER;
    execlp(prog, prog, file, NULL);
}

/* readme: print README file for current project */
int
main(int argc, char *argv[])
{
    int mode;
    int ch;
    char dir[PATH_MAX];
    char file[PATH_MAX];

    mode = MODE_HEADER;
    while ((ch = getopt(argc, argv, "cep")) != -1) {
        switch (ch) {
        case 'c':
            mode = MODE_CAT;
            break;
        case 'e':
            mode = MODE_EDITOR;
            break;
        case 'p':
            mode = MODE_PAGER;
            break;
        default:
            usage();
            break;
        }
    }
    argc -= optind;
    argv += optind;
    if (argc > 1)
        usage();
    if (argc == 1) {
        if (realpath(*argv, dir) == NULL) {
            err(1, "%s", *argv);
        }
    } else if (getcwd(dir, sizeof(dir)) == NULL) {
        err(1, NULL);
    }
    if (readme(file, dir) == NULL)
        return 1;
    switch (mode) {
    case MODE_HEADER:
        cat(file, 1);
        break;
    case MODE_CAT:
        cat(file, 0);
        break;
    case MODE_EDITOR:
        editor(file);
        break;
    case MODE_PAGER:
        pager(file);
        break;
    }
    return 0;
}
\$\endgroup\$
1
\$\begingroup\$

Looks good here. You've avoided some of the usual traps (e.g. passing plain char to <ctype.h> functions), so well done.

Some things I'd suggest changing:

(void)fprintf(stderr, "usage: readme [-cep] [dir]\n");

fprintf() isn't usually tagged "nodiscard", so I wouldn't be explicit about ignoring the return value unless your local coding standard really insists on it. Consider using fputs() instead for a single constant string.

exit(1);

That makes it hard to reuse the same function for explicitly-requested help.

glob_t g;
char *s;

Modern C allows mixing declarations and statements, so consider moving the declarations to where they are initialised, which removes some opportunity for error. In any case, the scope can be reduced to within the body of the loop, which helps clarify that the values are independent in each iteration.

    if (glob(file, 0, NULL, &g) != 0)
        return NULL;

Oops - we missed globfree() from that code path. And this prevents searching parent directories (because we get GLOB_NOMATCH if not found in current dir).

(void)snprintf(file, PATH_MAX, "%s/README*", dir);

Instead of ignoring the return value here, we should be checking that it is strictly less than PATH_MAX, and abort with ENAMETOOLONG instead of silently truncating.

    if (g.gl_pathc > 0) {
        ⋮
        return file;
    } else {
        globfree(&g);
    }

Given that the first block leaves the function, the else case can simply follow:

    if (g.gl_pathc > 0) {
        ⋮
        return file;
    }
    globfree(&g);
    if ((s = strrchr(dir, '/')) != NULL)

This explicit comparison to NULL is very wordy - almost Java-esque. Trust the C language definition that all non-null pointers are true:

    char *s = strrchr(dir, '/');
    if (!s) {
        break;
    }
    *s = '\0';

I would add a big fat warning that the function modifies dir rather than trusting that the absence of const makes that obvious.

With all the checking we really need, it might be simpler to just change our working directory in the search function, and return a relative file name. This tail-recursive implementation works and is much more robust:

/* search for a README* file in dir or up in the directory hierarchy */
/* file must point to storage of at least PATH_MAX chars */
/* dir will be modified, and the current working directory updated */
/* returns null on any error, or if no matches found */
static char *
readme(char *file)
{
    glob_t g;
    int status = glob(FILEPATTERN, 0, NULL, &g);
    if (status == GLOB_NOMATCH) {
        globfree(&g);
        char curr_dir[2]; /* big enough for "/" but nothing else */
        if (getcwd(curr_dir, sizeof curr_dir) || errno != ERANGE) {
            /* this is the root directory */
            return NULL;
        }
        /* else recurse up to parent directory */
        if (chdir("..")) {
            err(EXIT_FAILURE, "chdir");
        }
        return readme(file);
    }
    if (status) {
        err(EXIT_FAILURE, "glob");
    }
    assert(g.gl_pathc > 0);
    assert(strlen(g.gl_pathv[0]) < PATH_MAX);
    strcpy(file, g.gl_pathv[0]);
    globfree(&g);
    return file;
}

It seems strange that we use exec() for editing and paging, but want to reimplement cat and head ourselves. There's a slight complication in that we need a pipeline of two head processes to limit both lines and characters, but that's easily arranged (I'm assuming GNU head here, since POSIX doesn't standardise head -c):

    int pp[2];
    if (pipe(pp) < 0) {
        err(1, "pipe");
    }
    int child = fork();
    if (child < 0) {
        err(1, "fork");
    }
    if (!child) {
        /* read file and writes to pipe */
        close(pp[0]);
        dup2(pp[1], 1);
        execlp("head", "head", "-c", MAXCHARS, file, NULL);
    } else {
        /* read pipe and write to stdout */
        close(pp[1]);
        dup2(pp[0], 0);
        execlp("head", "head", "-n", MAXLINES, NULL);
    }

With separate functions for head and cat, we have a unified interface, and can replace the enum with a function pointer:

void (*handler)(const char*) = head;
int ch;
while ((ch = getopt(argc, argv, "cep")) != -1) {
    switch (ch) {
    case 'c':
        handler = cat;
        break;
    case 'e':
        handler = editor;
        break;
    case 'p':
        handler = pager;
        break;

It seems a bit odd that we can invoke the program with multiple contradictory arguments. But it's a valid choice, and it's consistent with some standard programs (e.g. that's what head does if given both -c and -n arguments).


Modified code

Incorporating all the improvements I suggested above:

#include <assert.h>
#include <err.h>
#include <errno.h>
#include <glob.h>
#include <limits.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>

#define FILEPATTERN "README*"
#define MAXLINES  "22"
#define MAXCHARS  "1024"
#define DEFEDITOR "vi"
#define DEFPAGER  "more"
#define VISUAL    "VISUAL"
#define EDITOR    "EDITOR"
#define PAGER     "PAGER"

/* show usage to appropriate stream */
static int
usage(int return_code)
{
    fputs("usage: readme [-cep] [dir]\n", return_code ? stderr : stdout);
    return return_code;
}

/* search for a README* file in dir or up in the directory hierarchy */
/* file must point to storage of at least PATH_MAX chars */
/* dir will be modified, and the current working directory updated */
/* returns null on any error, or if no matches found */
static char *
readme(char *file)
{
    glob_t g;
    int status = glob(FILEPATTERN, 0, NULL, &g);
    if (status == GLOB_NOMATCH) {
        globfree(&g);
        char curr_dir[2]; /* big enough for "/" but nothing else */
        if (getcwd(curr_dir, sizeof curr_dir) || errno != ERANGE) {
            /* this is the root directory */
            return NULL;
        }
        /* else recurse up to parent directory */
        if (chdir("..")) {
            err(EXIT_FAILURE, "chdir");
        }
        return readme(file);
    }
    if (status) {
        err(EXIT_FAILURE, "glob");
    }
    assert(g.gl_pathc > 0);
    assert(strlen(g.gl_pathv[0]) < PATH_MAX);
    strcpy(file, g.gl_pathv[0]);
    globfree(&g);
    return file;
}

/* print the header of file */
static void
head(const char *file)
{
    int pp[2];
    if (pipe(pp) < 0) {
        err(1, "pipe");
    }
    int child = fork();
    if (child < 0) {
        err(1, "fork");
    }
    if (!child) {
        /* read file and writes to pipe */
        close(pp[0]);
        dup2(pp[1], 1);
        execlp("head", "head", "-c", MAXCHARS, file, NULL);
    } else {
        /* read pipe and write to stdout */
        close(pp[1]);
        dup2(pp[0], 0);
        execlp("head", "head", "-n", MAXLINES, NULL);
    }
}

/* print the whole file */
static void
cat(const char *file)
{
    execlp("cat", "cat", file, NULL);
}

/* call editor on file */
static void
editor(const char *file)
{
    const char *prog = getenv(VISUAL);
    if (!prog) {
        prog = getenv(EDITOR);
    }
    if (!prog) {
        prog = DEFEDITOR;
    }
    execlp(prog, prog, file, NULL);
}

/* call pager on file */
static void
pager(const char *file)
{
    const char *prog = getenv(PAGER);
    if (!prog) {
        prog = DEFPAGER;
    }
    execlp(prog, prog, file, NULL);
}



typedef void (*handler)(const char*);

/* readme: print README file for current project */
int
main(int argc, char *argv[])
{
    void (*handler)(const char*) = head;
    int ch;
    while ((ch = getopt(argc, argv, "cep")) != -1) {
        switch (ch) {
        case 'c':
            handler = cat;
            break;
        case 'e':
            handler = editor;
            break;
        case 'p':
            handler = pager;
            break;
        case 'h':
            return usage(EXIT_SUCCESS);
        default:
            return usage(EXIT_FAILURE);
        }
    }
    argc -= optind;
    argv += optind;
    if (argc > 1) {
        return usage(EXIT_FAILURE);
    }

    if (argc == 1 && chdir(*argv)) {
        err(EXIT_FAILURE, "chdir");
    }

    char file[PATH_MAX];
    if (!readme(file)) {
        fputs("No README file found\n", stderr);
        return EXIT_FAILURE;
    }

    handler(file);
    /* if we get here, exec() failed */
    err(EXIT_FAILURE, "exec");
}
\$\endgroup\$

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.