简体   繁体   中英

Print contents of a file in reverse line order

So i've been running into issues trying to get my program to print output correctly. I have a text file with some content in it, and want to print it from the bottom up.

    fptr = fopen("file.txt", "r");

    fseek(fptr, 0, SEEK_END);
    count = ftell(fptr);

    while (i < count)
    {
            i++;
            fseek(fptr, -i, SEEK_END);
            printf("%c", fgetc(fptr));
    }

    printf("\n");
    fclose(fptr);

A sample output from this would be

Input:

Hello
My name is

Output:

si eman yM
olleH

What I want the output to be:

My name is
Hello

How you're doing it, reading a character and seeking backwards one, is inefficient.

The Perl module File::Readbackwards is a good read. Perl's IO is very close to C, and the code is well commented.

The basic algorithm is to read and buffer blocks, and find lines within that block, but starting at the end of the file and going backwards.

  1. Open the file.
  2. Seek to the end.
  3. Seek backwards to the previous block.
  4. Read that block into a buffer.

Once you have that buffer, scan backwards through the buffer until you find a newline. Now you have a complete line. If you fail to find a newline, read the previous block and try again.


That's non-trivial, so here's how you'd do it reading backwards one character at a time until you see a newline. I've used the fgets interface to get one line at a time.

char *fgets_backwards( char *str, int size, FILE *fp ) {
    /* Stop if we're at the beginning of the file */
    if( ftell(fp) == 0 ) {
        return NULL;
    }

    int i;
    /* Be sure not to overflow the string nor read past the start of the file */
    for( i = 0; ftell(fp) != 0 && i < size; i++ ) {
        /* Back up one character */
        fseek(fp, -1, SEEK_CUR);
        /* Read that character */
        str[i] = (char)fgetc(fp);

        /* We have the whole line if we see a newline, except at the start.
           This happens before we back up a character so the newline will
           appear on the next line. */
        if( str[i] == '\n' && i != 0 ) {
            break;
        }

        /* Back up the character we read. */
        fseek(fp, -1, SEEK_CUR);
    }

    /* Null terminate, overwriting the previous line's newline */
    str[i] = '\0';

    return str;
}

These lines will come out backwards, so reverse them. That's simple enough.

void reverse( char *start ) {
    size_t len = strlen(start);

    for( char *end = &start[len-1]; start < end; start++, end-- ) {
        char tmp = start[0];
        start[0] = end[0];
        end[0] = tmp;
    }
}

Putting it all together...

fseek( fp, 0, SEEK_END );

char line[1024];
while( fgets_backwards( line, 1024, fp ) != NULL ) {
    reverse(line);
    printf("%s", line);
}

Note, I was sloppy about error checking. Each call to fseek should be checked.


NOTE : I wrote this part before the OP clarified what they wanted. Oh well, it's still pretty cool.

Now that you have a complete line, you can tackle reversing it separate from reading the file.

char *reverse_by_word( char *string ) {
    size_t len = strlen(string);

    /* Allocate enough space to store string, and a null */
    char *reversed = malloc( len * sizeof(char) );

    /* Initialize reversed to be an empty string so strcat knows where to start */
    /* There's no need to initialize the rest of the string,
    /* the garbage from malloc will be overwritten */
    reversed[0] = '\0';

    /* Read the string backwards, character by characer */
    for( int i = (int)len - 1; i >= 0; i-- ) {
        /* If we see a space... */
        if( isspace( string[i] ) ) {
            /* Add the word after it to reversed */
            strcat( reversed, &string[i+1] );
            /* Faithfully reproduce the whitespace after the word */
            strncat( reversed, &string[i], 1 );
            /* Chop the string off at the space */
            string[i] = '\0';
        }
    }

    return reversed;
}

This is the destructive version, string gets chopped up with null bytes. It's possible to do this non-destructively and I might edit it later.

Since input and reversed are the same length, using strcpy with no bounds is safe.

We can test this works and faithfully reproduces all the whitespace.

#include <assert.h>

int main() {
    char input[] = " Hello  My\tname is ";

    char *reversed = reverse_by_word(input);
    printf( "'%s'\n", reversed );
    assert( strcmp(reversed, " is name\tMy  Hello ") == 0 );
}

Here's the non-destructive version. Basically the same idea, but instead of marking where we've already printed with null bytes, we remember it in last_idx .

char *reverse_by_word( const char *string ) {
    size_t len = strlen(string);

    char *reversed = malloc( len * sizeof(char) );
    reversed[0] = '\0';

    /* Read the string backwards, character by characer */
    int last_idx = (int)len;
    for( int i = (int)len - 1; i >= 0; i-- ) {
        /* If we see a space... */
        if( isspace( string[i] ) ) {
            /* Add the word before it, stop at the last word we saw. */
            strncat( reversed, &string[i+1], last_idx - i - 1);
            /* Faithfully reproduce the whitespace. */
            strncat( reversed, &string[i], 1 );

            /* Remember the last place we printed up to. */
            last_idx = i;
        }
    }

    return reversed;
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM