Line-wise input, EOF handling, and behavioral differences between implementations of getchar

I've implemented a menu system where all input calls ultimately go through io_getline, a function which reads a line of input up to the max buffer size or newline (the rest of the line is discarded if it exceeds the buffer size) and returns the number of characters written, or -1 if an EOF is encountered. When an EOF is encountered, io_getline will, from then on, permanently be in a state where the first character it attempts to read yields EOF (which is not a character) and it will always return -1. This is intentional, as I will soon demonstrate:

/* Does the same thing as getchar but with guaranteed EOF persistence */
static int
getchar_eof(void)
{
unsigned char ch;
static int eof_encountered;

if(eof_encountered)
return EOF;

if(feof(stdin)){
eof_encountered = 1;
return EOF;
}

return ch;
}

ssize_t
io_getline(char *buffer, size_t bufsz)
{
int ch;
char *bufp = buffer;

while((ch = getchar_eof()) != EOF && ch != '\n')
if(buffer != NULL && (size_t)(bufp - buffer) < bufsz)
*bufp++ = ch;

if(ch == EOF)
return -1;

return bufp - buffer;
}

/* two functions that use io_getline: */

int
{
(void)unused;
ssize_t nwritten;

showprompt();

if((nwritten = io_getline(menustate.input_str, 32)) == -1)
return CBRET_EOF; // causes caller to end the program

if(nwritten == 0)
return CBRET_RETURNTOPARENT;

return CBRET_DONOTHING;
}

void
waitcontinue(void)
{
printf("Press Enter to continue.");
io_getline(NULL, 0);
io_clearscreen();
}


A little bit about the menu structure: each page has three callbacks that are called in order: entry, getter and handler. The entry's job is to do anything that needs to be done before getting user input, the getter's job is to get input (str, u32, char_or_u32, and none) from the user, and handle EOF by ending the program, and the handler's job is to interpret the input obtained by the getter (if any). waitcontinue is called in some handler functions for the purpose of pagination, while generic_getter_str is one of the four getter functions.

As waitcontinue is not a getter, it does not handle EOF, but EOF will cause it to finish (this is expected). And this design decision is where I thankfully discovered a weird nuance with getchar, wherein it doesn't always persist EOF status. It is possible for getchar to return EOF, be called again, and prompt the user for input post-EOF. I managed to produce this behavior on both a repl.it Linux VM (repl.it/languages/c, without being logged in) and minGW on Windows (where I discovered it).

This goes against my understanding that getchar should constantly return EOF after the first time. The result of this is that any time waitcontinue is called in an environment where getchar behaves in this way, pressing ctrl-d (or ctrl-z enter if on Windows), the program will receive the EOF, skip the one waitcontinue call, and then continue to run, when the intent is for it to "bubble out" until a getter inevitably receives the EOF and ends the program.

This led me to re-implement getchar to guarantee the behavior that I want and expect, and it works fantastically, however, this is fascinating to me, and I now wonder if there are other nuances I might be missing here.

• The project this is from is mpassw. A Metroid password generator I've been writing for the past couple weeks. It works on both Linux and Windows (MinGW). I have not tested it with a more windows-y compiler like MSVC, nor have I tested it on Mac. But I have no reason to believe it wouldn't work just fine on OS X. Nov 22 at 19:01
• Terminology problem: "first character it reads is EOF" - but EOF is not a character. I'm not merely being pedantic here, because that's the thinking that leads to char c = getchar(), for example, or the idea that EOF is a single "thing" that can be consumed (or "received"). Nov 23 at 7:23
• True, I was burned by char c = getchar() some years ago, which was how I learned that you really do need an int, as EOF is defined in terms of int and is out of range for even unsigned char. Poor wording on my part. I'd edit the question to mention what the manpage says, but I see an earlier post on my profile mentioning that apparently there's a rule against that. I imagine the rule is about preserving the code as it was first posted so that the answers make sense, but now that you've mentioned the terminology, I'm not sure if it's appropriate to change it. Nov 24 at 21:29
• I'll go ahead and make the edit since 1. it's not a code change and 2. the onus is a comment rather than an answer Nov 24 at 21:35
• "I thankfully discovered a weird nuance with getchar, wherein it doesn't always persist EOF status." --> this is either non-conforming C behavior or a false positive. Nov 27 at 23:03

