How to read string from keyboard (including all newlines) using C?

Viewed 84

The following will use lorem.txt as the test file:

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

I have the following code meant to count lines, words, and characters in a file (trying to imitate the wc in Linux):

#include <stdio.h>

int main(){
    char data[500032];  // assigns 500KB of space for input string
    if (fgets(data, sizeof data, stdin)) {
        char *ptr = &data[0];  // initializes pointer at first character
        int count = 0;  // total character count
        int d1_count = 0;  // newline count
        int d23_count = 0;  // ' ' and '\t' count

        while (*ptr){
            char d1 = '\n';
            char d2 = ' ';
            char d3 = '\t';
            count++;  // counts character
            if (*ptr == d1){
                d1_count++; // counts newline
            }
            if (*ptr == d2 || *ptr == d3) {
                d23_count++;  // counts spaces or tabs
            }
            ptr++;  // increments pointer
        }
        printf("%d %d %d\n", d1_count, d23_count+1, count-1);
    }
}

In my Linux terminal, I use gcc -o wordc wordc.c to compile and then ./wordc < lorem.txt

However, I get 1 69 445 (1 line, 69 words, and 445 characters). This is the number of lines, words, and characters for the first paragraph only. I am expecting 7 lines, 207 words, and 1342 characters.

I assume what is happening is C stops reading the file once it finds a newline. How do I get it to stop doing this?

As an aside- I feel like assigning 500KB of space for a string is a bit hacky and wasteful. Are there any good ways to assign only as much space as I need?

Any help would be appreciated

2 Answers

Change the line

if (fgets(data, sizeof data, stdin)) {

to

while (fgets(data, sizeof data, stdin)) {

so that you are reading one line per loop iteration.

You will also have to move the lines

int count = 0;  // total character count
int d1_count = 0;  // newline count
int d23_count = 0;  // ' ' and '\t' count

outside the loop, because you want to remember these values between loop iterations.

You will also want to move the line

printf("%d %d %d\n", d1_count, d23_count+1, count-1);

outside the loop if you only want to print that line only once, instead of once per loop iteration.

I feel like assigning 500KB of space for a string is a bit hacky and wasteful. Are there any good ways to assign only as much space as I need?

The buffer must only be sufficiently large to store a single line. It does not have to store the entire file at once. Therefore, it would probably be sufficient to use a significantly smaller buffer.

Although it would be possible to use a dynamically allocated buffer (using malloc) and resize the buffer as necessary (using realloc), in this case, it is probably not necessary.

Since you stated in the question that you are using Linux, an alternative would be to use the POSIX-specfic function getline, which handles most of the memory management for you.

I have rewritten your program to use getline:

#include <stdio.h>
#include <stdlib.h>

int main() {
    char *data = NULL;
    size_t data_capacity = 0;
    int count = 0;  // total character count
    int d1_count = 0;  // newline count
    int d23_count = 0;  // ' ' and '\t' count

    while ( getline( &data, &data_capacity, stdin ) >= 0 ) {
        char *ptr = &data[0];  // initializes pointer at first character

        while (*ptr){
            char d1 = '\n';
            char d2 = ' ';
            char d3 = '\t';
            count++;  // counts character
            if (*ptr == d1){
                d1_count++; // counts newline
            }
            if (*ptr == d2 || *ptr == d3) {
                d23_count++;  // counts spaces or tabs
            }
            ptr++;  // increments pointer
        }
    }

    free( data );

    printf("%d %d %d\n", d1_count, d23_count+1, count-1);
}

With the input specified in the question, this program has the following output:

5 205 1339

This output is not quite correct, because you are counting the number of spaces in your program, not the number of words. You seem to be attempting to compensate for this by adding 1 to the number of spaces when printing that value. However, this is not sufficient. The exact solution depends on several factors, for example how you want to handle words that are split by a hyphen and a newline character, i.e. whether you want to count such words as one word or two words. However, since this is not the problem that you stated in the question, I will not address that issue.

EDIT: "Illusions" I did misinterpret the appearance of the highlight selector to copy/paste the sample strings from the OP to work with that data. What this highlight showed as the LF at the end of lines 1 & 3, and not on line 5, I took to mean an 'invisible SP' (or other control character) on 2 lines. Back-to-front! The shorter highlight on line 5 merely showed the absence of a LF at the end of that line. Wrong interpretation on my part.

This answer has been revised with this new insight.

It's tough to say what goes on when there are so many variables all interacting with one another.

Counting whitespace characters and fiddling their values is not the way to determine what would be recognised as a "word" (perhaps with a full stop attached)

Here's a version that is more trustworthy. It doesn't muck about reading from a file. One could use "external data" replacing indexing into this long string with c = fgetc( stdin ); (and ungetc() in the "read ahead" groping for the end of a word.)

#include <stdio.h>
#include <ctype.h>

char *in =
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. "
"Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. "
"Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. "
"Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
"\n"
"\n"
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. "
"Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. "
"Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. "
"Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
"\n"
"\n"
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. "
"Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. "
"Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. "
"Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
;

int main() {
    int lcnt = 0, wcnt = 0, ccnt = 0;

    for( int i = 0; in[i]; i++ ) {
        ccnt++;
        if( in[i] == '\n' )
            lcnt++;
        if( !isspace( in[i] ) ) {
            wcnt++;
            while( in[i+1] && !isspace( in[i+1] ) ) ccnt++, i++;
        }
    }

    printf( "%d lines, %d words, %d chars\n", lcnt, wcnt, ccnt );
    return 0;
}

And this is the output

4 lines, 207 words, 1339 chars

EDIT: Obviously, counting LF is insufficient. Better code coming soon

// 207 / 3 = 69 words per populated line.

//   445x? + 1xLF
// +   1xLF
// + 445x? + 1xLF
// +   1xLF
// + 445x?
// = 1339

EDIT2: Coming back to this, it seems the answer could do with improvement.

Dealing with the correct copy/paste of the OP data, the following gives the expected results. It accounts for the missing LF on line 5, and it is branchless.

b tracks "intraword" characters, and f is a flag set by transition at the beginning of a word.

Invocations of function isspace() could be replaced with c==' ' || c=='\t'... for more speed, if desired, and the ++ moved to the for() loop's 3rd part.

int main() {
    int lcnt = 0, wcnt = 0, ccnt = 0;

    for( int f, b = 0; in[ ccnt ]; b = f ) {
        lcnt += in[ ccnt ] == '\n';
        f = !isspace( in[ ccnt++ ] );
        wcnt += !b && f;
    }
    // humans don't notice if final LF present or not
    lcnt += (in[ ccnt - 1] != '\n');

    printf( "%d lines, %d words, %d chars\n", lcnt, wcnt, ccnt );
    return 0;
}
Related