5
\$\begingroup\$

Recently I thought about what a program would look like if all its characters that could become trigraphs became trigraphs. For example

int main(void)
{
    int array[10];
}

would become

int main(void)
??<
    int array??(10??);
??>

As a result I decided to make a program using ANSI C89 that takes a file in and converts each character to its corresponding trigraph if it has one and outputs the result to another file.

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

#define BUFFER_SIZE 1024
#define WRITE_BUFFER(x, size)                                            \
    do {                                                                 \
        memcpy(buffer + BUFFER_SIZE + write_buffer_length, (x), (size)); \
        write_buffer_length += (size);                                   \
    } while(0)                                                           \

int main(int argc, char **argv)
{
    int j;

    /* ignore the first argument */
    --argc;
    ++argv;

    /* check for invalid arguments */
    if(argc % 2 != 0) {
        fprintf(stderr, "Error: invalid arguments\n");
        fprintf(stderr, "Usage: %s input output ...\n", argv[-1]);
        fprintf(stderr, "Example: %s main.c result.c \n", argv[-1]);
        return -1;
    }

    for(j = 0; j < argc; j += 2) {
        char buffer[BUFFER_SIZE + BUFFER_SIZE * 3];
        FILE *read_file, *write_file;
        size_t bytes_read = BUFFER_SIZE;

        if(strcmp(argv[j], argv[j + 1]) == 0) {
            printf("Warning: using the same file for input and output is not supported\n");
            continue;
        }

        /* open a file for reading */
        if((read_file = fopen(argv[j], "r")) == NULL) {
            fprintf(stderr, "Error: could not open %s\n%s", argv[j], strerror(errno));
            return -2;
        }

        /* open a file for writing */
        if((write_file = fopen(argv[j + 1], "w")) == NULL) {
            fprintf(stderr, "Error: could not open %s\n%s", argv[j + 1], strerror(errno));
            return -3;
        }

        /* read the file in BUFFER_SIZE chunks */
        while (bytes_read == BUFFER_SIZE) {
            size_t i, write_buffer_length;
            bytes_read = fread(buffer, 1, BUFFER_SIZE, read_file);

            /* process each character in the buffer
             * and if needed convert it to a trigraph
             */

            write_buffer_length = 0;
            for (i = 0; i < bytes_read; ++i) {
                char const ch = buffer[i];

                switch (ch) {
                    case '#':
                        WRITE_BUFFER("\?\?=", 3);
                        break;
                    case '[':
                        WRITE_BUFFER("\?\?(", 3);
                        break;
                    case ']':
                        WRITE_BUFFER("\?\?)", 3);
                        break;
                    case '{':
                        WRITE_BUFFER("\?\?<", 3);
                        break;
                    case '}':
                        WRITE_BUFFER("\?\?>", 3);
                        break;
                    case '\\':
                        WRITE_BUFFER("\?\?/", 3);
                        break;
                    case '^':
                        WRITE_BUFFER("\?\?'", 3);
                        break;
                    case '~':
                        WRITE_BUFFER("\?\?-", 3);
                        break;
                    case '|':
                        WRITE_BUFFER("\?\?!", 3);
                        break;
                    default:
                        WRITE_BUFFER(&ch, 1);
                        break;
                }
            }

            fwrite(buffer + BUFFER_SIZE, 1, write_buffer_length, write_file);
        }

        fclose(read_file);
        fclose(write_file);
    }

    return 0;
}
\$\endgroup\$
4
\$\begingroup\$

Your program could be a lot shorter and simpler if you followed the "Unix philosophy."

  • Read from stdin and write to stdout. This eliminates your need to process arguments, and eliminates the for(j) loop.

  • Trust fread and fwrite to do I/O buffering on their own. (They do.)

Then your entire program would be something like

#include <stdio.h>

void trigraph(char c) {
    putchar('?');
    putchar('?');
    putchar(c);
}

int main() {
    int c;
    while ((c = getchar()) != EOF) {
        switch (c) {
            case '\\': trigraph('/'); break;
            case '#': trigraph('='); break;
            [...]
            default: putchar(c); break;
        }
    }
}

Then if you want to transform a bunch of files at once, that's as easy as

for i in *.c ; do ./a.out < "$i" > "${i%.c}.trigraphed.c" ; done

For more on the Unix philosophy of writing simple "pipeline" programs that compose well, I recommend the book Software Tools, by Kernighan and Plauger; or for that matter The C Programming Language, by Kernighan and Ritchie.

For an additional exercise, you might try writing a program to reverse this operation: replace every trigraph with its corresponding ASCII character.

| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ For fun, it would be interesting to also see this code with trigraphs applied. \$\endgroup\$ – chux - Reinstate Monica Oct 25 at 15:27
  • \$\begingroup\$ Well, you can run it through itself. ;) But that does remind me: perhaps the cases should cutely be written as case '??/': trigraph('/'); break; case '??=': trigraph('='); break; and so on. This would be either more self-documenting or less self-documenting, depending on your point of view. :) \$\endgroup\$ – Quuxplusone Oct 25 at 15:32
1
\$\begingroup\$
  • There is absolutely no reason why WRITE_BUFFER should be a macro and not a function.

  • Don't do exotic stuff like --argc; ++argv;. C programmers expect argument 0 to be the name of the executable. Similarly, argc % 2 is weird, why would you check for an even amount of arguments? Check for an exact amount of arguments, no more, no less.

  • Similarly, you should be able to keep all error handling out of the loops and just do it once. Keep it simple.

  • I'd expect the various valid trigraph sequences to be stored in some manner of table, rather than a big switch. For example, you could sacrifice 128 bytes data for a fast lookup table based on 7 bit ASCII:

    const char trigraph [128] = 
    {
      ['#'] = '=',
      ['['] = '(',
      ...
    };
    
    char in  = ... // input from file
    char out = trigraph[in];
    if(out != 0) // was it a candidate for trigraph replacement?
    {  
      // printf("??%c", out); etc
    
| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ the reason I check for an even amount of arguments is so every file read has a corresponding file to write to and that will not be true when there is an odd amount of arguments. \$\endgroup\$ – nullptr Oct 26 at 15:00
  • \$\begingroup\$ @nullptr And why would someone ever use the program with more than 2 files? That's not a very good UI and you gain no performance from it either, since the massive bottleneck here is the file i/o and everything else is negligible. It's a potential security hole too. \$\endgroup\$ – Lundin Oct 27 at 7:25

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.