Standard input stream behaviour on Linux

  • Thread starter Tomás Ó hÉilidhe
  • Start date
A

Antoninus Twink

-- not a wonderful idea, but chacun à son goût --

If you're going to be as pretentious as to litter your posts with
snippets of French, at least do it correctly.

Either "à chacun son goût", or else "chacun a son goût" with no accent.
 
J

John Bode

I have a fully-portable C program (or at least I think I do). It works
fine on Windows, but malfunctions on Linux. I suspect that there's
something I don't know about the standard input stream that's causing
the problem.

Here's how I wrote the program originally:

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

void TrimNewLineOffEnd(char *const str)
{
    str[strlen(str) - 1] = 0;

}

int main(void)
{
    char buf[20];

    unsigned age, siblings;

    printf("What age are you? ");

    scanf("%u",&age);

    printf("How many siblings do you have? ");

    scanf("%u", &siblings);

    printf("What's your name? ");

    fgets(buf,15,stdin);

    TrimNewLineOffEnd(buf);

    printf("\n\nYour name is %s, you're %u years old and you have %u
siblings!\n\n",
           buf,age,siblings);

    return 0;

}

This original code didn't work as intended on either Windows or Linux.
On both systems, when control reached "fgets", the user wasn't given a
chance to enter their name; instead, fgets returned immediately with
an empty string.

To remedy this problem on Windows, I put in "fflush(stdin)" right
before the call to "fgets". This fixed the problem and the program
worked as intended. This didn't work on Linux however.

What am I doing wrong?

The problem is the calls to scanf() with the %u conversion specifier
leave the trailing newline from the last entry in the input stream,
which causes fgets() to return immediately.

Let's walk through the code. First you prompt for the age, and then
type in a number (say 42) and hit Return. The scanf() call with the
%u conversion specifier will skip over any leading whitespace, then
read and convert the '4' and '2' characters, stopping at the first non-
numeric character in the input stream, which in this case is the
newline character generated when you hit return. Then you type in the
number of siblings (say 2). Again, the scanf() call will skip over
any leading whitespace (the newline character left over from the last
entry), then read and convert the 2, stopping again at the trailing
newline, which is left in the input stream.

Now here's the problem. You call fgets(), and the first thing it sees
the newline left over from the last input, so it returns immediately
instead of waiting for you to enter a name. Calling scanf() with the
"%s" or "%c" conversion specifiers will have the same effect.

fflush(stdin) won't work, because fflush() is not defined on input
streams. You have to change how you're reading your inputs.

What I've found to work best is to read *everything* as a string using
fgets() (since it will consume the trailing newline), then convert and
assign as necessary. It makes for more robust error handling.

Example (untested - no warranties express or implied):

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

/**
* Since the integer values we're reading are likely very
* small (most people don't live past 999 years old or have
* more than 999 siblings), we're constraining our input
* buffer to hold 3 characters + a newline character +
* the 0 terminator.
*/
#define BUFSIZE 3

/**
* Get a number from user input
*
* @param prompt [in] -- prompt string
* @param val [out] -- the desired value
* @return int -- 1 if value successfully read and converted
* 0 if error
*/
int getUnsignedValue(const char *prompt, usigned int *val)
{
char inBuf[BUFSIZE]; // input buffer
char *chk; // used for error checking
int ret = 0;

printf("%s : ", prompt);
fflush(stdout);

if (fgets(inBuf, sizeof inBuf, stdin))
{
/**
* first, look for a newline in the buffer; if it's
* not there, then the user entered a string that was
* longer than we were expecting and we need to
* reject it
*/
char *newline = strchr(inBuf, '\n');
if (!newline)
{
/**
* newline not found, string too long. However,
* we don't want to leave garbage in the input
* stream so we'll read until we see the newline
* or until there's a read error
*/
while (!newline && fgets(inBuf, sizeof inBuf, stdin))
{
newline = strchr(inBuf, '\n');
}
fprintf(stderr, "Error -- Input too long\n");
}
else
{
/**
* String was not too long, so we attempt to convert
* it to an unsigned integer using the strtoul()
* function.
*/
*val = (unsigned) strtoul(inBuf, &chk, 10);
if (!isspace(*chk) || *chk != 0)
{
fprintf(stderr, "Error -- input was not a valid number
\n");
}
else
{
ret = 1; // Success! val now contains the converted
value
}
}
}
else
{
fprintf(stderr, "Error occurred while reading from stdin\n");
}

return ret;
}

int main(void)
{
unsigned age, sibs;
char name[15];

if (!getUnsignedValue("What is your age", &age))
{
/* handle error */
}

if (!getUnsignedValue("How many siblings do you have", &sibs))
{
/* handle error */
}

printf("What is your name? ");
fflush(stdout);
if (fgets(name, sizeof name, stdin))
{
char *newline = strchr(name, '\n');
if (newline)
*newline = 0;

printf("Your name is %s, you are %u years old and you have %u
siblings\n");
}
else
{
fprintf(stderr, "Error while reading name from stdin\n");
}

return 0;
}

Obviously there are better ways to structure the code to give the user
opportunities to re-enter data. This is just a quick-n-dirty to show
how much interactive input can suck.
 
J

John Bode

         printf("Your name is %s, you are %u years old and you have %u
siblings\n");

And the other fuckup. Obviously, there should be three additional
arguments to that call:

printf("Your name is %s, you are %u years old and you have %u
siblings\n",
name, age, sibs);
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

Forum statistics

Threads
473,997
Messages
2,570,241
Members
46,831
Latest member
RusselWill

Latest Threads

Top