Search code examples
parsingstreamstdind

How can I read input after the wrong type has been entered in D readf?


I am wondering how to continue using stdin in D after the program has read an unsuitable value. (for example, letters when it was expecting an int) I wrote this to test it:

import std.stdio;


void main()
{
    int a;
    for(;;){
        try{
            stdin.readf(" %s", a);
            break;
        }catch(Exception e){
            writeln(e);
            writeln("Please enter a number.");
        }
    }
    writeln(a);
}

After entering incorrect values such as 'b', the program would print out the message indefinitly. I also examined the exception which indicated that it was trying to read the same characters again, so I made a version like this:

import std.stdio;


void main()
{
    int a;
    for(;;){
        try{
            stdin.readf(" %s", a);
            break;
        }catch(Exception e){
            writeln(e);
            writeln("Please enter a number.");
            char c;
            readf("%c", c);
        }
    }
    writeln(a);
}

Which still threw an exception when trying to read a, but not c. I also tried using stdin.clearerr(), which had no effect. Does anyone know how to solve this? Thanks.


Solution

  • My recommendation: don't use readf. It is so bad. Everyone goes to it at first since it is in the stdlib (and has been since 1979 lol, well scanf has... and imo i think scanf is better than readf! but i digress), and almost everyone has trouble with it. It is really picky about formats and whitespace consumption when it goes right, and when it goes wrong, it gives crappy error messages and leaves the input stream in an indeterminate state. And, on top of that, is still really limited in what data types it can actually read in and is horribly user-unfriendly, not even allowing things like working backspacing on most systems!

    Slightly less bad than readf is to use readln then strip and to!int it once you check the line and give errors. Something like this:

    import std.stdio;
    import std.string; // for strip, cuts off whitespace
    import std.algorithm.searching; // for all
    import std.ascii; // for isAscii
    import std.conv; // for to, does string to other type conversions
    
    int readInt() {
            for(;;) {
                    string line = stdin.readln();
                    line = line.strip();
                    if(all!isDigit(line))
                            return to!int(line);
                    else
                            writeln("Please enter a number");
            }
            assert(0);
    }
    
    
    void main()
    {
        int a = readInt();
        writeln(a);
    }
    

    I know that's a lot of import spam (and for a bunch of individual trivial functions too), and readln still sucks for the end user, but this little function is going to be so much nicer on your users and on yourself than trying to use readf. It will consistently consume one line at a time and give a nice message. Moreover, the same pattern can be extended to any other type of validation you need, and the call to readln can be replaced by a call to a more user-friendly function that allows editing and history and stuff later if you decide to go down that route.


    If you must use readf anyway though, easiest way to make things sane again in your catch block is still to just call readln and discard its result. So then it just skips the whole line containing the error, allowing your user to start fresh. That'd also drop if they were doing "1 2" and wanted two ints to be read at once... but meh, I'd rather start them fresh anyway than try to pick up an errored line half way through.