Basic noob question re console input

Discussion in 'C Programming' started by Robbie Brown, Feb 12, 2014.

  1. Robbie Brown

    Robbie Brown Guest

    It's been many (many) years since I had to write a console application.
    I want to be able to get a single integer from a console prompt.
    I initially thought I needed to flush stdin when I had what I wanted but
    this is apparently a big no no, so I hacked it.

    I have the following code

    int getChoice(){

    int num = 0, choice = 0;
    char c;

    do{
    printf("%s", "Enter an integer >> ");
    num = scanf("%d", &choice);
    while(c = getchar() != '\n');
    }
    while(num == 0);

    return choice;
    }

    This code ignores characters and multiple integers and simply fetches
    the first integer if there is one, embedded integers are ignored and
    discarded, the only time an integer is returned is if it's the first
    item in the stream.

    What are your opinions of this code. This must be such a common
    requirement that it has been solved countless times in the past.
     
    Robbie Brown, Feb 12, 2014
    #1
    1. Advertisements

  2. Robbie Brown

    James Kuyper Guest

    fflush() "... causes any unwritten data for that stream to be delivered
    to the host environment to be written to the file;" (7.21.5.2p2) What
    exactly did you expect the behavior would be on an input-only stream?
    With only the above description you might conclude that flush(stdin) is
    a no-op, since there is not, and never will be, any unwritten data in
    the buffer. However, elsewhere in the same clause the behavior in that
    case is explicitly specified to be undefined. It's also undefined for an
    input-output stream if the most recent operation performed on that
    stream was input.
    That code is written to ignore characters - scanf("%d") returns 0 if the
    next non-white-space character isn't a digit. If you don't want it to
    ignore characters, but instead to treat them as errors, one alternative
    is to retrieve one character at a time, and check for isdigit() and
    isspace(). You'll have to assemble the digits into a buffer, and pass
    that to sscanf() or strtod() to determine the corresponding integer,
    when the buffer is ready.
    Unfortunately, while it has been solved, there's no guaranteed portable
    C solution. All C streams have a buffering mode; what you want is
    unbuffered mode (_IONBF). It's up to each implementation what kind of
    buffering stdin has; it's often line-buffered, which means that no data
    it received by the program until a new-line has been typed. The behavior
    you describe is consistent with line-buffering.

    You can change the mode to _IONBF using setvbuf(), but it is
    implementation-defined what kinds of changes you can make. You might or
    might not be able to make that change to stdin.

    Two different functions, both named getch(), but with different
    interfaces, are available on two different operating systems that I'm
    familiar with for dealing with this problem. Look for getch() in the
    documentation for your system. Keep in mind that use of this function
    restricts the portability of your code to those systems that support it
    (with the same interface).
     
    James Kuyper, Feb 12, 2014
    #2
    1. Advertisements

  3. Robbie Brown

    Eric Sosman Guest

    `fflush(stdout);' would be a good idea here. See Question 12.4
    on the comp.lang.c Frequently Asked Questions (FAQ) page at
    You're not doing anything with `c' (which will be assigned
    either 0 or 1), so why bother with it?

    More seriously, if getchar() returns EOF (indicating end-of-
    input or an error), you'll be stuck here forever ...
    scanf() itself can also return EOF if there's an input error
    (as opposed to a non-integer input), in which case `num' will be
    non-zero but `choice' won't contain anything useful.
    The general feeling is that scanf() is very difficult to use
    with interactive input, so difficult that it's probably not worth
    trying. FAQ 12.20 has a fuller explanation, and suggestions for
    alternative approaches.
     
    Eric Sosman, Feb 12, 2014
    #3
  4. I'll try not to duplicate remarks but I think some thing remain to be
    said...
    You are missing parentheses. This repeatedly sets c to 0 or 1 depending
    on whether the character read is '\n' or not. You meant:

    int c; /* Explained already */
    while ((c = getchar()) != '\n');

    You see I've also moved the declaration of c. I like to give names the
    very smallest scope. If I were forced to used a compiler that did not
    allow mixed declarations and statements (it's in C >= C99), I'd still
    move it to after the 'do {' line.
    <snip>
     
    Ben Bacarisse, Feb 12, 2014
    #4
  5. Should be `int c;`; see below.
    As Eric mentioned, adding `fflush(stdout);` here is not a bad idea
    (though it's moderately likely to work without it).
    A more descriptive name than "num" would help, perhaps "items".
    You're missing parentheses and a check against EOF.

    EOF is the reason that c needs to be an int rather than a char.
    getchar() returns *either* the character just read, treated as an
    unsigned char and then converted to int, *or* EOF, which is a negative
    value (and therefore not the same as any valid unsigned char value).

    So the above should be:

    while ((c = getchar()) != '\n' && c != EOF);
    while (num != 1);
    Or if it's preceded by whitespace.

    scanf("%d", ...) skips any leading whitespace (including newlines),
    then reads a decimal integer optionally preceded by '-' or '+'.
    Any non-digits are left in the input stream. If it's able to read
    an integer, it returns 1 (more generally the number of items read).
    If the input doesn't contain a valid integer, it returns 0 (no
    items read). If there's another error, it returns EOF.
     
    Keith Thompson, Feb 12, 2014
    #5
  6. One more thing: Make this:

    int getChoice(void) {

    The empty parentheses are an obsolescent form of function definition.
    In the form you've written, it correctly says that getChoice has no
    parameters, but not that callers should call it with no arguments.
    If you called

    n = getChoice("superfluous argument");

    the compiler would not complain, and the behavior would be undefined.

    (Empty parentheses are valid in C++, and have a different meaning.)
     
    Keith Thompson, Feb 12, 2014
    #6
  7. Why not something like

    int getChoice() {
    char buf[128];
    char *endptr;
    int r;

    do {
    printf("%s", "Enter an integer >> ");
    fflush(stdout);
    if (fgets(buf, sizeof(buf), stdin) == NULL) {
    printf("EOF\n");
    return -1;
    }
    r = (int)strtol(buf, &endptr, 10);
    } while (*endptr != '\r' && *endptr != '\n');

    return r;
    }

    Mike.
     
    Miquel van Smoorenburg, Feb 13, 2014
    #7
  8. Robbie Brown

    Kaz Kylheku Guest

    Any situation in which scanf is used for user input is a poor approach.
    Consider buffering an entire line of input from the user using fgets
    and then using sscanf or other approaches to extract tokens.

    You're neglecting to check the return value of scanf to see how many
    conversions were successful.

    You're neglecting to check the return value of getchar for EOF.

    Your program will get into an infinite loop if the user invokes EOF
    from the terminal.

    The type char is not suitable for capturing the return value of getchar. The
    getchar function returns int, so you're invoking a potentially data-losing
    conversion which prodices an implementation-defined result.

    getchar can return values in the range 0 to UCHAR_MAX, plus the special
    value EOF which is some negative number.

    There is no requirement that the value of EOF fits into the type char.
    (For that matter, the plain char type need not be a signed type.)
    Due to the neglect of the return value of scanf, it also thinks it fetches the
    first integer if there *isn't* one.

    scanf is not particularly robust. If you scan a %d, but the input doesn't fit
    into an int, there is no error checking, just undefined behavior. The atoi
    function has the same problem.
     
    Kaz Kylheku, Feb 13, 2014
    #8
  9. Robbie Brown

    Eric Sosman Guest

    The do..while catches matching failures, although it doesn't
    handle actual I/O errors or end-of-input correctly.
     
    Eric Sosman, Feb 13, 2014
    #9
  10. There's no need to check for '\r' (unless you need to handle "foreign"
    files, such as Windows-format text files on a UNIX system). Text input
    translates native line endings to '\n'.
     
    Keith Thompson, Feb 13, 2014
    #10
  11. Robbie Brown

    Kaz Kylheku Guest

    Not to mention that it asks the user for an integer, but rejects zero.
     
    Kaz Kylheku, Feb 13, 2014
    #11
  12. Robbie Brown

    BartC Guest

    I long ago decided to deal only with line-oriented input for this sort of
    task. So:

    - Read one line of text into a string buffer. The user needs to press Enter
    to complete the line (for more interaction, you need a different approach).

    - Read actual input from this buffer. If it hits end-of-string, it might
    return 0 if trying to read a number, or "" if reading a name or string.
    (Note that fgets() might put a newline at the end; I would strip this out as
    it just gets in the way.)

    The point is that once it's in a string, then you can forget about the
    intricacies of C input processing, and use any string processing techniques
    to extract the data. (Someone mentioned sscanf(), but even atoi() will do.)

    However, there has been a lot of discussion here in the past about the best
    way to write a 'getline' function, because stdin could come from a file (or
    you could use this to read from files), and a file could potentially contain
    one line with billions of characters - it gets complicated. I would
    completely ignore that problem and just use a hard limit for line length to
    keep things simple. (I've used such limits for several decades with little
    mishap.)
     
    BartC, Feb 13, 2014
    #12
  13. Are you sure? The code a couple of messages up does not seem to reject
    zero.
     
    Ben Bacarisse, Feb 13, 2014
    #13
  14. Robbie Brown

    Robbie Brown Guest

    It doesn't reject 0, 0 is a valid input and is currently used to exit
    the application
     
    Robbie Brown, Feb 13, 2014
    #14
  15. Robbie Brown

    Kaz Kylheku Guest

    You're right; I got mixed up between num and choice when looking
    at the code a second time.
     
    Kaz Kylheku, Feb 13, 2014
    #15
  16. Robbie Brown

    Kaz Kylheku Guest

    By the way, it's necessary to check the return value of -1, not only 0:

    switch(scanf(<single item string>, &whatever)) {
    case -1: ... /* EOF before any conversion was made */
    case 0: ... /* no conversion was made */
    case 1: ... /* one conversion was made */
    }

    Consider this program:

    #include <stdio.h>

    int main()
    {
    int d;
    printf("%d\n", scanf("%d", &d));
    return 0;
    }

    Test runs on a Linux machine. (Invisible TTY input shown in square brackets):

    $ ./a.out
    [Ctrl-D][Enter]
    -1

    $ ./a.out
    abc
    0

    $ ./a.out
    42
    1

    As you can see, if scanf is not able to read a single character in converting
    %d, it returns -1. If there is junk, then this counts as a botched conversion
    and the count of successful conversions is returned.
     
    Kaz Kylheku, Feb 13, 2014
    #16
  17. Robbie Brown

    Robbie Brown Guest

    First of all thanks for all the thoughts and ideas regarding this problem
    I have come up with a solution.
    I fully expect to have missed all sorts of things but it works perfectly
    as far as I can tell on 64 bit Linux/gcc/c99

    The requirements are as follows
    input int >> foo //fail, try again
    input int >> foobarbaz //fail, try again
    input int >> f65oo //fail, try again
    input int >> 9 //success, returns 9
    input int >> 56tr //success, returns 56
    input int >> q 34 fsdd //fail, try again
    input int >> 999 //success, return 999
    input int >> 9999 //success, return 999
    input int >> 0 //success, return 0
    input int >> 999xyz //success, return 999
    input int >> 999 //success, return 99
    input int >> 999 //success, return 9
    input int >> 999 //fail, try again
    etc etc

    so, allow user to enter an int from 0 to 999
    ignore larger numbers and trailing non digits
    and input with 1 or more non-digit prefix.

    The problem has been the data remaining on the input stream
    after a failure, the following solves that problem in a single line
    I have included fflush(stdout) but I don't understand why I need it yet,
    that's my next lesson.

    int getChoice2(void){

    int BUFSIZE = 4;
    char buffer[BUFSIZE];
    int count = 0, choice = 0;

    while(count == 0 || count == EOF){

    printf("%s", "Input an integer >> ");
    fflush(stdout);
    if(NULL != fgets(buffer, BUFSIZE, stdin)){
    count = sscanf(buffer, "%d", &choice);
    stdin[0]._IO_read_ptr = stdin[0]._IO_read_end;
    }
    }

    return choice;
    }


    calm down, it's C, "you can do anything you want" :)
     
    Robbie Brown, Feb 14, 2014
    #17
  18. Robbie Brown

    Eric Sosman Guest

    I don't see why you expect (or want) three different outcomes
    for three almost-identical inputs. In any case, the code you post
    will give the same outcome ("success, return 999") for all three
    and will not treat them differently.
    Did you read the FAQ you were referred to? Question 12.4.
    This might work on some C implementations some of the time,
    but it is not guaranteed to work on all implementations, nor even
    all of the time on yours. If you want to get rid of unconsumed
    input reliably, you need to consume it -- i.e., read it.
    Subject to your employer's right to fire your sorry ass for
    foolishness, of course. (Not even the self-employed are safe
    from such sanctions.)

    When I was young and dumb I thought it was clever to write
    clever code. Now that I'm old and battle-scarred, I know that
    writing clever code is one of the stupider things one can do.
    Think upon this, Grasshopper.
     
    Eric Sosman, Feb 14, 2014
    #18
  19. Robbie Brown

    Eric Sosman Guest

    Oh, ratz: I looked at the wrong piece of code: The revised
    code you posted will in fact treat them differently -- although
    the "Why would you want this?" still remains on open question.

    Also: In the revised version, the loop

    .... keeps on trying after end-of-input. That may well leave you
    stuck forever should end-of-input (or I/O error) actually occur.
     
    Eric Sosman, Feb 14, 2014
    #19
  20. Robbie Brown

    Robbie Brown Guest

    Yes, of course, but I don't understand *exactly* why it's required, as
    in, I haven't seen for myself, but I'll get there.
    Well I can't comment on that as I only have a Linux box here. I think I
    have an old Windows machine around somewhere, maybe I'll dig it out
    although the processor architecture is likely the same.

    Anyway, if you look at how

    getchar();
    fgetc();
    scanf();

    and

    fgets();

    work they all work by advancing the read pointer if they successfully
    read one or or more characters, advancing the read pointer 'by hand'
    simply simulates a read ... it's just much, much faster. I'm currently
    investigating what happens to the pushback pointer etc
    Yes master ... but keep grasping those pebbles, they'll be in my hand
    before you know it :)
     
    Robbie Brown, Feb 14, 2014
    #20
    1. Advertisements

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 (here). After that, you can post your question and our members will help you out.