Redundant code

getchar_eof() essentially mimics getchar() and feof(). It fails to work well when ferror() is true or stdin is re-opened.

getchar() already returns EOF when stdin end-of-file flag is set.

Instead use, getchar(), feof(), ferror().

Bug

int getchar_eof(void) returns junk (indeterminant data) when a rare input error encountered as the return value of fread() was not used.

Bug

io_getline() returns -1 when end-of-file occurs, even if some data was read. To be like fgets(), only return -1 when 1) end-of-file occurs and nothing read or 2) input error.

Non-portable type

ssize_t is not defined in standard C.

generic_getter_str() misnamed

generic_getter_str() does not get a string (hinted by the _str) as no null character terminated array is gotten.

io_getline(buffer, ...) does not form a string in buffer as it lacks a null character.

Repetitive test

if(buffer != NULL && (size_t)(bufp - buffer) < bufsz) repeated tests buffer != NULL. Once is enough.

if(buffer != NULL) bufsz == 0;  // Test once
while((ch = getchar_eof()) != EOF && ch != '\n')
if((size_t)(bufp - buffer) < bufsz)
*bufp++ = ch;


Candidate alternative - untested.

int io_getline(size_t bufsz, char buffer[bufsz]) {
if (bufsz > INT_MAX) {
bufsz = INT_MAX;  // Or some other error handling
}
if (buffer == NULL) {
bufsz = 0;
}

int ch;
char *bufp = buffer;

while ((ch = getchar()) != EOF) {
if (ch == '\n') {
break;
}
if ((size_t) (bufp - buffer) + 1u < bufsz) {
*bufp++ = (char) ch;
}
}

if (ch == EOF) {
if (feof(stdin)) {
return -1;
}
} else {
return -1; // Input error
}
}

if ((size_t) (bufp - buffer) < bufsz) {
*bufp = '\0';  // Form a string if able
}

return (int) (bufp - buffer);
}

• "io_getline() returns -1 when end-of-file occurs, even if some data was read." this is actually not a bug. The input is expected to come from a buffered commandline, i.e. a user types a line of input and terminates it by hitting enter. A line terminated by EOF is not a valid line, so it is discarded, just like excess input beyond the buffer size. Nov 28 at 0:44
• And generic_getter_str is not misnamed. Just because it's not a null-terminated string doesn't make it not a string. The function used to be named generic_getter_char, but after the introduction of generic_getter_char_or_u32, (single char or u32), it no longer made sense to keep it named char. I suppose I could name it _charvec. That said, in the wider context of the program, I make a point to avoid using null-terminated strings as data structures, and thus avoid str* lib functions in favor of their mem* counterparts. Nov 28 at 0:45
• The only string functions that appear in the program are used with string literals, and when a string function has to interact with one of my buffers, an intermediary buffer is used to keep their behavior under control (there is only one such case) Nov 28 at 0:53
• @BradenBest "I make a point to avoid using null-terminated strings" --> C does not specify any context where a string is not null character terminated. C lib does define "A string is a contiguous sequence of characters terminated by and including the first null character." Of course you can use string in some other context, but then that risks misunderstanding with the spec. and with users as evidenced by these comments. Nov 28 at 1:55
• @BradenBest "The input is expected to come from a buffered commandline, i.e. a user types a line of input and terminates it by hitting enter." --> OK, define it as you wish, yet it still differs from fgets(), a function commonly used to get lines and does serve as a model for many get line like code. IIRC, *nix getine() also does not discard a last line lacking a '\n'. Nov 28 at 1:57