How to swap characters in a file specified (file name and characters) in the command line?

Viewed 184

I'm learning about file commands and trying to write some programs. I want to specify two characters in command line, where the second one will replace the first one, every time the first character is found. The command line input would be [program_name].exe [file].txt [old_char] [new_char]. I came across this thread which had the same problem and I tried to fix my code by looking at the answer, but it creates an infinite loop.

int main(int argc, char *argv[])
{
    FILE *fp;
    char *string = {"Hellx"};
    char c;
    if ((fp = fopen(argv[1], "w")) != NULL)
    {
        fputs(string, fp);
        printf("Successfully written.\n");
        fclose(fp);

        if ((fp = fopen(argv[1], "r+")) != NULL)
        {
            while ((c = fgetc(fp)) != EOF)
            {
                if (c == *argv[2])
                {
                    fseek(fp, ftell(fp) - 1, SEEK_SET);
                    fprintf(fp, "%c", *argv[3]);
                    printf("Successfully changed.\n");
                }
            }
            fclose(fp);
        }
        else
            printf("Error on opening!");
    }
    else
        printf("Error on writing!");
    return 0;
}

So the output for this would be: Helloelloelloelloelloelloello..., while it should just change x to o. What's the problem with this code?

3 Answers

Your code does not work because you do not call fseek() or rewind() when switching from writing back to reading. Also note that you do not need to call ftell() to step back: you can use -1L and SEEK_CUR. It is also safer to open the file in binary more for this kind of file patching in place.

Furthermore, fgetc() returns an int value that does not fit in a char. Use int type for c to detect EOF reliably. Also note that the byte value returned by fgetc() when successful is the value of an unsigned char so comparing it to a char might fail for non-ASCII bytes.

Here is a modified version:

#include <stdio.h>

int main(int argc, char *argv[]) {
    FILE *fp;
    const char *string = { "Hellx" };

    if (argc < 4) {
        fprintf("missing command line arguments\n");
        return 1;
    }

    char *filename = argv[1];
    unsigned char c1 = argv[2][0];
    unsigned char c2 = argv[3][0];

    if ((fp = fopen(filename, "w")) == NULL) {
        fprintf(stderr, "cannot open %s for writing\n", filename);
        return 1;
    }
    fputs(string, fp);
    printf("Successfully written.\n");
    fclose(fp);
    
    if ((fp = fopen(filename, "rb+")) == NULL) {
        printf("Cannot reopen %s\n", filename);
        return 1;
    }
    while ((c = fgetc(fp)) != EOF) {
        if (c == c1) {
            fseek(fp, -1L, SEEK_CUR);
            fputc(c2, fp);
            fseek(fp, 0L, SEEK_CUR);
            printf("Successfully changed.\n");
        }
    }
    fclose(fp);
    return 0;
}

The man page for fopen(), in the section which discusses file opening modes, says

When the "r+", "w+", or "a+" access type is specified, both reading and writing are enabled (the file is said to be open for "update"). However, when you switch from reading to writing, the input operation must encounter an EOF marker. If there is no EOF, you must use an intervening call to a file positioning function. The file positioning functions are fsetpos, fseek, and rewind. When you switch from writing to reading, you must use an intervening call to either fflush or to a file positioning function.

(my bolding)

So I suggest adding

fflush(fp);

after the fprintf() statement, as no repositioning is needed.


As mentioned, you also should change the type to

int c;

so that EOF -1 can be distinguished from data 0xFF.

The reasons for why your program is failing were already extensively debated and solved in other answers and comments.

Is there a short way to do this without "r+" and without fflush?

There is, more than one, here is an example where the file is opened to read and write, this way there is no need to always be opening and closing it, using w+ flag, it will also create the file if it doesn't exist:

#include <stdio.h>

int main(int argc, char *argv[])
{
    FILE *fp;
    int c;
    char *string = {"Hellx"};
    if (argc > 3)
    {
        if ((fp = fopen(argv[1], "w+")) != NULL) // open to write and read
        {

            fputs(string, fp);
            printf("Successfully written.\n");           
            rewind(fp); // back to the beginning of the file
            while ((c = fgetc(fp)) != EOF)
            {
                if(c == argv[2][0]) // if the character exists...
                { 
                    fseek(fp, -1, SEEK_CUR);
                    fprintf(fp, "%c", argv[3][0]); // replace
                    printf("Successfully changed.\n");
                    fseek(fp, 0, SEEK_CUR);
                }
            }
            fclose(fp);
        }
        else
            fprintf(stderr, "Error on writing!");
    }
    else
        fprintf(stderr, "Too few arguments!");
}

Footnote:

I agree with William Pursell and Weather Vane, a more robust way to do this would be to use two different files.

Related