RSS Amplifier

Zach Berwaldt on zachberwaldt.dev · Jan 2, 2025

Improving minigrep

0
Sign in to vote or save

Zach Berwaldt · Zach Berwaldt

This article assumes you read my article that covered a very basic implementation of the grep tool called minigrep. I figure most readers aren’t interested in going back to read it, so here where we left our code last:

#include <stdio.h>
#include <string.h>

int main(int argc, char * argv[])
{
    if (argc < 3) {
        printf("Usage: %s <pattern> <path>\n", argv[0]);
        return 1;
    }

    char * query = argv[1];
    char * path = argv[2];

    FILE * file = fopen(path, "r");
    if (!file) {
        printf("error opening file");
        return 1;
    }

    char line[1024];
    while (fgets(line, sizeof(line), file)) {
        if (strstr(line, query)) {
            printf("%s", line);
        }
    }

    fclose(file);
    return 0;
}

Plan

We are going to make the following refactors to our program:

  1. define a config structure
  2. wrap search into a function.
  3. print errors to stderr

We could do more than this, but that is the nature of many projects: There is always more to do. Something that I struggle with occasionally is determining where the line of diminishing returns is. For this article we will focus on these.

Let’s begin.

Define A Config Structure.

I want to create a data type that will describe and hold the options for my program. Most languages have their own ways of defining types, but in C the method I will be using is the struct or “structure”.

#include <stdio.h>
#include <string.h>

+ typedef struct {
+    const char * query;
+    const char * path;
+ } Config;

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

To put it simply I just created a structure called Config and it has two fields or attributes: The pattern I am searching for and the path of the file I am searching. In my starting code I had these pieces of information in their own variables.

    // ... snip

    char * query = argv[1];
    char * path = argv[2];

    // ... snip

You may be asking “why bother?” and it’s a fair question. I simply like grouping the config into a struct because, in a way, it’s self documenting. In a small program like this one, maybe that isn’t much of a value-add. In bigger projects it will be important. So I am just practicing good habits.

One last note, I could have defined my struct like this:

struct Config {
    const char * query;
    const char * path;
};

It’s perfectly valid to do that, but then I would have to use struct Config any time I am trying to use it.

Using The Config.

Now that I have a struct I need to go and use it in the rest of my code.

// ...snip

typedef struct {
   char * query;
   char * path;
} Config;

int main(int argc, char * argv[])
{
    if (argc < 3) { // ... snip }

-   char * pattern = argv[1];
-   char * path = argv[2];
+   Config config = { .query = argv[1], .path = argv[2] };

-   FILE * file = fopen(path, "r");
+   FILE * file = fopen(config.path, "r");
    if (!file) {
        printf("error opening file");
        return 1;
    }

    char line[1024];
    while (fgets(line, sizeof(line), file)) {
-       if (strstr(line, query)) {
+       if (strstr(line, config.query)) {
            printf("%s", line);
        }
    }

    // ... snip
}

Wrap Search Into A Function.

Next I want to move my searching logic into a function. This function is going to need two arguments: the query and the file contents. Note that I am designating my query as const This is just to denote that I am not going to modify the query in the function. Think of it as saying “this argument is readonly”.


+void search(const char * query, FILE * contents)
+{
+}

Then I’m simply going to move my while loop that does the searching into the body of that function:


void search (const char * query, FILE * contents)
{
+   char line[1024];
-   while (fgets(line, sizeof(line), file)) {
+   while (fgets(line, sizeof(line), contents)) {
-       if (strstr(line, config.query)) {
+       if (strstr(line, query)) {
+           printf("%s", line);
+       }
+   }
}


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

-   char line[1024];
-   while (fgets(line, sizeof(line), file)) {
-       if (strstr(line, config.query)) {
-           printf("%s", line);
-       }
-   }
+   search(config.query, file);

    // ... snip
}

Print Errors To stderr

Currently when I have some problem I just print it to output with printf with everything else, particularly my results if I have any. That’s not what I want. Instead I want to send my errors to a different output called stderr. This way if I am sending my results to a file, for example, my errors are not mingled in with them.

#include <stdio.h>
#include <string.h>

typedef struct {
    char * query;
    char * path;
} Config;

void search(char * const query, FILE * contents)
{
    char line[1024];
    while (fgets(line, sizeof(line), contents)) {
        if (strstr(line, query)) {
            printf("%s", line);
        }
    }
}

int main(int argc, char * argv[])
{
    if (argc < 3) {
-       printf("Usage: %s <pattern> <path>\n", argv[0]);
+       fprintf(stderr, "Usage: %s <pattern> <path>\n", argv[0]);
        return 1;
    }

    Config config = { .query = argv[1], .path = argv[2] };

    FILE * file = fopen(config.path, "r");
    if (!file) {
-       printf("error opening file");
+       perror("fopen");
        return 1;
    }

    search(config.query, file);

    fclose(file);
    return 0;
}

Conclusion

That wraps up this next version of the minigrep tool.

Some additional changes I could make, and cover in a future article:

  • add case insensitive search
  • add line numbers to output

Here is the final code:

#include <stdio.h>
#include <string.h>

typedef struct {
    const char * query;
    const char * path;
} Config;

void search(const char * query, FILE * contents)
{
    char line[1024];
    while (fgets(line, sizeof(line), contents)) {
        if (strstr(line, query)) {
            printf("%s", line);
        }
    }
}

int main(int argc, char * argv[])
{
    if (argc < 3) {
        fprintf(stderr, "Usage: %s <pattern> <path>\n", argv[0]);
        return 1;
    }

    Config config = { .query = argv[1], .path = argv[2] };

    FILE * file = fopen(config.path, "r");
    if (!file) {
        perror("fopen");
        return 1;
    }

    search(config.query, file);

    fclose(file);
    return 0;
}

Read the original on zachberwaldt.dev

Comments

Nothing yet. Say the first thing.

    Sign in to join the conversation.